adds ability to define an idle timeout

This commit is contained in:
Johannes M. Schmitt 2013-08-02 21:20:26 +02:00
parent b788094e63
commit b922ba22e5
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);"');