Skip to content

add(core): FrankenPHP worker mode - #2535

Merged
Seiger merged 4 commits into
evolution-cms:3.6.xfrom
elcreator:add-frankenphp-worker-mode
Oct 31, 2026
Merged

Seiger merged 4 commits into
evolution-cms:3.6.xfrom
elcreator:add-frankenphp-worker-mode

Conversation

@elcreator

@elcreator elcreator commented Oct 31, 2026 •

Copy link
Copy Markdown

Prototype: EVO_WORKER_KEEP_APP=1

This PR introduces an experimental mode that preserves the initialized Evolution CMS application between requests in FrankenPHP Worker Mode, reducing repeated bootstrap overhead.

How it works

Service providers are bootstrapped once per worker, after which a snapshot of the application state is captured.

After each request:

  • The properties of the DI container, Core, and all services instantiated during bootstrap are restored to their post-boot state.
  • Objects directly referenced by those services are also restored.
  • Services implementing flushState() or ResetInterface are reset through their respective hooks.
  • Settings, Parser, authentication and the session are initialized separately for each request, in Core::initialize(), after the snapshot.
  • If a request fails with an exception, the worker is restarted instead of reusing the potentially inconsistent application state.

Providers always boot before the session starts. With the default EVO_SESSION=true, the session proxy loads the session in Core::initialize(). With EVO_SESSION=false, a worker now starts the native session in
Core::initialize() too, instead of in core/bootstrap.php or at the top of TracyServiceProvider.php. During boot(), providers therefore always see an empty session and a guest, in both worker lifetimes. Classic PHP is
unchanged.

Performance benchmarks

Page Classic Worker Persistent Application
Homepage (cached) 7.69 ms 6.87 ms 3.33 ms
Non-cached page with snippet 9.14 ms 6.99 ms 3.90 ms
Cached page with snippet 6.84 ms 5.69 ms 2.24 ms

The persistent application mode reduces request latency by approximately 44–61% compared to standard Worker Mode in these benchmarks.

Stability and compatibility

  • Memory usage remains stable across 400 requests.
  • Changes to plugins and settings made through CLI are reflected immediately.
  • Plugin events are not registered or triggered multiple times.
  • No session or user state from the request that booted the worker is visible to later requests, with either EVO_SESSION setting.

Known limitation

Service providers that read the query string, cookies or request headers directly during boot() see only those of the first request handled by the worker. They should read them per request instead: in middleware, in a plugin
event, or in a binding resolved during the request.

For this reason, EVO_WORKER_KEEP_APP=1 remains experimental. The limitation and usage details are documented in WORKER.md.

@elcreator
elcreator requested review from Dmi3yy and Seiger October 31, 2026 01:05
@MiddleSokilAI

Copy link
Copy Markdown
Collaborator

Review of head 4469fc734d8551acc4585636802390486058f96d. There are two request-isolation concerns in EVO_WORKER_KEEP_APP=1 and a runtime-validation gap to address before merge. These are static code-review findings, not runtime reproductions; neither isolation finding concerns the default classic mode.

1. Reset cached route controllers between kept-app requests

ApplicationSnapshot captures directly referenced objects, but does not traverse objects inside arrays. Laravel's RouteCollection stores Route objects in arrays, and Route::getController() caches its controller. There is no corresponding flushController() call in worker cleanup.

A controller retaining a constructor-injected Request or other request-specific state can therefore reuse that state on a later invocation, even after container instances are restored.

Requested change: reset cached controllers and relevant route-level request state between requests. Add a regression test with two requests to the same controller, different users/request values, and verify the second invocation receives only its own state. Closure-only routes do not cover this case.

2. Keep Eloquent boot state consistent with restored event listeners

Snapshot restoration restores the shared event dispatcher's listeners, while releaseApplication() preserves the model boot registry in kept-app mode.

A model first booted after the snapshot can lose its registered event listeners while remaining marked as booted. Subsequent requests then skip booting it again. This concerns late-booted custom/package models; it is not a claim that every built-in model is affected.

Requested change: restore model boot/global-scope state consistently with dispatcher state. Test a model first instantiated during request one and verify its creating/saving callbacks still run exactly once during request two. Unconditionally clearing all booted models alone can duplicate listeners already present in the snapshot.

3. Execute the actual worker smoke tests before merge

The reviewed checks showed 12 successes and one skipped Docker job. In build.yml, this job runs only on push/manual events and contains the actual FrankenPHP smoke tests. The PR's green checks therefore do not establish that these runtime tests passed.

Requested change: provide a successful runtime run for the reviewed head covering KEEP_APP=0, KEEP_APP=1, recycling and both regression scenarios above.

If keeping the initial change smaller is preferable, the experimental KEEP_APP lifetime could be split from basic opt-in worker support.

Review by SokilAI, AI assistant to Serhii Korneliuk.

@Seiger
Seiger merged commit 830003b into evolution-cms:3.6.x Oct 31, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants