From ff32079c91eeef1bf2e3ea587f2c88be60a09202 Mon Sep 17 00:00:00 2001 From: Karsten Nilsen Date: Sat, 27 Dec 2025 17:29:18 +0100 Subject: [PATCH] Add a memory threshold setting for worker restarts Introduce `frankenphp_min_memory_mb` option to enforce a minimum memory threshold before handling new requests. This prevents running workers when memory is critically low, enhancing stability. Option is configurable via environment variables or runtime options. Default value is -1 which disables this new functionality. --- src/frankenphp-symfony/README.md | 14 +++++++++++ src/frankenphp-symfony/src/Runner.php | 35 +++++++++++++++++++++++++- src/frankenphp-symfony/src/Runtime.php | 4 ++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/frankenphp-symfony/README.md b/src/frankenphp-symfony/README.md index 1dd5341a..f9373c0b 100644 --- a/src/frankenphp-symfony/README.md +++ b/src/frankenphp-symfony/README.md @@ -40,3 +40,17 @@ return function (array $context) { ## 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) +* `frankenphp_min_memory_mb`: the minimum amount of available memory (in MB) required before handling a new request. If available memory falls below this threshold after the first request, the worker will restart (default to `-1` to disable) + +These options can also be configured via environment variables: + +``` +docker run \ + -e FRANKENPHP_CONFIG="worker ./public/index.php" \ + -e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \ + -e FRANKENPHP_LOOP_MAX=500 \ + -e FRANKENPHP_MIN_MEMORY_MB=64 \ + -v $PWD:/app \ + -p 80:80 -p 443:443 \ + dunglas/frankenphp +``` diff --git a/src/frankenphp-symfony/src/Runner.php b/src/frankenphp-symfony/src/Runner.php index afb704d4..a0fbbdd2 100644 --- a/src/frankenphp-symfony/src/Runner.php +++ b/src/frankenphp-symfony/src/Runner.php @@ -19,6 +19,7 @@ class Runner implements RunnerInterface public function __construct( private HttpKernelInterface $kernel, private int $loopMax, + private int $minMemoryMb = -1, ) { } @@ -56,8 +57,40 @@ public function run(): int } gc_collect_cycles(); - } while ($ret && (-1 === $this->loopMax || ++$loops < $this->loopMax)); + ++$loops; + } while ($ret && (-1 === $this->loopMax || $loops < $this->loopMax) && $this->hasAvailableMemory($loops)); return 0; } + + private function hasAvailableMemory(int $loops): bool + { + if (-1 === $this->minMemoryMb || 1 === $loops) { // Always allow the first cycle + return true; + } + + $memoryLimit = \ini_get('memory_limit'); + if (-1 === (int) $memoryLimit || '' === $memoryLimit) { + return true; + } + $memoryLimit = \trim($memoryLimit); + if (strlen($memoryLimit) < 2) { + return true; // Unexpected value, consider enough mem available + } + $last = \strtolower($memoryLimit[\strlen($memoryLimit) - 1]); + if (!\in_array($last, ['g', 'm', 'k'])) { + return true; // Unexpected value, consider enough mem available + } + + $value = (int) $memoryLimit; // (int) stops at first non-numeric char + $memoryLimitMb = match ($last) { + 'g' => $value * 1024, + 'k' => (int) ($value / 1024), + default => $value, + }; + $usedMemoryMb = \memory_get_usage(true) / 1024 / 1024; + $availableMemoryMb = $memoryLimitMb - $usedMemoryMb; + + return $availableMemoryMb >= $this->minMemoryMb; + } } diff --git a/src/frankenphp-symfony/src/Runtime.php b/src/frankenphp-symfony/src/Runtime.php index efae98bc..b1ca8564 100644 --- a/src/frankenphp-symfony/src/Runtime.php +++ b/src/frankenphp-symfony/src/Runtime.php @@ -18,11 +18,13 @@ class Runtime extends SymfonyRuntime /** * @param array{ * frankenphp_loop_max?: int, + * frankenphp_min_memory_mb?: 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); + $options['frankenphp_min_memory_mb'] = (int) ($options['frankenphp_min_memory_mb'] ?? $_SERVER['FRANKENPHP_MIN_MEMORY_MB'] ?? $_ENV['FRANKENPHP_MIN_MEMORY_MB'] ?? -1); parent::__construct($options); } @@ -30,7 +32,7 @@ public function __construct(array $options = []) public function getRunner(?object $application): RunnerInterface { if ($application instanceof HttpKernelInterface && ($_SERVER['FRANKENPHP_WORKER'] ?? false)) { - return new Runner($application, $this->options['frankenphp_loop_max']); + return new Runner($application, $this->options['frankenphp_loop_max'], $this->options['frankenphp_min_memory_mb']); } return parent::getRunner($application);