Skip to content

Commit

Permalink
Merge pull request #28 from PHPFastCGI/shutdown
Browse files Browse the repository at this point in the history
Auto Shutdown
  • Loading branch information
AndrewCarterUK authored Dec 1, 2016
2 parents 57fcfed + b453541 commit 4c8185a
Show file tree
Hide file tree
Showing 14 changed files with 88 additions and 64 deletions.
3 changes: 3 additions & 0 deletions src/Command/DaemonRunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function __construct(KernelInterface $kernel, DriverContainerInterface $d
->addOption('request-limit', null, InputOption::VALUE_OPTIONAL, 'The maximum number of requests to handle before shutting down')
->addOption('memory-limit', null, InputOption::VALUE_OPTIONAL, 'The memory limit on the daemon instance before shutting down')
->addOption('time-limit', null, InputOption::VALUE_OPTIONAL, 'The time limit on the daemon in seconds before shutting down')
->addOption('auto-shutdown', null, InputOption::VALUE_NONE, 'Perform a graceful shutdown after receiving a 5XX HTTP status code')
->addOption('driver', null, InputOption::VALUE_OPTIONAL, 'The implementation of the FastCGI protocol to use', 'userland');
}

Expand All @@ -78,12 +79,14 @@ private function getDaemonOptions(InputInterface $input, OutputInterface $output
$requestLimit = $input->getOption('request-limit') ?: DaemonOptions::NO_LIMIT;
$memoryLimit = $input->getOption('memory-limit') ?: DaemonOptions::NO_LIMIT;
$timeLimit = $input->getOption('time-limit') ?: DaemonOptions::NO_LIMIT;
$autoShutdown = $input->getOption('auto-shutdown');

return new DaemonOptions([
DaemonOptions::LOGGER => $logger,
DaemonOptions::REQUEST_LIMIT => $requestLimit,
DaemonOptions::MEMORY_LIMIT => $memoryLimit,
DaemonOptions::TIME_LIMIT => $timeLimit,
DaemonOptions::AUTO_SHUTDOWN => $autoShutdown,
]);
}

Expand Down
4 changes: 3 additions & 1 deletion src/DaemonInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public function run();
/**
* Flag the daemon for shutting down. This will stop it from accepting
* requests.
*
* @param string|null Optional message.
*/
public function flagShutdown();
public function flagShutdown($message = null);
}
2 changes: 2 additions & 0 deletions src/DaemonOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class DaemonOptions
const REQUEST_LIMIT = 'request-limit';
const MEMORY_LIMIT = 'memory-limit';
const TIME_LIMIT = 'time-limit';
const AUTO_SHUTDOWN = 'auto-shutdown';

/**
* @var array
Expand Down Expand Up @@ -44,6 +45,7 @@ public function __construct(array $options = [])
self::REQUEST_LIMIT => self::NO_LIMIT,
self::MEMORY_LIMIT => self::NO_LIMIT,
self::TIME_LIMIT => self::NO_LIMIT,
self::AUTO_SHUTDOWN => false,
];

foreach ($options as $option => $value) {
Expand Down
44 changes: 32 additions & 12 deletions src/DaemonTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@

namespace PHPFastCGI\FastCGIDaemon;

use PHPFastCGI\FastCGIDaemon\Exception\MemoryLimitException;
use PHPFastCGI\FastCGIDaemon\Exception\RequestLimitException;
use PHPFastCGI\FastCGIDaemon\Exception\ShutdownException;
use PHPFastCGI\FastCGIDaemon\Exception\TimeLimitException;

trait DaemonTrait
{
Expand All @@ -14,6 +11,11 @@ trait DaemonTrait
*/
private $isShutdown = false;

/**
* @var string
*/
private $shutdownMessage = '';

/**
* @var int
*/
Expand All @@ -29,12 +31,20 @@ trait DaemonTrait
*/
private $memoryLimit;

/**
* @var bool
*/
private $autoShutdown;

/**
* Flags the daemon for shutting down.
*
* @param string $message Optional shutdown message
*/
public function flagShutdown()
public function flagShutdown($message = null)
{
$this->isShutdown = true;
$this->shutdownMessage = (null === $message ? 'Daemon flagged for shutdown' : $message);
}

