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}

Paths

Below are the source code lines that represent each code path as identified by Xdebug. Please note a path is not necessarily coterminous with a line, a line may contain multiple paths and therefore show up more than once. Please also be aware that some paths may include implicit rather than explicit branches, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

FrankenPhpWorker->__construct
29        private readonly Closure $handleRequest,
30        public readonly int $maxRequests,
31        public readonly int $maxMemory,
32    ) {}
FrankenPhpWorker->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    }
FrankenPhpWorker->defaultMaxMemory
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
 
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
 
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
FrankenPhpWorker->memorySetting
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
 
105            return null;
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
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',
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
114        return ini_parse_quantity($value);
115    }
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
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',
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
 
114        return ini_parse_quantity($value);
115    }
FrankenPhpWorker->requestsSetting
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
 
90            return null;
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
94            throw new RuntimeException(self::MAX_REQUESTS . ' must be a number of requests, 0 for no limit');
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
97        return (int) $value;
98    }
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
94            throw new RuntimeException(self::MAX_REQUESTS . ' must be a number of requests, 0 for no limit');
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
93        if (preg_match('/^\d+$/', $value) !== 1) {
 
97        return (int) $value;
98    }
FrankenPhpWorker->retires
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
78                || $this->maxRequests > 0
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
80                || $this->maxMemory > 0
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
78                || $this->maxRequests > 0
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
80                || $this->maxMemory > 0
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
78                || $this->maxRequests > 0
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
78                || $this->maxRequests > 0
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
80                || $this->maxMemory > 0
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
78                || $this->maxRequests > 0
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
80                || $this->maxMemory > 0
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
78                || $this->maxRequests > 0
 
79                && $handled >= $this->maxRequests
 
79                && $handled >= $this->maxRequests
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
79                && $handled >= $this->maxRequests
 
80                || $this->maxMemory > 0
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
79                && $handled >= $this->maxRequests
 
80                || $this->maxMemory > 0
 
81                && memory_get_usage(true) >= $this->maxMemory
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
 
79                && $handled >= $this->maxRequests
 
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
FrankenPhpWorker->serve
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 {
 
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));
 
69        } while ($running && !$this->retires($app, $handled));
 
69        } while ($running && !$this->retires($app, $handled));
 
71        return false;
72    }
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 {
 
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));
 
69        } while ($running && !$this->retires($app, $handled));
 
69        } while ($running && !$this->retires($app, $handled));
 
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));
 
69        } while ($running && !$this->retires($app, $handled));
 
71        return false;
72    }
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 {
 
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));
 
69        } while ($running && !$this->retires($app, $handled));
 
71        return false;
72    }
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 {
 
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));
 
69        } while ($running && !$this->retires($app, $handled));
 
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));
 
69        } while ($running && !$this->retires($app, $handled));
 
69        } while ($running && !$this->retires($app, $handled));
 
71        return false;
72    }
FrankenPhpWorker->setting
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
124    }
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
124    }
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
124    }
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
124    }
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
124    }
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
 
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
 
123        return $value === '' ? null : $value;
124    }
{closure:/workspace/celema/core/src/Runtime/FrankenPhpWorker.php:61-66}
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            });