Skip to content

Commit 99f3aec

Browse files
Added autoshutdown functionality
1 parent 083d05d commit 99f3aec

File tree

9 files changed

+84
-29
lines changed

9 files changed

+84
-29
lines changed

src/Command/DaemonRunCommand.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ public function __construct(KernelInterface $kernel, DriverContainerInterface $d
5959
->addOption('request-limit', null, InputOption::VALUE_OPTIONAL, 'The maximum number of requests to handle before shutting down')
6060
->addOption('memory-limit', null, InputOption::VALUE_OPTIONAL, 'The memory limit on the daemon instance before shutting down')
6161
->addOption('time-limit', null, InputOption::VALUE_OPTIONAL, 'The time limit on the daemon in seconds before shutting down')
62+
->addOption('auto-shutdown', null, InputOption::VALUE_NONE, 'Perform a graceful shutdown after receiving a 5XX HTTP status code')
6263
->addOption('driver', null, InputOption::VALUE_OPTIONAL, 'The implementation of the FastCGI protocol to use', 'userland');
6364
}
6465

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

8284
return new DaemonOptions([
8385
DaemonOptions::LOGGER => $logger,
8486
DaemonOptions::REQUEST_LIMIT => $requestLimit,
8587
DaemonOptions::MEMORY_LIMIT => $memoryLimit,
8688
DaemonOptions::TIME_LIMIT => $timeLimit,
89+
DaemonOptions::AUTO_SHUTDOWN => $autoShutdown,
8790
]);
8891
}
8992

src/DaemonOptions.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class DaemonOptions
1717
const REQUEST_LIMIT = 'request-limit';
1818
const MEMORY_LIMIT = 'memory-limit';
1919
const TIME_LIMIT = 'time-limit';
20+
const AUTO_SHUTDOWN = 'auto-shutdown';
2021

2122
/**
2223
* @var array
@@ -44,6 +45,7 @@ public function __construct(array $options = [])
4445
self::REQUEST_LIMIT => self::NO_LIMIT,
4546
self::MEMORY_LIMIT => self::NO_LIMIT,
4647
self::TIME_LIMIT => self::NO_LIMIT,
48+
self::AUTO_SHUTDOWN => false,
4749
];
4850

4951
foreach ($options as $option => $value) {

src/DaemonTrait.php

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22

33
namespace PHPFastCGI\FastCGIDaemon;
44

5-
use PHPFastCGI\FastCGIDaemon\Exception\MemoryLimitException;
6-
use PHPFastCGI\FastCGIDaemon\Exception\RequestLimitException;
75
use PHPFastCGI\FastCGIDaemon\Exception\ShutdownException;
8-
use PHPFastCGI\FastCGIDaemon\Exception\TimeLimitException;
96

107
trait DaemonTrait
118
{
@@ -14,6 +11,11 @@ trait DaemonTrait
1411
*/
1512
private $isShutdown = false;
1613

14+
/**
15+
* @var string
16+
*/
17+
private $shutdownMessage = '';
18+
1719
/**
1820
* @var int
1921
*/
@@ -29,12 +31,20 @@ trait DaemonTrait
2931
*/
3032
private $memoryLimit;
3133

34+
/**
35+
* @var bool
36+
*/
37+
private $autoShutdown;
38+
3239
/**
3340
* Flags the daemon for shutting down.
41+
*
42+
* @param string $message Optional shutdown message
3443
*/
35-
public function flagShutdown()
44+
public function flagShutdown($message = null)
3645
{
3746
$this->isShutdown = true;
47+
$this->shutdownMessage = (null === $message ? 'Daemon flagged for shutdown' : $message);
3848
}
3949

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

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

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

6172
/**
62-
* Increments the request count.
73+
* Increments the request count and looks for application errors.
6374
*
64-
* @param int $number The number of requests to increment the count by
75+
* @param int[] $statusCodes The status codes of sent responses
6576
*/
66-
private function incrementRequestCount($number)
77+
private function considerStatusCodes($statusCodes)
6778
{
68-
$this->requestCount += $number;
79+
$this->requestCount += count($statusCodes);
80+
81+
if ($this->autoShutdown) {
82+
foreach ($statusCodes as $statusCode) {
83+
if ($statusCode >= 500 && $statusCode < 600) {
84+
$this->flagShutdown('Automatic shutdown following status code: ' . $statusCode);
85+
break;
86+
}
87+
}
88+
}
6989
}
7090

