Skip to content

Commit 819a02a

Browse files
michalsnpaulbalandanneznaika0
authored
feat: FrankenPHP Worker Mode (#9889)
Co-authored-by: John Paul E. Balandan, CPA <[email protected]> Co-authored-by: neznaika0 <[email protected]>
1 parent fc0cb19 commit 819a02a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1912
-132
lines changed

app/Config/Optimize.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*
88
* NOTE: This class does not extend BaseConfig for performance reasons.
99
* So you cannot replace the property values with Environment Variables.
10+
*
11+
* WARNING: Do not use these options when running the app in the Worker Mode.
1012
*/
1113
class Optimize
1214
{

app/Config/WorkerMode.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Config;
4+
5+
/**
6+
* This configuration controls how CodeIgniter behaves when running
7+
* in worker mode (with FrankenPHP).
8+
*/
9+
class WorkerMode
10+
{
11+
/**
12+
* Persistent Services
13+
*
14+
* List of service names that should persist across requests.
15+
* These services will NOT be reset between requests.
16+
*
17+
* Services not in this list will be reset for each request to prevent
18+
* state leakage.
19+
*
20+
* Recommended persistent services:
21+
* - `autoloader`: PSR-4 autoloading configuration
22+
* - `locator`: File locator
23+
* - `exceptions`: Exception handler
24+
* - `commands`: CLI commands registry
25+
* - `codeigniter`: Main application instance
26+
* - `superglobals`: Superglobals wrapper
27+
* - `routes`: Router configuration
28+
* - `cache`: Cache instance
29+
*
30+
* @var list<string>
31+
*/
32+
public array $persistentServices = [
33+
'autoloader',
34+
'locator',
35+
'exceptions',
36+
'commands',
37+
'codeigniter',
38+
'superglobals',
39+
'routes',
40+
'cache',
41+
];
42+
43+
/**
44+
* Force Garbage Collection
45+
*
46+
* Whether to force garbage collection after each request.
47+
* Helps prevent memory leaks at a small performance cost.
48+
*/
49+
public bool $forceGarbageCollection = true;
50+
}

system/Boot.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,39 @@ public static function bootWeb(Paths $paths): int
7676
return EXIT_SUCCESS;
7777
}
7878

79+
/**
80+
* Bootstrap for FrankenPHP worker mode.
81+
*
82+
* This method performs one-time initialization for worker mode,
83+
* loading everything except the CodeIgniter instance, which should
84+
* be created fresh for each request.
85+
*
86+
* @used-by `public/frankenphp-worker.php`
87+
*/
88+
public static function bootWorker(Paths $paths): CodeIgniter
89+
{
90+
static::definePathConstants($paths);
91+
if (! defined('APP_NAMESPACE')) {
92+
static::loadConstants();
93+
}
94+
static::checkMissingExtensions();
95+
96+
static::loadDotEnv($paths);
97+
static::defineEnvironment();
98+
static::loadEnvironmentBootstrap($paths);
99+
100+
static::loadCommonFunctions();
101+
static::loadAutoloader();
102+
static::setExceptionHandler();
103+
static::initializeKint();
104+
105+
static::checkOptimizationsForWorker();
106+
107+
static::autoloadHelpers();
108+
109+
return Boot::initializeCodeIgniter();
110+
}
111+
79112
/**
80113
* Used by command line scripts other than
81114
* * `spark`
@@ -333,6 +366,20 @@ protected static function checkMissingExtensions(): void
333366
exit(EXIT_ERROR);
334367
}
335368

369+
protected static function checkOptimizationsForWorker(): void
370+
{
371+
if (class_exists(Optimize::class)) {
372+
$optimize = new Optimize();
373+
374+
if ($optimize->configCacheEnabled || $optimize->locatorCacheEnabled) {
375+
echo 'Optimization settings (configCacheEnabled, locatorCacheEnabled) '
376+
. 'must be disabled in Config\Optimize when running in Worker Mode.';
377+
378+
exit(EXIT_ERROR);
379+
}
380+
}
381+
}
382+
336383
protected static function initializeKint(): void
337384
{
338385
service('autoloader')->initializeKint(CI_DEBUG);

system/Cache/Handlers/BaseHandler.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,26 @@ public function remember(string $key, int $ttl, Closure $callback): mixed
8787

8888
return $value;
8989
}
90+
91+
/**
92+
* Check if connection is alive.
93+
*
94+
* Default implementation for handlers that don't require connection management.
95+
* Handlers with persistent connections (Redis, Predis, Memcached) should override this.
96+
*/
97+
public function ping(): bool
98+
{
99+
return true;
100+
}
101+
102+
/**
103+
* Reconnect to the cache server.
104+
*
105+
* Default implementation for handlers that don't require connection management.
106+
* Handlers with persistent connections (Redis, Predis, Memcached) should override this.
107+
*/
108+
public function reconnect(): bool
109+
{
110+
return true;
111+
}
90112
}

system/Cache/Handlers/MemcachedHandler.php

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,6 @@ public function __construct(Cache $config)
5757
$this->config = array_merge($this->config, $config->memcached);
5858
}
5959

