Concurrency, statics and long-lived processes
This page is for engineers who write or review PHP for Exponential. Two facts make some classic PHP habits unsafe: under Velocity one process serves many requests, and on any server many processes (and sometimes several machines) serve one site at the same time. The rules here come from Kernel architecture and the worker pool specification.
Contents
- Two kinds of sharing
- Statics and singletons
- Registrations must be idempotent
- Ending a request
- Code that changes under a running worker
- Resources the application must manage
- Races between processes
- Testing for these bugs
Two kinds of sharing
| Sharing | Where | Typical bug |
|---|---|---|
| In time: one process, many requests | Velocity persistent workers, the warm-up | a value from request A appears in request B |
| In space: many processes, one site | every server, several web nodes, cronjobs beside web requests | two processes write the same file or row |
Statics and singletons
The kernel uses static caches and singletons widely (eZINI::instance(), eZDB::instance(), per-class caches).
Velocity restores static class properties to their start-up snapshot after each request, so a static cache does not
leak between requests in a normal worker. Two cases still go wrong:
- State set during the warm-up becomes every worker's baseline. A warm-up resets user-class statics to their defaults and keeps only configuration and type registries.
- State the snapshot cannot see: globals named in
keepGlobals, registries filled once withinclude_once, resources held in statics (connections, file handles).
The rule for kernel code: no per-request data in statics without a reset. Prefer passing values as parameters, and keep request-scoped caches keyed on something that changes per request.
Registrations must be idempotent
A listener, handler or callback attached once per request in a long-lived worker accumulates: the second request runs
it twice. Register in a way that is a no-op when the registration exists already, and name listeners as
Class::method strings so duplicates are detectable. This applies to ezpEvent listeners, template operator
registrations and autoloaders (Events, extension points and plugins).
Ending a request
- Do not use
exitfor flow control. Kernel code ends a request througheZExecution::cleanExit(). - Under Velocity,
exitanddieare rewritten to end the request, not the worker. - Do not wrap a download followed by
cleanExit()in acatch ( Exception )block that also catches the request-ending signal: the page is appended to the download and the output is doubled.
Code that changes under a running worker
Workers inherit the classes the warm-up loaded. After a deploy:
- a new class named in a shipped INI file is unknown to a worker started before the deploy; guard its use with
class_exists()where the kernel loads it, asezpKernelWebdoes; - a changed class reaches the workers only after
exp:velocity restartorexp:velocity deploy; - a changed template or stylesheet needs no restart, only the template caches and Velocity's response cache.
Resources the application must manage
| Resource | What to do |
|---|---|
| Database transaction state | commit or roll back before the request ends; never leave a transaction open |
| Database connection in the warm-up | close it before the fork, so workers do not share a socket |
| File handles, stream contexts, cURL handles | close them in the request that opened them |
| Signal handlers | do not install them in request code |
Races between processes
- Files under
var/may be written by different operating system users (the web server user, the user that runs cron or the console, Velocity's worker user). Create files with the installation's file modes and test with mixed users (File modes). - Cache files go through
eZClusterFileHandler, which handles concurrent generation on shared storage. - Cronjob parts must be safe to run twice and to be interrupted.
- SQLite serializes writers with a lock file; long write transactions block other writers.
Testing for these bugs
- Run a test twice in the same process: state left behind by the first run shows up in the second.
- Under Velocity, request several distinct URLs in a row and check that each returns its own content.
- A test that passes alone and fails in the whole run shares state with another test; run the two files together to find the pair (CI gates).
- Watch the Velocity log for workers replaced over the memory ceiling: a leak.
Related pages
- Velocity persistent workers
- The request lifecycle
- Performance in extensions
- Free documentation: this page is free to copy, translate and improve under the GNU FDL 1.3.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.