/**
Expand All @@ -48,6 +58,7 @@ private function setupDaemon(DaemonOptions $daemonOptions)
$this->requestCount = 0;
$this->requestLimit = $daemonOptions->getOption(DaemonOptions::REQUEST_LIMIT);
$this->memoryLimit = $daemonOptions->getOption(DaemonOptions::MEMORY_LIMIT);
$this->autoShutdown = $daemonOptions->getOption(DaemonOptions::AUTO_SHUTDOWN);

$timeLimit = $daemonOptions->getOption(DaemonOptions::TIME_LIMIT);

Expand All @@ -59,13 +70,22 @@ private function setupDaemon(DaemonOptions $daemonOptions)
}

/**
* Increments the request count.
* Increments the request count and looks for application errors.
*
* @param int $number The number of requests to increment the count by
* @param int[] $statusCodes The status codes of sent responses
*/
private function incrementRequestCount($number)
private function considerStatusCodes($statusCodes)
{
$this->requestCount += $number;
$this->requestCount += count($statusCodes);

if ($this->autoShutdown) {
foreach ($statusCodes as $statusCode) {
if ($statusCode >= 500 && $statusCode < 600) {
$this->flagShutdown('Automatic shutdown following status code: ' . $statusCode);
break;
}
}
}
}

/**
Expand All @@ -83,7 +103,7 @@ private function installSignalHandlers()
});

pcntl_signal(SIGALRM, function () {
throw new TimeLimitException('Daemon time limit reached (received SIGALRM)');
throw new ShutdownException('Daemon time limit reached (received SIGALRM)');
});
}

Expand All @@ -97,22 +117,22 @@ private function installSignalHandlers()
private function checkDaemonLimits()
{
if ($this->isShutdown) {
throw new ShutdownException('Daemon flagged for shutdown');
throw new ShutdownException($this->shutdownMessage);
}

pcntl_signal_dispatch();

if (DaemonOptions::NO_LIMIT !== $this->requestLimit) {
if ($this->requestLimit <= $this->requestCount) {
throw new RequestLimitException('Daemon request limit reached ('.$this->requestCount.' of '.$this->requestLimit.')');
throw new ShutdownException('Daemon request limit reached ('.$this->requestCount.' of '.$this->requestLimit.')');
}
}

if (DaemonOptions::NO_LIMIT !== $this->memoryLimit) {
$memoryUsage = memory_get_usage(true);

if ($this->memoryLimit <= $memoryUsage) {
throw new MemoryLimitException('Daemon memory limit reached ('.$memoryUsage.' of '.$this->memoryLimit.' bytes)');
throw new ShutdownException('Daemon memory limit reached ('.$memoryUsage.' of '.$this->memoryLimit.' bytes)');
}
}
}
Expand Down
22 changes: 14 additions & 8 deletions src/Driver/Userland/ConnectionHandler/ConnectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,17 @@ public function ready()
$this->buffer .= $data;
$this->bufferLength += $dataLength;

$dispatchedRequests = 0;
$statusCodes = [];

while (null !== ($record = $this->readRecord())) {
$dispatchedRequests += $this->processRecord($record);
$statusCode = $this->processRecord($record);

if (null != $statusCode) {
$statusCodes[] = $statusCode;
}
}

return $dispatchedRequests;
return $statusCodes;
} catch (\Exception $exception) {
$this->close();

Expand Down Expand Up @@ -198,17 +202,16 @@ private function processRecord(array $record)
if (null !== $content) {
fwrite($this->requests[$requestId]['stdin'], $content);
} else {
$this->dispatchRequest($requestId);

return 1; // One request was dispatched
// Returns the status code
return $this->dispatchRequest($requestId);
}
} elseif (DaemonInterface::FCGI_ABORT_REQUEST === $record['type']) {
$this->endRequest($requestId);
} else {
throw new ProtocolException('Unexpected packet of type: '.$record['type']);
}

return 0; // Zero requests were dispatched
return null; // No status code to return
}

/**
Expand Down Expand Up @@ -345,7 +348,7 @@ private function writeRecord($requestId, $type, $content = null)
* Write a response to the connection as FCGI_STDOUT records.
*
* @param int $requestId The request id to write to
* @param string $headerData The header data to write (including terminating CRLFCRLF)
* @param string $headerData The header -data to write (including terminating CRLFCRLF)
* @param StreamInterface $stream The stream to write
*/
private function writeResponse($requestId, $headerData, StreamInterface $stream)
Expand Down Expand Up @@ -399,6 +402,9 @@ private function dispatchRequest($requestId)
}

$this->endRequest($requestId);

// This method exists on PSR-7 and Symfony responses
return $response->getStatusCode();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ interface ConnectionHandlerInterface
* Triggered when the connection the handler was assigned to is ready to
* be read.
*
* @return int The number of requests dispatched during the function call
* @return int[] The status codes of requests dispatched during the function call
*/
public function ready();

Expand Down
4 changes: 2 additions & 2 deletions src/Driver/Userland/UserlandDaemon.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ private function processConnectionPool()
}