60-
/**
61-
* Closes the connection to Memcache(d) if present.
62-
*/
63-
public function __destruct()
64-
{
65-
if ($this->memcached instanceof Memcached) {
66-
$this->memcached->quit();
67-
} elseif ($this->memcached instanceof Memcache) {
68-
$this->memcached->close();
69-
}
70-
}
71-
7260
public function initialize(): void
7361
{
7462
try {
@@ -230,4 +218,46 @@ public function isSupported(): bool
230218
{
231219
return extension_loaded('memcached') || extension_loaded('memcache');
232220
}
221+
222+
public function ping(): bool
223+
{
224+
$version = $this->memcached->getVersion();
225+
226+
if ($this->memcached instanceof Memcached) {
227+
// Memcached extension returns array with server:port => version
228+
if (! is_array($version)) {
229+
return false;
230+
}
231+
232+
$serverKey = $this->config['host'] . ':' . $this->config['port'];
233+
234+
return isset($version[$serverKey]) && $version[$serverKey] !== false;
235+
}
236+
237+
if ($this->memcached instanceof Memcache) {
238+
// Memcache extension returns string version
239+
return is_string($version) && $version !== '';
240+
}
241+
242+
return false;
243+
}
244+
245+
public function reconnect(): bool
246+
{
247+
if ($this->memcached instanceof Memcached) {
248+
$this->memcached->quit();
249+
} elseif ($this->memcached instanceof Memcache) {
250+
$this->memcached->close();
251+
}
252+
253+
try {
254+
$this->initialize();
255+
256+
return true;
257+
} catch (CriticalError $e) {
258+
log_message('error', 'Cache: Memcached reconnection failed: ' . $e->getMessage());
259+
260+
return false;
261+
}
262+
}
233263
}

system/Cache/Handlers/PredisHandler.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,38 @@ public function isSupported(): bool
204204
{
205205
return class_exists(Client::class);
206206
}
207+
208+
public function ping(): bool
209+
{
210+
try {
211+
$result = $this->redis->ping();
212+
213+
if (is_object($result)) {
214+
return $result->getPayload() === 'PONG';
215+
}
216+
217+
return $result === 'PONG';
218+
} catch (Exception) {
219+
return false;
220+
}
221+
}
222+
223+
public function reconnect(): bool
224+
{
225+
try {
226+
$this->redis->disconnect();
227+
} catch (Exception) {
228+
// Connection already dead, that's fine
229+
}
230+
231+
try {
232+
$this->initialize();
233+
234+
return true;
235+
} catch (CriticalError $e) {
236+
log_message('error', 'Cache: Predis reconnection failed: ' . $e->getMessage());
237+
238+
return false;
239+
}
240+
}
207241
}

system/Cache/Handlers/RedisHandler.php

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,16 +64,6 @@ public function __construct(Cache $config)
6464
$this->config = array_merge($this->config, $config->redis);
6565
}
6666

67-
/**
68-
* Closes the connection to Redis if present.
69-
*/
70-
public function __destruct()
71-
{
72-
if (isset($this->redis)) {
73-
$this->redis->close();
74-
}
75-
}
76-
7767
public function initialize(): void
7868
{
7969
$config = $this->config;
@@ -229,4 +219,40 @@ public function isSupported(): bool
229219
{
230220
return extension_loaded('redis');
231221
}
222+
223+
public function ping(): bool
224+
{
225+
if (! isset($this->redis)) {
226+
return false;
227+
}
228+
229+
try {
230+
$result = $this->redis->ping();
231+
232+
return in_array($result, [true, '+PONG'], true);
233+
} catch (RedisException) {
234+
return false;
235+
}
236+
}
237+
238+
public function reconnect(): bool
239+
{
240+
if (isset($this->redis)) {
241+
try {
242+
$this->redis->close();
243+
} catch (RedisException) {
244+
// Connection already dead, that's fine
245+
}
246+
}
247+
248+
try {
249+
$this->initialize();
250+
251+
return true;
252+
} catch (CriticalError $e) {
253+
log_message('error', 'Cache: Redis reconnection failed: ' . $e->getMessage());
254+
255+
return false;
256+
}
257+
}
232258
}

system/CodeIgniter.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ class CodeIgniter
9595
/**
9696
* Current response.
9797
*
98-
* @var ResponseInterface
98+
* @var ResponseInterface|null
9999
*/
100100
protected $response;
101101

102102
/**
103103
* Router to use.
104104
*
105-
* @var Router
105+
* @var Router|null
106106
*/
107107
protected $router;
108108

@@ -116,14 +116,14 @@ class CodeIgniter
116116
/**
117117
* Controller method to invoke.
118118
*
119-
* @var string
119+
* @var string|null
120120
*/
121121
protected $method;
122122

123123
/**
124124
* Output handler to use.
125125
*
126-
* @var string
126+
* @var string|null
127127
*/
128128
protected $output;
129129

@@ -192,6 +192,24 @@ public function initialize()
192192
date_default_timezone_set($this->config->appTimezone ?? 'UTC');
193193
}
194194

195+
/**
196+
* Reset request-specific state for worker mode.
197+
* Clears all request/response data to prepare for the next request.
198+
*/
199+
public function resetForWorkerMode(): void
200+
{
201+
$this->request = null;
202+
$this->response = null;
203+
$this->router = null;
204+
$this->controller = null;
205+
$this->method = null;
206+
$this->output = null;
207+
208+
// Reset timing
209+
$this->startTime = null;
210+
$this->totalTime = 0;
211+
}
212+
195213
/**
196214
* Initializes Kint
197215
*

0 commit comments

Comments
 (0)