feat: declared background workers + frankenphp_get_worker_handle() - #2617
feat: declared background workers + frankenphp_get_worker_handle()#2617nicolas-grekas wants to merge 1 commit into
Conversation
Background workers are long-lived non-HTTP PHP scripts declared via WithWorkerBackground() or the `background` flag in Caddyfile worker blocks. The script runs in a loop: it is re-run on cooperative exit (status 0) and restarted with a quadratic backoff on crash, failing hard on max_consecutive_failures during startup only. frankenphp_get_worker_handle() returns a stream over the read end of a per-thread stop pipe; draining the thread (shutdown, reboot, handler transition) closes the write end, so a script parked in stream_select wakes up and can exit gracefully. Background workers attach to a Server through the existing WithWorkerServerScope(); their name is mandatory (it is the script's identity, exposed as FRANKENPHP_WORKER) and lives in the global worker namespace, which the Caddy module already qualifies per server.
|
Please rewrite the PR description to not be LLM slop reasoning with itself about what it did and why. I've tried reading this three times and I just can't. |
|
Sure, I'll let you know when I'm done, for now I just let it do the rebase 馃槄 |
henderkes
left a comment
There was a problem hiding this comment.
What happens here when a global background worker and a php_server scoped background worker share the same name and are both eligible for the same source file?
| } | ||
|
|
||
| /* Dup so the returned stream owns its fd: closing the stream (or request | ||
| * shutdown destroying it) never touches worker_stop_fds[0], which stays |
There was a problem hiding this comment.
aren't we leaking fd's on every restart then?
| // cooperative exit: the script is re-run with a reset backoff, unless | ||
| // the thread is being drained (beforeScriptExecution checks the state) | ||
| if exitStatus == 0 { | ||
| metrics.StopWorker(worker.name, StopReasonRestart) |
There was a problem hiding this comment.
without any further checks here, isn't this going to keep rapidly restarting a background script that just returns early? for worker scripts we have a guard, but I don't see one here.
| return | ||
| } | ||
|
|
||
| close(thread.drainChan) |
There was a problem hiding this comment.
| thread.handler.drain() | |
| close(thread.drainChan) |
henderkes
left a comment
There was a problem hiding this comment.
found another one, anyway, have you tested this on windows?
| int frankenphp_set_background_worker_and_get_stop_fd_write(void) { | ||
| is_background_worker = true; | ||
| /* Bg workers don't enforce max_execution_time; disarm any lingering timer. */ | ||
| zend_unset_timeout(); |
There was a problem hiding this comment.
we have to guard these #ifdef ZEND_MAX_EXECUTION_TIMERS because of macos
A worker marked
backgroundruns its script in a loop outside the HTTP request cycle.frankenphp_get_worker_handle()hands the script a stream that reaches EOF when FrankenPHP drains the worker, so it can park onstream_select()and exit gracefully on shutdown, reboot or restart.PHP API
frankenphp_get_worker_handle(): resource, closed when the worker is drained (the Go side closes the write end of a per-thread stop pipe). Throws when called outside a background worker. Each call dups the read fd into a fresh stream owned by its zval, so closing one never touches the fd the C side owns.Go API
WithWorkerBackground(). Background workers attach to aServerthrough the existingWithWorkerServerScope().num >= 1is required (no lazy-start in this build) and the name is mandatory, since it is the script's identity and is exposed as$_SERVER['FRANKENPHP_WORKER'].Lifecycle
backgroundWorkerThreadimplementsthreadHandlerand mirrorsworkerThread's state machine: boot, re-run on cooperative exit (status 0, backoff reset), crash-restart with quadratic backoff, hard failure onmax_consecutive_failuresduring startup only.drain()is wired intothread.shutdown()andrebootAllThreads(), so shutdowns and watch-triggered reboots wake parked background workers instead of waiting out the force-kill grace period.Caddy
backgroundflag inside worker blocks, in bothphp_serverand global ones.nameis required,matchis rejected. Metrics and logs use the worker name, which server-qualified naming already keeps unique acrossphp_serverblocks, so two blocks may declare the same worker name.Deferred
frankenphp_ensure_background_worker()and lazy-start machineryfrankenphp_set_vars/frankenphp_get_vars)frankenphp_start_background_worker()runtime API; the primitives here are compatible with itTests
TestBackgroundWorkerLifecycle: boots, touches its sentinel, parks on the stop pipe,Shutdown()returns within 10sTestBackgroundWorkerCrashRestarts:exit(1)on first boot, the respawned run touches the "restarted" sentinelTestBackgroundWorkerOnServer: inherits the server env,FRANKENPHP_WORKERcarries the name, HTTP requests on the same server still serveTestBackgroundWorkerValidation: name required,num >= 1, global name namespace, request matchers rejectedTestWorkerBackgroundConfig/RequiresName/RejectsMatch: Caddyfile parsingSupersedes #2543 and #2398.