merged branch schmittjoh/processIdleTimeout (PR #8651)

This PR was merged into the master branch.

Discussion
----------

adds ability to define an idle timeout

This adds the ability to define an idle timeout which in contrast to the current timeout considers only the time since the last output was produced by a process.

It also adds a special exception for timeout cases.

Commits
-------

b922ba2 adds ability to define an idle timeout
This commit is contained in:
Fabien Potencier 2013-08-03 08:07:08 +02:00
commit e4da1956a2
3 changed files with 171 additions and 15 deletions

View File

@ -0,0 +1,69 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Process\Exception;
use Symfony\Component\Process\Process;
/**
* Exception that is thrown when a process times out.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ProcessTimedOutException extends RuntimeException
{
const TYPE_GENERAL = 1;
const TYPE_IDLE = 2;
private $process;
private $timeoutType;
public function __construct(Process $process, $timeoutType)
{
$this->process = $process;
$this->timeoutType = $timeoutType;
parent::__construct(sprintf(
'The process "%s" exceeded the timeout of %s seconds.',
$process->getCommandLine(),
$this->getExceededTimeout()
));
}
public function getProcess()
{
return $this->process;
}
public function isGeneralTimeout()
{
return $this->timeoutType === self::TYPE_GENERAL;
}
public function isIdleTimeout()
{
return $this->timeoutType === self::TYPE_IDLE;
}
public function getExceededTimeout()
{
switch ($this->timeoutType) {
case self::TYPE_GENERAL:
return $this->process->getTimeout();
case self::TYPE_IDLE:
return $this->process->getIdleTimeout();
default:
throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType));
}
}
}

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Process;
use Symfony\Component\Process\Exception\InvalidArgumentException;
use Symfony\Component\Process\Exception\LogicException;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Exception\RuntimeException;
/**
@ -44,7 +45,9 @@ class Process
private $env;
private $stdin;
private $starttime;
private $lastOutputTime;
private $timeout;
private $idleTimeout;
private $options;
private $exitcode;
private $fallbackExitcode;
@ -231,7 +234,7 @@ class Process
throw new RuntimeException('Process is already running');
}
$this->starttime = microtime(true);
$this->starttime = $this->lastOutputTime = microtime(true);
$this->stdout = '';
$this->stderr = '';
$this->incrementalOutputOffset = 0;
@ -795,6 +798,7 @@ class Process
*/
public function addOutput($line)
{
$this->lastOutputTime = microtime(true);
$this->stdout .= $line;
}
@ -805,6 +809,7 @@ class Process
*/
public function addErrorOutput($line)
{
$this->lastOutputTime = microtime(true);
$this->stderr .= $line;
}
@ -835,19 +840,29 @@ class Process
/**
* Gets the process timeout.
*
* @return integer|null The timeout in seconds or null if it's disabled
* @return float|null The timeout in seconds or null if it's disabled
*/
public function getTimeout()
{
return $this->timeout;
}
/**
* Gets the process idle timeout.
*
* @return float|null
*/
public function getIdleTimeout()
{
return $this->idleTimeout;
}
/**
* Sets the process timeout.
*
* To disable the timeout, set this value to null.
*
* @param float|null $timeout The timeout in seconds
* @param integer|float|null $timeout The timeout in seconds
*
* @return self The current Process instance
*
@ -855,19 +870,23 @@ class Process
*/
public function setTimeout($timeout)
{
if (null === $timeout) {
$this->timeout = null;
$this->timeout = $this->validateTimeout($timeout);
return $this;
}
return $this;
}
$timeout = (float) $timeout;
if ($timeout < 0) {
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
}
$this->timeout = $timeout;
/**
* Sets the process idle timeout.
*
* @param integer|float|null $timeout
*
* @return self The current Process instance.
*
* @throws InvalidArgumentException if the timeout is negative
*/
public function setIdleTimeout($timeout)
{
$this->idleTimeout = $this->validateTimeout($timeout);
return $this;
}
@ -1078,7 +1097,13 @@ class Process
if (0 < $this->timeout && $this->timeout < microtime(true) - $this->starttime) {
$this->stop(0);
throw new RuntimeException('The process timed-out.');
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL);
}
if (0 < $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) {
$this->stop(0);
throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE);
}
}
@ -1253,4 +1278,26 @@ class Process
// stream_select returns false when the `select` system call is interrupted by an incoming signal
return isset($lastError['message']) && false !== stripos($lastError['message'], 'interrupted system call');
}
/**
* Validates and returns the filtered timeout.
*
* @param integer|float|null $timeout
*
* @return float|null
*/
private function validateTimeout($timeout)
{
if (null === $timeout) {
return null;
}
$timeout = (float) $timeout;
if ($timeout < 0) {
throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.');
}
return $timeout;
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Process\Tests;
use Symfony\Component\Process\Exception\ProcessTimedOutException;
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\RuntimeException;
@ -429,6 +430,45 @@ abstract class AbstractProcessTest extends \PHPUnit_Framework_TestCase
$this->assertLessThan($timeout + $precision, $duration);
}
/**
* @group idle-timeout
*/
public function testIdleTimeout()
{
$process = $this->getProcess('sleep 3');
$process->setTimeout(10);
$process->setIdleTimeout(1);
try {
$process->run();
$this->fail('A timeout exception was expected.');
} catch (ProcessTimedOutException $ex) {
$this->assertTrue($ex->isIdleTimeout());
$this->assertFalse($ex->isGeneralTimeout());
$this->assertEquals(1.0, $ex->getExceededTimeout());
}
}
/**
* @group idle-timeout
*/
public function testIdleTimeoutNotExceededWhenOutputIsSent()
{
$process = $this->getProcess('echo "foo"; sleep 1; echo "foo"; sleep 1; echo "foo"; sleep 1; echo "foo"; sleep 5;');
$process->setTimeout(5);
$process->setIdleTimeout(3);
try {
$process->run();
$this->fail('A timeout exception was expected.');
} catch (ProcessTimedOutException $ex) {
$this->assertTrue($ex->isGeneralTimeout());
$this->assertFalse($ex->isIdleTimeout());
$this->assertEquals(5.0, $ex->getExceededTimeout());
}
}
public function testGetPid()
{
$process = $this->getProcess('php -r "sleep(1);"');