From 2a952fef829aa9ad697c5b658519849969ffccba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 30 Oct 2023 09:25:13 +0100 Subject: [PATCH 01/10] frankenphp-symfony: call gc_collect_cycles() after handling request (#142) Triggering the garbage collector after the request has been handled and when the worker may be idle prevents the garbage collection from being randomly done in the middle of the handling of a request (which delays the delivery of the HTTP response). Another option, that is done by Laravel Octone, is to trigger the GC when a configurable amount of memory has been consumed instead of after every request. I think this patch is good enough for FrankenPHP as usually many workers will be available. --- src/Runner.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Runner.php b/src/Runner.php index 46c8378..73c9e0b 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -16,11 +16,8 @@ */ class Runner implements RunnerInterface { - private HttpKernelInterface $kernel; - - public function __construct(HttpKernelInterface $kernel) + public function __construct(private HttpKernelInterface $kernel) { - $this->kernel = $kernel; } public function run(): int @@ -40,6 +37,8 @@ public function run(): int if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) { $this->kernel->terminate($sfRequest, $sfResponse); } + + gc_collect_cycles(); } while ($ret); return 0; From d7539a9af4b1f12ac5e4deb888a47c41b9693fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 30 Oct 2023 09:26:16 +0100 Subject: [PATCH 02/10] feat: set APP_RUNTIME_MODE for frankenphp-symfony (#143) Follows https://github.com/symfony/symfony/pull/52079. --- src/Runner.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Runner.php b/src/Runner.php index 73c9e0b..aabf279 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -23,6 +23,8 @@ public function __construct(private HttpKernelInterface $kernel) public function run(): int { $server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY); + $server['APP_RUNTIME_MODE'] = 'web=1&worker=1'; + do { $ret = \frankenphp_handle_request(function () use ($server, &$sfRequest, &$sfResponse): void { // Merge the environment variables coming from DotEnv with the ones tight to the current request From 19b7bc3d937121eac4f61d87e39b4f144fbb9d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 30 Oct 2023 13:51:22 +0100 Subject: [PATCH 03/10] frankenphp-symfony: perf improvement (#148) Backport of some micro-optimizations by @francislavoie https://github.com/laravel/octane/pull/764#discussion_r1375165179 --------- Co-authored-by: Francis Lavoie --- src/Runner.php | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Runner.php b/src/Runner.php index aabf279..0de2c89 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -22,22 +22,25 @@ public function __construct(private HttpKernelInterface $kernel) public function run(): int { + $kernel = $this->kernel; $server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY); $server['APP_RUNTIME_MODE'] = 'web=1&worker=1'; - do { - $ret = \frankenphp_handle_request(function () use ($server, &$sfRequest, &$sfResponse): void { - // Merge the environment variables coming from DotEnv with the ones tight to the current request - $_SERVER += $server; + $handler = static function () use ($kernel, $server, &$sfRequest, &$sfResponse): void { + // Merge the environment variables coming from DotEnv with the ones tied to the current request + $_SERVER += $server; + + $sfRequest = Request::createFromGlobals(); + $sfResponse = $kernel->handle($sfRequest); - $sfRequest = Request::createFromGlobals(); - $sfResponse = $this->kernel->handle($sfRequest); + $sfResponse->send(); + }; - $sfResponse->send(); - }); + do { + $ret = \frankenphp_handle_request($handler); - if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) { - $this->kernel->terminate($sfRequest, $sfResponse); + if ($kernel instanceof TerminableInterface && $sfRequest && $sfResponse) { + $kernel->terminate($sfRequest, $sfResponse); } gc_collect_cycles(); From 1339d2d94930ef8ab46d02a3fb9c5583ca1ef365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 30 Oct 2023 15:51:19 +0100 Subject: [PATCH 04/10] frankenphp-symfony: tiny refactoring (#151) Tiny refactoring as suggested in https://github.com/php-runtime/runtime/pull/148#discussion_r1375910285 --- src/Runner.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Runner.php b/src/Runner.php index 0de2c89..d5f08c2 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -22,16 +22,15 @@ public function __construct(private HttpKernelInterface $kernel) public function run(): int { - $kernel = $this->kernel; $server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY); $server['APP_RUNTIME_MODE'] = 'web=1&worker=1'; - $handler = static function () use ($kernel, $server, &$sfRequest, &$sfResponse): void { + $handler = function () use ($server, &$sfRequest, &$sfResponse): void { // Merge the environment variables coming from DotEnv with the ones tied to the current request $_SERVER += $server; $sfRequest = Request::createFromGlobals(); - $sfResponse = $kernel->handle($sfRequest); + $sfResponse = $this->kernel->handle($sfRequest); $sfResponse->send(); }; @@ -39,8 +38,8 @@ public function run(): int do { $ret = \frankenphp_handle_request($handler); - if ($kernel instanceof TerminableInterface && $sfRequest && $sfResponse) { - $kernel->terminate($sfRequest, $sfResponse); + if ($this->kernel instanceof TerminableInterface && $sfRequest && $sfResponse) { + $this->kernel->terminate($sfRequest, $sfResponse); } gc_collect_cycles(); From d66150d0dd548907739238cedd37fc7345dfe5a9 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Wed, 22 Nov 2023 13:14:23 +0100 Subject: [PATCH 05/10] Allow Symfony 7 (#152) --- CHANGELOG.md | 6 ++++++ composer.json | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ecda4d..3b25ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [NOT RELEASED] + +### Added + +- Add support for Symfony 7 + ## 0.1.0 First version diff --git a/composer.json b/composer.json index 959bc01..cace03f 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ ], "require": { "php": ">=8.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/http-kernel": "^5.4 || ^6.0", - "symfony/runtime": "^5.4 || ^6.0" + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", + "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", + "symfony/runtime": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { "phpunit/phpunit": "^9.5" From 5a16ebbf61bc8f2e4210c34a49ccc2fea907064e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 12 Dec 2023 11:34:27 +0100 Subject: [PATCH 06/10] frankenphp: don't stop the worker script even if a connection with a client is aborted (#155) See https://github.com/dunglas/frankenphp/pull/316 and https://github.com/dunglas/frankenphp/issues/310#issuecomment-1821833524. --- src/Runner.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Runner.php b/src/Runner.php index d5f08c2..72917ad 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -22,6 +22,9 @@ public function __construct(private HttpKernelInterface $kernel) public function run(): int { + // Prevent worker script termination when a client connection is interrupted + ignore_user_abort(true); + $server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY); $server['APP_RUNTIME_MODE'] = 'web=1&worker=1'; From b3bc258b7f9d74d773852927c38eff775c504097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 12 Dec 2023 11:36:10 +0100 Subject: [PATCH 07/10] frankenphp: add frankenphp_loop_max option (#156) See https://github.com/dunglas/frankenphp/issues/280. --- CHANGELOG.md | 1 + README.md | 4 ++++ src/Runner.php | 9 ++++++--- src/Runtime.php | 14 +++++++++++++- tests/RunnerTest.php | 2 +- 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b25ec1..fdfa053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Add support for Symfony 7 +- Add `frankenphp_loop_max` option ## 0.1.0 diff --git a/README.md b/README.md index 3f030a7..1dd5341 100644 --- a/README.md +++ b/README.md @@ -36,3 +36,7 @@ return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; ``` + +## Options + +* `frankenphp_loop_max`: the number of requests after which the worker must restart, to prevent weird memory leaks (default to `500`, set to `-1` to never restart) diff --git a/src/Runner.php b/src/Runner.php index 72917ad..de8a9ad 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -16,8 +16,10 @@ */ class Runner implements RunnerInterface { - public function __construct(private HttpKernelInterface $kernel) - { + public function __construct( + private HttpKernelInterface $kernel, + private int $loopMax, + ) { } public function run(): int @@ -38,6 +40,7 @@ public function run(): int $sfResponse->send(); }; + $loops = 0; do { $ret = \frankenphp_handle_request($handler); @@ -46,7 +49,7 @@ public function run(): int } gc_collect_cycles(); - } while ($ret); + } while ($ret && (-1 === $this->loopMax || ++$loops <= $this->loopMax)); return 0; } diff --git a/src/Runtime.php b/src/Runtime.php index e45cb3f..efae98b 100644 --- a/src/Runtime.php +++ b/src/Runtime.php @@ -15,10 +15,22 @@ */ class Runtime extends SymfonyRuntime { + /** + * @param array{ + * frankenphp_loop_max?: int, + * } $options + */ + public function __construct(array $options = []) + { + $options['frankenphp_loop_max'] = (int) ($options['frankenphp_loop_max'] ?? $_SERVER['FRANKENPHP_LOOP_MAX'] ?? $_ENV['FRANKENPHP_LOOP_MAX'] ?? 500); + + parent::__construct($options); + } + public function getRunner(?object $application): RunnerInterface { if ($application instanceof HttpKernelInterface && ($_SERVER['FRANKENPHP_WORKER'] ?? false)) { - return new Runner($application); + return new Runner($application, $this->options['frankenphp_loop_max']); } return parent::getRunner($application); diff --git a/tests/RunnerTest.php b/tests/RunnerTest.php index 8993114..8d3caa7 100644 --- a/tests/RunnerTest.php +++ b/tests/RunnerTest.php @@ -37,7 +37,7 @@ public function testRun(): void $_SERVER['FOO'] = 'bar'; - $runner = new Runner($application); + $runner = new Runner($application, 500); $this->assertSame(0, $runner->run()); } } From 56822c3631d9522a3136a4c33082d006bdfe4bad Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 12 Dec 2023 13:06:11 +0100 Subject: [PATCH 08/10] Prepare release for Franken (#159) This will fix #157 --- CHANGELOG.md | 5 ++--- composer.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfa053..6aeb4c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,10 @@ # Change Log -## [NOT RELEASED] - -### Added +## 0.2.0 - Add support for Symfony 7 - Add `frankenphp_loop_max` option +- Drop support for PHP 8.0 ## 0.1.0 diff --git a/composer.json b/composer.json index cace03f..e389bf8 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ } ], "require": { - "php": ">=8.0", + "php": ">=8.1", "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0", "symfony/http-kernel": "^5.4 || ^6.0 || ^7.0", "symfony/runtime": "^5.4 || ^6.0 || ^7.0" From 38a5dfa1b1e40d8e0b3bbc91d84a03cf4e65fcf4 Mon Sep 17 00:00:00 2001 From: Tamcy Date: Sat, 15 Jun 2024 04:56:26 +0800 Subject: [PATCH 09/10] frankenphp: break loop when loopMax is reached (#174) Fixes the issue that the loop doesn't exist when `$loopMax` is reached, but `$loopMax+1`. Please refer to https://github.com/php-runtime/runtime/pull/173 for details. --- src/Runner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Runner.php b/src/Runner.php index de8a9ad..89bcd49 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -49,7 +49,7 @@ public function run(): int } gc_collect_cycles(); - } while ($ret && (-1 === $this->loopMax || ++$loops <= $this->loopMax)); + } while ($ret && (-1 === $this->loopMax || ++$loops < $this->loopMax)); return 0; } From 34b8c25e4b1043dec2a51dfebbc776260acf1921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 3 Dec 2024 17:13:06 +0100 Subject: [PATCH 10/10] [FrankenPHP] Add Xdebug support (#179) Use the new [`xdebug_connect_to_client()`](https://xdebug.org/docs/all_functions#xdebug_connect_to_client) function if available to allow debugging with Xdebug in worker mode. --- src/Runner.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Runner.php b/src/Runner.php index 89bcd49..afb704d 100644 --- a/src/Runner.php +++ b/src/Runner.php @@ -27,10 +27,17 @@ public function run(): int // Prevent worker script termination when a client connection is interrupted ignore_user_abort(true); + $xdebugConnectToClient = function_exists('xdebug_connect_to_client'); + $server = array_filter($_SERVER, static fn (string $key) => !str_starts_with($key, 'HTTP_'), ARRAY_FILTER_USE_KEY); $server['APP_RUNTIME_MODE'] = 'web=1&worker=1'; - $handler = function () use ($server, &$sfRequest, &$sfResponse): void { + $handler = function () use ($server, &$sfRequest, &$sfResponse, $xdebugConnectToClient): void { + // Connect to the Xdebug client if it's available + if ($xdebugConnectToClient) { + xdebug_connect_to_client(); + } + // Merge the environment variables coming from DotEnv with the ones tied to the current request $_SERVER += $server;