7191
/**
@@ -83,7 +103,7 @@ private function installSignalHandlers()
83103
});
84104

85105
pcntl_signal(SIGALRM, function () {
86-
throw new TimeLimitException('Daemon time limit reached (received SIGALRM)');
106+
throw new ShutdownException('Daemon time limit reached (received SIGALRM)');
87107
});
88108
}
89109

@@ -97,22 +117,22 @@ private function installSignalHandlers()
97117
private function checkDaemonLimits()
98118
{
99119
if ($this->isShutdown) {
100-
throw new ShutdownException('Daemon flagged for shutdown');
120+
throw new ShutdownException($this->shutdownMessage);
101121
}
102122

103123
pcntl_signal_dispatch();
104124

105125
if (DaemonOptions::NO_LIMIT !== $this->requestLimit) {
106126
if ($this->requestLimit <= $this->requestCount) {
107-
throw new RequestLimitException('Daemon request limit reached ('.$this->requestCount.' of '.$this->requestLimit.')');
127+
throw new ShutdownException('Daemon request limit reached ('.$this->requestCount.' of '.$this->requestLimit.')');
108128
}
109129
}
110130

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

114134
if ($this->memoryLimit <= $memoryUsage) {
115-
throw new MemoryLimitException('Daemon memory limit reached ('.$memoryUsage.' of '.$this->memoryLimit.' bytes)');
135+
throw new ShutdownException('Daemon memory limit reached ('.$memoryUsage.' of '.$this->memoryLimit.' bytes)');
116136
}
117137
}
118138
}

src/Driver/Userland/ConnectionHandler/ConnectionHandler.php

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,17 @@ public function ready()
8585
$this->buffer .= $data;
8686
$this->bufferLength += $dataLength;
8787

88-
$dispatchedRequests = 0;
88+
$statusCodes = [];
8989

9090
while (null !== ($record = $this->readRecord())) {
91-
$dispatchedRequests += $this->processRecord($record);
91+
$statusCode = $this->processRecord($record);
92+
93+
if (null != $statusCode) {
94+
$statusCodes[] = $statusCode;
95+
}
9296
}
9397

94-
return $dispatchedRequests;
98+
return $statusCodes;
9599
} catch (\Exception $exception) {
96100
$this->close();
97101

@@ -198,17 +202,16 @@ private function processRecord(array $record)
198202
if (null !== $content) {
199203
fwrite($this->requests[$requestId]['stdin'], $content);
200204
} else {
201-
$this->dispatchRequest($requestId);
202-
203-
return 1; // One request was dispatched
205+
// Returns the status code
206+
return $this->dispatchRequest($requestId);
204207
}
205208
} elseif (DaemonInterface::FCGI_ABORT_REQUEST === $record['type']) {
206209
$this->endRequest($requestId);
207210
} else {
208211
throw new ProtocolException('Unexpected packet of type: '.$record['type']);
209212
}
210213

211-
return 0; // Zero requests were dispatched
214+
return null; // No status code to return
212215
}
213216

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

401404
$this->endRequest($requestId);
405+
406+
// This method exists on PSR-7 and Symfony responses
407+
return $response->getStatusCode();
402408
}
403409

404410
/**

src/Driver/Userland/ConnectionHandler/ConnectionHandlerInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ interface ConnectionHandlerInterface
1414
* Triggered when the connection the handler was assigned to is ready to
1515
* be read.
1616
*
17-
* @return int The number of requests dispatched during the function call
17+
* @return int[] The status codes of requests dispatched during the function call
1818
*/
1919
public function ready();
2020

src/Driver/Userland/UserlandDaemon.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ private function processConnectionPool()
103103
}
104104

105105
try {
106-
$dispatchedRequests = $this->connectionHandlers[$id]->ready();
107-
$this->incrementRequestCount($dispatchedRequests);
106+
$statusCodes = $this->connectionHandlers[$id]->ready();
107+
$this->considerStatusCodes($statusCodes);
108108
} catch (UserlandDaemonException $exception) {
109109
$this->daemonOptions->getOption(DaemonOptions::LOGGER)->error($exception->getMessage());
110110
}