try {
$dispatchedRequests = $this->connectionHandlers[$id]->ready();
$this->incrementRequestCount($dispatchedRequests);
$statusCodes = $this->connectionHandlers[$id]->ready();
$this->considerStatusCodes($statusCodes);
} catch (UserlandDaemonException $exception) {
$this->daemonOptions->getOption(DaemonOptions::LOGGER)->error($exception->getMessage());
}
Expand Down
11 changes: 0 additions & 11 deletions src/Exception/MemoryLimitException.php

This file was deleted.

11 changes: 0 additions & 11 deletions src/Exception/RequestLimitException.php

This file was deleted.

11 changes: 0 additions & 11 deletions src/Exception/TimeLimitException.php

This file was deleted.

10 changes: 5 additions & 5 deletions test/Command/DaemonRunCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ public function testConfiguration()
$this->assertTrue($definition->getOption($optionName)->isValueOptional());
}

$defaults = ['driver' => 'userland'];
$this->assertFalse($definition->getOption('auto-shutdown')->isValueRequired());
$this->assertFalse($definition->getOption('auto-shutdown')->isValueOptional());

foreach ($defaults as $optionName => $defaultValue) {
$this->assertEquals($defaultValue, $definition->getOption($optionName)->getDefault());
}
$this->assertEquals('userland', $definition->getOption('driver')->getDefault());
}

/**
Expand Down Expand Up @@ -74,12 +73,13 @@ public function testDaemonOptions()
]);
$output = new NullOutput();

// Create expected daemon configuration
// Create expected daemon configuration
$options = new DaemonOptions([
DaemonOptions::LOGGER => new ConsoleLogger($output),
DaemonOptions::REQUEST_LIMIT => $requestLimit,
DaemonOptions::MEMORY_LIMIT => $memoryLimit,
DaemonOptions::TIME_LIMIT => $timeLimit,
DaemonOptions::AUTO_SHUTDOWN => false,
]);

// Create testing context using expectations
Expand Down
1 change: 1 addition & 0 deletions test/DaemonOptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function testDaemonOptions()
$this->assertEquals($requestLimit, $options->getOption(DaemonOptions::REQUEST_LIMIT));
$this->assertEquals($memoryLimit, $options->getOption(DaemonOptions::MEMORY_LIMIT));
$this->assertEquals($timeLimit, $options->getOption(DaemonOptions::TIME_LIMIT));
$this->assertEquals(false, $options->getOption(DaemonOptions::AUTO_SHUTDOWN));
}

/**
Expand Down
25 changes: 24 additions & 1 deletion test/Driver/Userland/UserlandDaemonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,27 @@ public function testShutdown()
$this->assertEquals('Daemon shutdown requested (received SIGINT)', $context['logger']->getMessages()[0]['message']);
}

private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $memoryLimit = DaemonOptions::NO_LIMIT, $timeLimit = DaemonOptions::NO_LIMIT)
/**
* Tests that daemon auto-shutdown
*/
public function testAutoShutdown()
{
$context = $this->createTestingContext(
DaemonOptions::NO_LIMIT, DaemonOptions::NO_LIMIT,
DaemonOptions::NO_LIMIT, true
);

$socket = stream_socket_client($context['address']);
$connectionWrapper = new ConnectionWrapper($socket);

$connectionWrapper->writeRequest(1, ['TEST_AUTO_SHUTDOWN' => ''], '');

$context['daemon']->run();

$this->assertEquals('Automatic shutdown following status code: 500', $context['logger']->getMessages()[0]['message']);
}

private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $memoryLimit = DaemonOptions::NO_LIMIT, $timeLimit = DaemonOptions::NO_LIMIT, $autoShutdown = false)
{
$context = [
'kernel' => new MockKernel([
Expand All @@ -155,6 +175,8 @@ private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $
throw new UserlandDaemonException($params['DAEMON_EXCEPTION']);
} elseif (isset($params['SHUTDOWN'])) {
posix_kill(posix_getpid(), SIGINT);
} elseif (isset($params['TEST_AUTO_SHUTDOWN'])) {
return new Response('php://memory', 500);
}

return new Response();
Expand All @@ -169,6 +191,7 @@ private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $
DaemonOptions::REQUEST_LIMIT => $requestLimit,
DaemonOptions::MEMORY_LIMIT => $memoryLimit,
DaemonOptions::TIME_LIMIT => $timeLimit,
DaemonOptions::AUTO_SHUTDOWN => $autoShutdown,
]);

$context['serverSocket'] = stream_socket_server($context['address']);
Expand Down
2 changes: 1 addition & 1 deletion test/Helper/Mocker/MockDaemon.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function run()
return $this->delegateCall('run', func_get_args());
}

public function flagShutdown()
public function flagShutdown($message = null)
{
return $this->delegateCall('flagShutdown', func_get_args());
}
Expand Down

0 comments on commit 4c8185a

Please sign in to comment.