Skip to content

Commit

Permalink
Merge pull request #1852 from brefphp/fpm-logs
Browse files Browse the repository at this point in the history
Fix #1369 and #1714 Support large amount of logs
  • Loading branch information
mnapoli committed Aug 16, 2024
2 parents 5c173d7 + 52e7015 commit acfe18b
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 18 deletions.
61 changes: 47 additions & 14 deletions src/FpmRuntime/FpmHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use hollodotme\FastCGI\Interfaces\ProvidesRequestData;
use hollodotme\FastCGI\Interfaces\ProvidesResponseData;
use hollodotme\FastCGI\SocketConnections\UnixDomainSocket;
use Symfony\Component\Process\Process;
use RuntimeException;
use Throwable;

/**
Expand Down Expand Up @@ -47,7 +47,8 @@ final class FpmHandler extends HttpHandler
private UnixDomainSocket $connection;
private string $handler;
private string $configFile;
private ?Process $fpm = null;
/** @var resource|null */
private $fpm;

public function __construct(string $handler, string $configFile = self::CONFIG)
{
Expand Down Expand Up @@ -76,13 +77,12 @@ public function start(): void
* --nodaemonize: we want to keep control of the process
* --force-stderr: force logs to be sent to stderr, which will allow us to send them to CloudWatch
*/
$this->fpm = new Process(['php-fpm', '--nodaemonize', '--force-stderr', '--fpm-config', $this->configFile]);
$resource = @proc_open(['php-fpm', '--nodaemonize', '--force-stderr', '--fpm-config', $this->configFile], [], $pipes);

$this->fpm->setTimeout(null);
$this->fpm->start(function ($type, $output): void {
// Send any PHP-FPM log to CloudWatch
echo $output;
});
if (! is_resource($resource)) {
throw new RuntimeException('PHP-FPM failed to start');
}
$this->fpm = $resource;

$this->client = new Client;
$this->connection = new UnixDomainSocket(self::SOCKET, 1000, 900000);
Expand All @@ -95,12 +95,11 @@ public function start(): void
*/
public function stop(): void
{
if ($this->fpm && $this->fpm->isRunning()) {
if ($this->isFpmRunning()) {
// Give it less than a second to stop (500ms should be plenty enough time)
// this is for the case where the script timed out: we reserve 1 second before the end
// of the Lambda timeout, so we must kill everything and restart FPM in 1 second.
// Note: Symfony will first try sending SIGTERM (15) and then SIGKILL (9)
$this->fpm->stop(0.5);
$this->stopFpm(0.5);
if ($this->isReady()) {
throw new Exception('PHP-FPM cannot be stopped');
}
Expand Down Expand Up @@ -192,7 +191,7 @@ public function handleRequest(HttpRequestEvent $event, Context $context): HttpRe
*/
private function ensureStillRunning(): void
{
if (! $this->fpm || ! $this->fpm->isRunning()) {
if (! $this->isFpmRunning()) {
throw new Exception('PHP-FPM has stopped for an unknown reason');
}
}
Expand All @@ -215,8 +214,9 @@ private function waitUntilReady(): void
}

// If the process has crashed we can stop immediately
if (! $this->fpm->isRunning()) {
throw new Exception('PHP-FPM failed to start: ' . PHP_EOL . $this->fpm->getOutput() . PHP_EOL . $this->fpm->getErrorOutput());
if (! $this->isFpmRunning()) {
// The output of FPM is in the stderr of the Lambda process
throw new Exception('PHP-FPM failed to start');
}
}
}
Expand Down Expand Up @@ -337,4 +337,37 @@ private function getResponseHeaders(ProvidesResponseData $response): array
{
return array_change_key_case($response->getHeaders(), CASE_LOWER);
}

public function stopFpm(float $timeout): void
{
if (! $this->fpm) {
return;
}

$timeoutMicro = microtime(true) + $timeout;
if ($this->isFpmRunning()) {
$pid = proc_get_status($this->fpm)['pid'];
// SIGTERM
@posix_kill($pid, 15);
do {
usleep(1000);
// @phpstan-ignore-next-line
} while ($this->isFpmRunning() && microtime(true) < $timeoutMicro);

// @phpstan-ignore-next-line
if ($this->isFpmRunning()) {
// SIGKILL
@posix_kill($pid, 9);
usleep(1000);
}
}

proc_close($this->fpm);
$this->fpm = null;
}

private function isFpmRunning(): bool
{
return $this->fpm && proc_get_status($this->fpm)['running'];
}
}
15 changes: 13 additions & 2 deletions tests/FpmRuntime/FpmHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1254,8 +1254,9 @@ public function test worker logs are still written in case of a timeou
], new Context('abc', time(), 'abc', 'abc'));
$this->fail('No exception was thrown');
} catch (Timeout $e) {
$logs = ob_get_contents();
self::assertStringContainsString('This is a log message', $logs);
$this->markTestSkipped('We can no longer test this since proc_open skips output buffering, we cannot capture the logs');
// $logs = ob_get_contents();
// self::assertStringContainsString('This is a log message', $logs);
}
}

Expand Down Expand Up @@ -1285,6 +1286,16 @@ public function test warmer events do not invoke the application()
self::assertEquals(['Lambda is warm'], $result);
}

/**
* @see https://github.com/brefphp/bref/issues/1369
*/
public function test large logs()
{
$response = $this->get('large-logs.php');

self::assertSame('Hello, world!', $response['body']);
}

private function assertGlobalVariables(array $event, array $expectedGlobalVariables): void
{
$this->startFpm(__DIR__ . '/fixtures/request.php');
Expand Down
7 changes: 7 additions & 0 deletions tests/FpmRuntime/fixtures/large-logs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

$stderr = fopen('php://stderr', 'wb');
$bytes = 512 * 512;
fwrite($stderr, str_repeat('x', $bytes));

echo 'Hello, world!';
6 changes: 4 additions & 2 deletions tests/FpmRuntime/fixtures/php-fpm.conf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ error_log = /dev/null
; Log above warning because PHP-FPM logs useless notices
;log_level = 'warning'

; New PHP 7.3 option that includes the maximum length when writing to stderr
log_limit = 8192

[default]
pm = static
; We only need one child because a lambda can process only one request at a time
Expand All @@ -13,8 +16,7 @@ clear_env = yes
; Forward stderr of PHP processes to stderr of PHP-FPM (so that it can be sent to cloudwatch)
catch_workers_output = yes
; New PHP 7.3 option that disables a verbose log prefix
; Disabled for now until we switch to PHP 7.3
;decorate_workers_output = no
decorate_workers_output = no
; Limit the number of core dump logs to 1 to avoid filling up the /tmp disk
; See https://github.com/brefphp/bref/issues/275
rlimit_core = 1

0 comments on commit acfe18b

Please sign in to comment.