test/Command/DaemonRunCommandTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,10 @@ public function testConfiguration()
3232
$this->assertTrue($definition->getOption($optionName)->isValueOptional());
3333
}
3434

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

37-
foreach ($defaults as $optionName => $defaultValue) {
38-
$this->assertEquals($defaultValue, $definition->getOption($optionName)->getDefault());
39-
}
38+
$this->assertEquals('userland', $definition->getOption('driver')->getDefault());
4039
}
4140

4241
/**
@@ -74,12 +73,13 @@ public function testDaemonOptions()
7473
]);
7574
$output = new NullOutput();
7675

77-
// Create expected daemon configuration
76+
// Create expected daemon configuration
7877
$options = new DaemonOptions([
7978
DaemonOptions::LOGGER => new ConsoleLogger($output),
8079
DaemonOptions::REQUEST_LIMIT => $requestLimit,
8180
DaemonOptions::MEMORY_LIMIT => $memoryLimit,
8281
DaemonOptions::TIME_LIMIT => $timeLimit,
82+
DaemonOptions::AUTO_SHUTDOWN => false,
8383
]);
8484

8585
// Create testing context using expectations

test/DaemonOptionsTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function testDaemonOptions()
2929
$this->assertEquals($requestLimit, $options->getOption(DaemonOptions::REQUEST_LIMIT));
3030
$this->assertEquals($memoryLimit, $options->getOption(DaemonOptions::MEMORY_LIMIT));
3131
$this->assertEquals($timeLimit, $options->getOption(DaemonOptions::TIME_LIMIT));
32+
$this->assertEquals(false, $options->getOption(DaemonOptions::AUTO_SHUTDOWN));
3233
}
3334

3435
/**

test/Driver/Userland/UserlandDaemonTest.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,27 @@ public function testShutdown()
142142
$this->assertEquals('Daemon shutdown requested (received SIGINT)', $context['logger']->getMessages()[0]['message']);
143143
}
144144

145-
private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $memoryLimit = DaemonOptions::NO_LIMIT, $timeLimit = DaemonOptions::NO_LIMIT)
145+
/**
146+
* Tests that daemon auto-shutdown
147+
*/
148+
public function testAutoShutdown()
149+
{
150+
$context = $this->createTestingContext(
151+
DaemonOptions::NO_LIMIT, DaemonOptions::NO_LIMIT,
152+
DaemonOptions::NO_LIMIT, true
153+
);
154+
155+
$socket = stream_socket_client($context['address']);
156+
$connectionWrapper = new ConnectionWrapper($socket);
157+
158+
$connectionWrapper->writeRequest(1, ['TEST_AUTO_SHUTDOWN' => ''], '');
159+
160+
$context['daemon']->run();
161+
162+
$this->assertEquals('Automatic shutdown following status code: 500', $context['logger']->getMessages()[0]['message']);
163+
}
164+
165+
private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $memoryLimit = DaemonOptions::NO_LIMIT, $timeLimit = DaemonOptions::NO_LIMIT, $autoShutdown = false)
146166
{
147167
$context = [
148168
'kernel' => new MockKernel([
@@ -155,6 +175,8 @@ private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $
155175
throw new UserlandDaemonException($params['DAEMON_EXCEPTION']);
156176
} elseif (isset($params['SHUTDOWN'])) {
157177
posix_kill(posix_getpid(), SIGINT);
178+
} elseif (isset($params['TEST_AUTO_SHUTDOWN'])) {
179+
return new Response('php://memory', 500);
158180
}
159181

160182
return new Response();
@@ -169,6 +191,7 @@ private function createTestingContext($requestLimit = DaemonOptions::NO_LIMIT, $
169191
DaemonOptions::REQUEST_LIMIT => $requestLimit,
170192
DaemonOptions::MEMORY_LIMIT => $memoryLimit,
171193
DaemonOptions::TIME_LIMIT => $timeLimit,
194+
DaemonOptions::AUTO_SHUTDOWN => $autoShutdown,
172195
]);
173196

174197
$context['serverSocket'] = stream_socket_server($context['address']);

0 commit comments

Comments
 (0)