Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
43 / 43
91.84% covered (success)
91.84%
45 / 49
57.14% covered (warning)
57.14%
20 / 35
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FrankenPhpWorker
100.00% covered (success)
100.00%
43 / 43
91.84% covered (success)
91.84%
45 / 49
57.14% covered (warning)
57.14%
20 / 35
100.00% covered (success)
100.00%
8 / 8
55.71
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serve
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
2.06
 retires
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
9 / 9
55.56% covered (warning)
55.56%
5 / 9
100.00% covered (success)
100.00%
1 / 1
7.19
 requestsSetting
100.00% covered (success)
100.00%
6 / 6
87.50% covered (warning)
87.50%
7 / 8
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
3.58
 memorySetting
100.00% covered (success)
100.00%
8 / 8
87.50% covered (warning)
87.50%
7 / 8
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
3.58
 setting
100.00% covered (success)
100.00%
3 / 3
90.00% covered (success)
90.00%
9 / 10
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
5.67
 defaultMaxMemory
100.00% covered (success)
100.00%
3 / 3
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Core\Runtime;
6
7use Celema\Core\App;
8use Celema\Core\Exception\RuntimeException;
9use Closure;
10
11/**
12 * Handles requests in a FrankenPHP worker until FrankenPHP stops it or it
13 * retires: after a request that left the app unusable, after a configured
14 * number of requests, or above a memory threshold. FrankenPHP then starts
15 * a fresh worker.
16 *
17 * @internal
18 */
19final class FrankenPhpWorker
20{
21    public const string MAX_REQUESTS = 'CELEMA_WORKER_MAX_REQUESTS';
22    public const string MAX_MEMORY = 'CELEMA_WORKER_MAX_MEMORY';
23
24    /**
25     * @param Closure(callable(): void): bool $handleRequest Waits for a request and runs the
26     *     callback for it; returns false when the worker should stop (frankenphp_handle_request).
27     */
28    public function __construct(
29        private readonly Closure $handleRequest,
30        public readonly int $maxRequests,
31        public readonly int $maxMemory,
32    ) {}
33
34    /**
35     * Explicit limits win over the worker's environment (for example the
36     * `env` directive of a Caddyfile worker block). Without either, the
37     * request limit is off and the memory limit is 80 % of memory_limit.
38     *
39     * @param Closure(callable(): void): bool $handleRequest
40     */
41    public static function configure(
42        Closure $handleRequest,
43        ?int $maxRequests = null,
44        ?int $maxMemory = null,
45    ): self {
46        return new self(
47            $handleRequest,
48            $maxRequests ?? self::requestsSetting() ?? 0,
49            $maxMemory ?? self::memorySetting() ?? self::defaultMaxMemory(),
50        );
51    }
52
53    public function serve(App $app): false
54    {
55        // A client that disconnects must not abort a request halfway, as
56        // its teardown has to run before the worker takes the next one.
57        ignore_user_abort(true);
58        $handled = 0;
59
60        do {
61            $running = ($this->handleRequest)(static function () use ($app): void {
62                // PHP's stat cache would keep file sizes and modification
63                // times from earlier requests.
64                clearstatcache();
65                $app->run();
66            });
67            $handled++;
68            gc_collect_cycles();
69        } while ($running && !$this->retires($app, $handled));
70
71        return false;
72    }
73
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
78                || $this->maxRequests > 0
79                && $handled >= $this->maxRequests
80                || $this->maxMemory > 0
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
84
85    private static function requestsSetting(): ?int
86    {
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
90            return null;
91        }
92
93        if (preg_match('/^\d+$/', $value) !== 1) {
94            throw new RuntimeException(self::MAX_REQUESTS . ' must be a number of requests, 0 for no limit');
95        }
96
97        return (int) $value;
98    }
99
100    private static function memorySetting(): ?int
101    {
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
105            return null;
106        }
107
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
109            throw new RuntimeException(
110                self::MAX_MEMORY . ' must be a number of bytes, optionally with K, M or G, 0 for no limit',
111            );
112        }
113
114        return ini_parse_quantity($value);
115    }
116
117    /** @return ?non-empty-string */
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
124    }
125
126    private static function defaultMaxMemory(): int
127    {
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
133}