Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #1369 and #1714 Support large amounts of logs #1852

Merged
merged 4 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading