Skip to content

feat: declared background workers + frankenphp_get_worker_handle() - #2617

Open
nicolas-grekas wants to merge 1 commit into
php:mainfrom
nicolas-grekas:bgworker-server
Open

feat: declared background workers + frankenphp_get_worker_handle()#2617
nicolas-grekas wants to merge 1 commit into
php:mainfrom
nicolas-grekas:bgworker-server

Conversation

@nicolas-grekas

Copy link
Copy Markdown
Contributor

Reopens #2543, which GitHub auto-closed when #2499 merged and refactor/phpserver was deleted. The base branch is gone, so #2543 itself can no longer be reopened or retargeted; this is the same single commit rebased onto main. Supersedes #2398.

A worker declared with WithWorkerBackground() / background in the Caddyfile runs its script in a loop outside the HTTP request cycle. The frankenphp_get_worker_handle() primitive hands the script a stream that reaches EOF when FrankenPHP drains the worker, so it can park on stream_select() and exit gracefully on shutdown, reboot or restart.

What's in

  • PHP API: frankenphp_get_worker_handle(): resource, closes when the worker is drained (the Go side closes the write end of a per-thread stop pipe).
  • Go API: WithWorkerBackground(). Background workers attach to a Server through WithWorkerServerScope(); num >= 1 is required (no lazy-start in this build) and the name is mandatory since it is the script's identity ($_SERVER['FRANKENPHP_WORKER']).
  • Lifecycle: backgroundWorkerThread implements threadHandler and mirrors workerThread's state machine: boot, re-run on cooperative exit (status 0, backoff reset), crash-restart with quadratic backoff, hard failure on max_consecutive_failures during startup only.
  • Caddy: background flag inside worker blocks (php_server and global). name is required, match is rejected. Metrics and logs use the worker name, which the server-qualified naming already keeps unique across php_server blocks, so two blocks can declare the same worker name.

Rebase onto main

The commit is unchanged in substance. Four adjustments were needed against current main:

  • toWorkerOptions() gained opts = append(opts, wc.options...) in fix: review follow-ups on the php_server refactor聽#2565; the new WithWorkerBackground() is appended alongside it rather than replacing it.
  • newWorker()'s global-filename check was restructured by fix: review follow-ups on the php_server refactor聽#2565 into a o.server == nil block that also rejects unscoped request matchers. The background exemption is now nested inside it: background workers are keyed by name and stay out of globalWorkersByPath, so several may share one script. The background validation still runs earlier in the function, so a background worker with a matcher keeps its own "cannot match requests" error rather than falling through to the unscoped-matcher one.
  • frankenphp_opcache_restart_hook() and the zend_accel_schedule_restart_hook wiring were removed from frankenphp.c on main; the conflict only carried them as context, so they are not reintroduced.
  • NewServer() is now NewServer(root string, options ...ServerOption), so bgworker_test.go uses WithServerName() / WithServerEnv().

drain() is still wired into thread.shutdown() and rebootAllThreads(), so shutdowns and watch-triggered reboots wake parked background workers instead of hitting the force-kill grace period.

Deferred (kept out for review surface)

Test plan

  • TestBackgroundWorkerLifecycle: bg worker boots, touches sentinel, parks on the stop pipe, Shutdown() returns within 10s.
  • TestBackgroundWorkerCrashRestarts: exit(1) on first boot, the respawned run touches the "restarted" sentinel.
  • TestBackgroundWorkerOnServer: server-scoped worker inherits the server env, FRANKENPHP_WORKER carries the name, HTTP requests on the same server still serve.
  • TestBackgroundWorkerValidation: name required, num >= 1, global name namespace, request matchers rejected.
  • TestWorkerBackgroundConfig / RequiresName / RejectsMatch (Caddyfile parsing).

Both modules pass go vet including test files, go build compiles the C side, and frankenphp.c is clang-format clean. The runtime tests need CI: on my box (WSL2) per-thread engine bootstrap of an embed ZTS build is pathologically slow and linking needs dev libs I do not have, so every Init()-based test is unrunnable locally, including on unmodified base commits.

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.
@henderkes

Copy link
Copy Markdown
Contributor

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.

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.

2 participants