feat: background workers = non-HTTP workers with shared state#2287
feat: background workers = non-HTTP workers with shared state#2287nicolas-grekas wants to merge 12 commits intophp:mainfrom
Conversation
e1655ab to
867e9b3
Compare
|
Interesting approach to parallelism, what would be a concrete use case for only letting information flow one way from the sidekick to the http workers? Usually the flow would be inverted, where a http worker offloads work to a pool of 'sidekick' workers and can optionally wait for a task to complete. |
da54ab8 to
a06ba36
Compare
|
Thank you for the contribution. Interesting idea, but I'm thinking we should merge the approach with #1883. The kind of worker is the same, how they are started is but a detail. @nicolas-grekas the Caddyfile setting should likely be per |
ad71bfe to
05e9702
Compare
|
@AlliBalliBaba The use case isn't task offloading (HTTP->worker), but out-of-band reconfigurability (environment->worker->HTTP). Sidekicks observe external systems (Redis Sentinel failover, secret rotation, feature flag changes, etc.) and publish updated configuration that HTTP workers pick up on their next request; with per-request consistency guaranteed via Task offloading (what you describe) is a valid and complementary pattern, but it solves a different problem. The non-HTTP worker foundation here could support both. @henderkes Agreed that the underlying non-HTTP worker type overlaps with #1883. The foundation (skip HTTP startup/shutdown, immediate readiness, cooperative shutdown) is the same. The difference is the API layer and the DX goals:
Happy to follow up with your proposals now that this is hopefully clarified. |
05e9702 to
8a56d4c
Compare
|
Great PR! Couldn't we create a single API that covers both use case? We try to keep the number of public symbols and config option as small as possible! |
Yes, that's why I'd like to unify the two API's and background implementations into one. Unfortunately the first task worker attempt didn't make it into |
|
The PHP-side API has been significantly reworked since the initial iteration: I replaced The old design used
Key improvements:
Other changes:
|
cb65f46 to
4dda455
Compare
|
Thanks @dunglas and @henderkes for the feedback. I share the goal of keeping the API surface minimal. Thinking about it more, the current API is actually quite small and already general:
The name "sidekick" works as a generic concept: a helper running alongside. The current set_vars/get_vars protocol covers the config-publishing use case. For task offloading (HTTP->worker) later, the same sidekick infrastructure could support:
Same worker type, same So the path would be:
The foundation (non-HTTP threads, cooperative shutdown, crash recovery, per-php_server scoping) is shared. Only the communication primitives differ. WDYT? |
b3734f5 to
ed79f46
Compare
|
|
|
Hmm, it seems they are on some versions, for example here: https://github.com/php/frankenphp/actions/runs/23192689128/job/67392820942?pr=2287#step:10:3614 For the cache, I'm not aware of a Github feature that allow to clear everything unfortunately 🙁 |
|
Marc has already articulated most of where I land, so I'll stay short and add a few angles I don't think have come up yet. On the DNS / Redis / Symfony service name analogy, I think the comparison doesn't hold. Those APIs deliberately separate concerns: DNS has About testability: a global function whose call can spawn a worker process is hostile to unit tests. Libraries adopting this will either need to wrap it in their own abstraction or give up on isolation in tests. A store-shaped API is materially more mockable. Also, about the principle of least surprise: Finally, genuine question about the API: is it possible to unset a key? |
|
Edited: I missed last response by @alexandre-daubois and I agree with him. API updated. Thanks, everyone, for the depth of this one! @nicolas-grekas for the huge amount of work, and @henderkes, @AlliBalliBaba, @alexandre-daubois, @dbu for the careful pushback. I've read through the whole thread, and I think we're close to merging it. Most of the back-and-forth is really Here's my opinion on this: Caddyfile and the whole Go/C runtime stay as Nicolas designed them, but we make small changes to the PHP API:
On unsetting a key: with snapshot semantics it's just set_vars a new array without the key — no dedicated primitive needed. If we ever add per-key writes we'd add a matching unset. We can apply the same logic for #2319, drop the
The fact that a worker picks up the task is an implementation detail. WDYT? |
|
Looks like the best of both worlds @dunglas. Dropping Sorry if this was answered somewhere in the comments: what's the defined behavior when the caller has no worker scope, e.g. called from an HTTP request context, a CLI script, or any non-worker code path? Should it be no-op or throw a |
|
I would throw too |
Why do we need a timeout for the get_vars? Shouldn't that just return immediately since the prior
Perhaps this should return an object on which php can call
You're talking about I'm generally happy with that direction, but I'd still want to argue the case for being able to define multiple background worker scripts. We went out of our way to support non-framework code all the way up until this point, for the gain I see (for a single script would already mostly disappear with an explicit |
|
Thanks @dunglas for the proposal, I think we're very close. Let me suggest a small refinement that I think fully addresses the debuggability objection without giving up anything structural. Proposal (noted about #2319 also)frankenphp_require_background_worker(string $name, float $timeout = 30.0): void
frankenphp_set_vars(array $vars): void
frankenphp_get_vars(string|array $name): array
frankenphp_get_worker_handle(): resourceFour functions, same count as your proposal. Two differences:
|
|
I don't like WDYT about |
|
We wouldn't ensure a background worker, we would ensure a background worker is running. I'm with you though, require feels wrong. I'm still in favour of |
Yes, I think we all meant
We already don't do frankenphp sapi bootup (embed instead) in the cli version. With my proposed php-src change it would use the cli sapi, still without the frankenphp extension.
I was thinking of potential worker orchestration from php side later. But thinking about it again, we could do that with streams too, so it's fine.
Sorry, I should've re-read the current version. We've been through so many iterations, at this point it's all getting a bit fuzzy, haha. No further objections from my side then. |
|
Thanks @henderkes for the follow-up confirming no further objections. On your php-src change proposal: if it lands and makes FrankenPHP CLI use the I pushed a set of refinements on top of the previous round. Summary of what changed and why: 1. Rename
|
|
The last version of the public API sounds good to me! Excellent work. |
I'm sorry, but I strongly disagree here.
When this is a real concern (and I'm not sure it is), I think we should shut down workers that haven't been asked for in a while. I'm all for keeping it as simple as possible: |
|
ensure/start I'll follow your lead - @dunglas any stronger opinion?
I agree, and that's now closer to one behavior! the only special case is failing early when a worker cannot start while http workers didn't call handle_request yet. I think that's a net safety gain that will improve robustness for ppl that can start things early, because it makes putting frankenphp live safer. The backoff mechanism of http workers will help recover from that automatically on startup when possible, while providing quicker feedback. |
|
This PR is absolutely massive. 3k loc change ... I'd argue breaking it down by scope and merge in minimal working systems, iterating as you go and paying attention to related issues so you learn the pain points users experience. For this PR ... There's so much going on, and some of it is not-obviously-wrong. There are at least 3 potential race conditions that jump out at me immediately, double close issues (which can create a security vulnerability or corruption), workers potentially getting stuck in half-started states, caddy file ordering issues, lack of synchronization, etc. Sure, many of these problems "go away" by enforcing exactly one worker thread and assuming users only use caddy to run frankenphp, but it would be a ton of work to remove that constraint if/when we want to. I'd be happy to review the whole diff, but my personal preference is to break it down. Here's where I see some seams:
You could stop here, or keep going. User demand (how can I add more instances?) gives a good reason to continue.
Each of these is independently useful, independently reviewable, and (importantly) independently revertible if a design choice turns out to be wrong. Step 4 alone covers probably 80% of what users will actually reach for. If steps 5–7 take another release or two while patterns emerge from issues, that's fine; the feature is still shipped. The other thing this buys you: each slice lets the next one's API be informed by what users actually do with the previous one. Shipping 3k lines at once locks in set_vars / get_vars / ensure / batch-names / scoping / catch-all semantics before anyone has written a single real background worker against them. This is good work, and I'm excited to see where it goes. |
Second step of the split suggested in php#2287: land the persistent-zval subsystem as a standalone, reviewable header, independent of background workers. Easier to review in isolation: this is the subsystem most likely to hide latent refcount or memory-lifetime bugs. ## What - persistent_zval.h (renamed from the bg_worker_vars.h draft, prefix dropped for generality): - persistent_zval_validate: whitelist (scalars, arrays of allowed values, enum instances). Everything else is rejected so callers can fail fast before allocating. - persistent_zval_persist: deep-copy request -> persistent memory (pemalloc). Fast paths baked in: interned strings are shared, opcache-immutable arrays are passed by pointer without copying or owning. - persistent_zval_free: deep-free; skips interned strings and immutable arrays (borrowed, not owned). - persistent_zval_to_request: deep-copy persistent -> fresh request memory. Enums are re-resolved by class + case name on each read. - frankenphp.c: the header is included only when FRANKENPHP_TEST_HOOKS is defined. The first real consumer (background workers) will drop the guard. - Test hook gated on FRANKENPHP_TEST_HOOKS: - PHP function frankenphp_test_persist_roundtrip(mixed): mixed runs validate -> persist -> to_request -> free and returns the result. - Registered at MINIT via zend_register_functions so it never appears in ext_functions[] and never ships in production builds. - CI workflows set -DFRANKENPHP_TEST_HOOKS in CGO_CFLAGS (tests.yaml + sanitizers.yaml). windows.yaml is the release build, not a test runner, and stays untouched. - TestPersistentZvalRoundtrip drives the PHP fixture covering scalars, interned + long strings, nested arrays with mixed keys, enums (with === identity preserved), and invalid inputs (stdClass, resources, nested bad values) that must throw LogicException. Skips cleanly when the flag isn't set. ## Notes - Build verified without the flag (production path, no unused-function warnings) and with the flag (test path, function registered). - The FRANKENPHP_TEST_HOOKS guard around the header include goes away in the PR landing the first real caller; the test hook itself goes away in the same step once end-to-end tests cover the code paths. - Budget: +380 / -4 (header 252, frankenphp.c 49, test 54, PHP fixture 88, workflow 4).
Third step of the split suggested in php#2287: land the handler-interface extension point that later handler types (background workers) need, without introducing any new behaviour. Each handler gains a drain() method, called by drainWorkerThreads right before drainChan is closed. All current implementations (regularThread, workerThread, inactiveThread, taskThread) are no-ops, so observable behaviour is unchanged. A later handler that needs to wake up a thread parked in a blocking C call (e.g. by closing a stop pipe) plugs its signal in here without modifying drainWorkerThreads again. - phpthread.go: interface gains drain(). - threadregular.go / threadworker.go / threadinactive.go / threadtasks_test.go: empty drain() on each handler. - worker.go: drainWorkerThreads calls thread.handler.drain() right before close(thread.drainChan). Full test suite and caddy module tests pass under -race.
|
Thanks @withinboredom that's really useful! I think I found and fixed all the issues you described. |
Introduces a small, self-contained primitive that unblocks a PHP thread stuck in a blocking call (sleep, synchronous I/O, etc.) so the graceful drain used by RestartWorkers and DrainWorkers can make progress instead of waiting for the block to return on its own. The primitive is useful on its own and gives follow-up graceful-shutdown work a reviewed foundation to build on. - frankenphp.c: add frankenphp_init_force_kill / frankenphp_save_php_timer / frankenphp_force_kill_thread / frankenphp_destroy_force_kill. The per-thread PHP timer handle (Linux/FreeBSD ZTS) or OS thread handle (Windows) is captured at thread boot and stored in a pre-sized array so the kill path can fire from any goroutine without touching per-thread PHP state. Linux/FreeBSD arm PHP's max_execution_time timer (delivers SIGALRM -> "Maximum execution time exceeded"); Windows uses CancelSynchronousIo + QueueUserAPC to interrupt I/O and alertable waits; macOS and other platforms are a safe no-op (the thread is abandoned and exits when the blocking call returns naturally). - phpmainthread.go: wire frankenphp_init_force_kill into initPHPThreads (sized to maxThreads, matching the thread_metrics allocation) and frankenphp_destroy_force_kill into drainPHPThreads. - worker.go: add a 5-second graceful-drain grace period to drainWorkerThreads. Once elapsed, arm the force-kill primitive on any thread still outside Yielding and keep waiting on ready.Wait(); the kill lets the thread return from its blocking call so the drain completes in bounded time instead of hanging. - worker_test.go + testdata/worker-sleep.php: TestRestartWorkersForceKillsStuckThread drives the path end-to-end. A worker blocks inside sleep(60) below frankenphp_handle_request (so drainChan close can't reach it); the test asserts RestartWorkers returns within 8s (grace + slack). The test skips on platforms without the underlying primitive.
… 'thread-handler-drain-seam' into bg-worker-integration * commit '6e14d11': feat: cross-platform force-kill primitive for stuck PHP threads * commit '11feb20': style: clang-format persistent_zval.h feat: persistent-zval helpers (deep-copy zval trees across threads) * commit '25acb19': refactor: add drain() seam to threadHandler interface
Fifth step of the split suggested in php#2287. Builds on the minimal background worker from step 4: - PHP function frankenphp_ensure_background_worker(string $name, float $timeout = 30.0): void. Lazy-starts the named worker if not already running, waits for it to publish its first vars, returns void on success and throws on timeout / boot failure. - Two-mode semantics: - Bootstrap (called from an HTTP worker's boot phase, before frankenphp_handle_request): fail-fast. Watches sk.bootFailure on a 50ms ticker alongside ready/aborted/deadline so a broken dependency visibly fails the HTTP worker instead of serving degraded traffic. - Runtime (inside frankenphp_handle_request, classic request path): tolerant. Waits up to the timeout, letting the restart-with- backoff cycle recover from transient boot failures. - Registry + lookup layer: - backgroundWorkerRegistry tracks the template options (env, watch, maxConsecutiveFailures, requestOptions) from one declaration plus its live instances. Catch-all registries have a maxWorkers cap. - backgroundWorkerLookup holds a name map + a single catch-all slot. - reserve() atomic insert-or-return-existing; abortStart() wakes ensure waiters via a new aborted channel so a reserve/abandon race can't hang them until deadline. - Catch-all worker: a name-less bg declaration matches any ensure() name at runtime, subject to max_threads (default 16). Caddyfile support: `worker { background; file ... }` without `name`. - Named lazy path: a num=0 named declaration defers thread attach until ensure() asks for it; the worker struct created at init is reused rather than duplicated. - Boot-failure enrichment: bootFailureInfo now carries the captured PG(last_error_*) ("<msg> in <file> on line <n>"), grabbed on the C side before php_request_shutdown clears it. Ensure's timeout error surfaces it. - $_SERVER['FRANKENPHP_WORKER_NAME'] and $argv[1] are now populated for background workers so catch-all instances can tell which instance they are. - calculateMaxThreads reserves per-bg-worker thread budget separately from the HTTP worker count, scaling with max_threads on catch-alls, so lazy starts have room to schedule. - TestEnsureBackgroundWorkerNamedLazy: num=0 named declaration, ensure() from a non-worker request starts it + reads its vars. - TestEnsureBackgroundWorkerCatchAll: two ensures with distinct names against a single catch-all declaration; each publishes its own identity via $_SERVER. - TestEnsureBackgroundWorkerCatchAllCap: max_threads=2 on the catch- all; third distinct name hits the cap error. - TestEnsureBackgroundWorkerUndeclared: ensure() on a name that is neither named nor covered by a catch-all returns the config error. - Step-4 tests (TestBackgroundWorker, TestBackgroundWorkerErrorPaths, TestBackgroundWorkerRestartForceKillsStuckThread) still pass. - Batch name support on ensure (string[] argument): follow-up. - Per-php_server scoping (BackgroundScope): step 6. - Pools (num > 1, named-worker max_threads > 1) and multi-entrypoint: step 7.
Sixth step of the split suggested in php#2287. Builds on step 5: - BackgroundScope opaque type (int under the hood; obtain values via NextBackgroundWorkerScope, a counter). Zero is the global/embed scope; each php_server block gets a distinct non-zero scope. - Per-scope lookups: - backgroundLookups map[BackgroundScope]*backgroundWorkerLookup replaces the single global backgroundLookup. - buildBackgroundWorkerLookups iterates the declared bg workers into their scope's lookup; each declaration still gets its own registry. - getLookup(thread) resolves the active scope from the calling thread: worker handler -> request context -> global (0). - Options to drive the scope: - frankenphp.WithWorkerBackgroundScope(scope) tags a declaration with a scope. - frankenphp.WithRequestBackgroundScope(scope) tags a request so ensure/get_vars from a regular (non-worker) request resolve to the right block's lookup. - Caddy wiring: FrankenPHPModule.Provision allocates one scope per module instance (idempotent across re-provisions) and threads it into both worker declarations and ServeHTTP. Two php_server blocks can now declare background workers with the same user-facing name without colliding. - Global workersByName collision dropped for bg workers: bg workers resolve through their scope's lookup, so the same PHP-visible name can appear in two scopes without tripping the duplicate check. ## Tests - TestBackgroundWorkerScopeIsolation declares two bg workers named "shared" in distinct scopes, publishes distinct markers from each, and reads them back via scope-tagged requests. Confirms lookups resolve independently. All step-4 and step-5 tests still pass. ## Deferred to step 7 - Pools (num > 1 per named worker, max_threads > 1 for named workers). - Multiple declarations sharing one entrypoint file in one scope.
Seventh (final in the split) step of the split suggested in php#2287. Lifts the remaining constraints from the minimal path: - Pools: named bg workers can now declare num > 1 (pool of threads per worker) and max_threads > 1. Each thread in the pool shares the same backgroundWorkerState, so set_vars / get_vars are scoped per-worker-name, not per-thread. - Per-thread stop-pipe: the write fd moved from worker to handler. Each thread in a pool gets its own stop pipe, so drain() can wake them independently. Pools no longer overwrite one another's fd through the shared worker struct. - Multi-entrypoint: multiple named bg workers in the same scope can share the same entrypoint file. Each gets its own registry from buildBackgroundWorkerLookups, so they inherit independent env/watch/failure-policy options. Drops the filename-uniqueness check for bg workers (it was already skipped via allowPathMatching, but this step lifts the last Caddyfile-level rejection). - Caddyfile: `num > 1` and `max_threads > 1` on named background workers no longer error out. Catch-all semantics unchanged: max_threads caps lazy-started instance count. ## Tests - TestBackgroundWorkerPool: num=3 pool, verifies all threads boot and share state through set_vars/get_vars. - TestBackgroundWorkerMultiEntrypoint: two named bg workers sharing one entrypoint file resolve to distinct instances by name. All previous bg worker tests still pass.
Eighth step on top of php#2287's split. User-facing polish on the ensure API plus a small $_SERVER flag, both landing together because they are small and closely related to the worker-handling surface. - frankenphp_ensure_background_worker now accepts string|array. The array form shares one deadline across all names and preserves the same mode semantics (fail-fast in HTTP-worker bootstrap, tolerant everywhere else). Empty arrays and non-string elements raise clear ValueError / TypeError instead of silent no-ops or cryptic failures. - $_SERVER['FRANKENPHP_WORKER_BACKGROUND'] = true in background worker scripts, alongside the existing FRANKENPHP_WORKER_NAME and argv/argc wiring. Gives scripts a single-key branch for "am I a bg worker?" without checking each function independently. ## Tests - TestEnsureBackgroundWorkerBatch: three workers ensured in one call, each publishing its own name, all read back after the batch returns. - TestEnsureBackgroundWorkerBatchEmpty: [] rejected with ValueError. - TestEnsureBackgroundWorkerBatchNonString: ['a', 42] rejected with TypeError before any worker starts. - TestBackgroundWorkerServerFlag: bg worker sees FRANKENPHP_WORKER_BACKGROUND=true in $_SERVER. ## Deferred - CLI-mode function hiding was in the sidekicks draft but turned out to be dead code (the frankenphp PHP module isn't loaded in CLI, so the functions don't exist there either). - C-side per-request get_vars cache: step 9 (needs benchmarks). - Docs: step 10 (will cover the final API including batch ensure).
Ninth step on top of php#2287's split. Adds a C-side per-request cache keyed on the background worker's vars version so repeated get_vars reads within one request run at O(1) and return the same HashTable pointer. ## What - __thread HashTable *bg_vars_cache maps worker name -> { version, cached_zval }. Initialized lazily on first get_vars call per request. Destroyed before php_request_shutdown tears down request memory, so the cached zvals are torn down while their backing request-memory structures are still alive. - go_frankenphp_get_vars grew callerVersion / outVersion out-params: - If callerVersion matches the live varsVersion, Go skips the deep copy entirely and only reports outVersion. The C side reuses its cached zval (with ZVAL_COPY for refcount bump). - If versions differ, Go runs the normal copy-under-RLock path and reports the fresh version for the caller to cache. - PHP_FUNCTION(frankenphp_get_vars) consults the cache before calling Go, then either reuses the cached zval (hit) or stores the fresh copy (miss). Identity is preserved: $vars === $prev_vars holds across reads within one request. ## Tests - TestGetVarsCacheIdentity: two reads in one request return the same zval (=== true). - TestGetVarsCacheManyReads: 500 reads in one script complete without memory corruption, proving the cache tear-down at request end is correct. All 16 existing bg worker tests still pass.
Covers the full public API landed across the preceding steps: the named/catch-all Caddyfile configuration, the two-mode frankenphp_ensure_background_worker() semantics (fail-fast at HTTP bootstrap, tolerant elsewhere) and its batch form, the pure-read frankenphp_get_vars(), frankenphp_set_vars() with its allowed value types (scalars, nested arrays, enum cases), the signaling stream via frankenphp_get_worker_handle(), and runtime behaviour (dedicated threads, $_SERVER flags, crash recovery with stale vars, 5-second grace period followed by force-kill, per-php_server scoping, and the pool / multi-entrypoint limits).
Note
Description updated to reflect the latest pushes. API names and semantics are final pending review; see the thread for the back-and-forth that led here.
Summary
Background workers are long-running PHP workers that run outside the HTTP cycle. They observe their environment (Redis, DB, filesystem, etc.) and publish variables that HTTP threads (workers or classic requests) read per-request, enabling real-time reconfiguration without restarts or polling.
PHP API
Four functions:
frankenphp_ensure_background_worker(string|array $name, float $timeout = 30.0): void— declares a dependency on one or more background workers. Lazy-starts them if needed, blocks until each has calledset_vars()at least once or the timeout expires. Two behaviors depending on caller:frankenphp_handle_request): fail-fast. Any boot failure throws immediately with the captured details instead of waiting for the backoff cycle. Use for strict dependency declaration at boot.frankenphp_handle_request, classic request-per-process): tolerant lazy-start. First caller pays the startup cost; later callers see the worker already reserved. Processes only start workers they actually exercise.frankenphp_set_vars(array $vars): void— publishes vars from a background worker script (persistent memory, cross-thread). Skips all work when data is unchanged (===check).frankenphp_get_vars(string $name): array— pure read. Returns the latest published vars. Throws if the worker isn't running or hasn't calledset_vars()yet. Generational cache: repeated calls within a single HTTP request return the same array instance (===is O(1)).frankenphp_get_worker_handle(): resource— readable stream for shutdown signaling. Closed on shutdown (EOF).In CLI mode (
frankenphp php-cli), none of these functions are exposed (MINIT-level hiding viazend_hash_str_del).function_exists()returnsfalse, so library code can degrade gracefully.Caddyfile configuration
backgroundmarks a worker as non-HTTPnamespecifies an exact worker name; workers withoutnameare catch-all for lazy-started namesmax_threadson catch-all sets a safety cap for lazy-started instances (defaults to 16)max_consecutive_failuresdefaults to 6 (same as HTTP workers)max_execution_timeautomatically disabled for background workersphp_serverblock has its own isolated scope (opaqueBackgroundScopetype managed byfrankenphp.NextBackgroundWorkerScope())Shutdown
On restart/shutdown, the signaling stream is closed. Workers detect this via
fgets()returningfalse(EOF). Workers have a 5-second grace period. In-flightensure_background_workercalls unblock onglobalCtx.Done()instead of waiting out their timeout.After the grace period, a best-effort force-kill is attempted:
max_execution_timetimer cross-thread viatimer_settime(EG(max_execution_timer_timer))CancelSynchronousIo+QueueUserAPCinterrupts blocking I/O and alertable waitsDuring the restart window,
get_varsreturns the last published data (stale but available, kept in persistent memory across restarts). A warning is logged on crash.Boot-failure reporting
When a background worker fails before calling
set_vars,ensure_background_workerthrows aRuntimeExceptionwith the captured details: worker name, resolved entrypoint path, exit status, number of attempts, and the last PHP error (message, file, line) captured fromPG(last_error_*).Forward compatibility
The signaling stream is forward-compatible with the PHP 8.6 poll API RFC.
Poll::addReadableaccepts stream resources directly; code written today withstream_selectwill work on 8.6 withPoll, no API change needed.Architecture
php_serverscope isolation via opaqueBackgroundScopetype. Internal registry is unexported.backgroundWorkerThreadhandler implementingthreadHandlerinterface, decoupled from HTTP worker code paths.drain()closes the signaling stream (EOF) for clean shutdown signaling.pemalloc) withRWMutexfor safe cross-thread sharing.set_varsskip: uses PHP's===(zend_is_identical) to detect unchanged data, skips validation, persistent copy, write lock, and version bump.IS_ARRAY_IMMUTABLE).ZSTR_IS_INTERNED): skip copy/free for shared-memory strings.ensure_background_workeraccepts a batch of names with a shared deadline; fail-fast in bootstrap mode reports the failing worker's details.$_SERVER['FRANKENPHP_WORKER_NAME']set for background workers.$_SERVER['FRANKENPHP_WORKER_BACKGROUND']set for all workers (true/false).Example
Test coverage
Unit tests, integration tests, and one Caddy integration test covering: bootstrap fail-fast, runtime tolerant lazy-start, multi-name ensure, get_vars pure read, set_vars validation (types, objects, refs), CLI function hiding, enum support, binary-safe strings, multiple entrypoints, crash-restart reclassification, boot-failure rich errors, signaling stream, worker restart lifecycle, named auto-start with
m#prefix, edge cases (empty name, negative timeout, timeout=0).All tests pass on PHP 8.2, 8.3, 8.4, and 8.5 with
-race. Zero memory leaks on PHP debug builds.Documentation
Full docs at
docs/background-workers.md.