Skip to content

feat: declared background workers + frankenphp_get_worker_handle()#2398

Open
nicolas-grekas wants to merge 1 commit intophp:mainfrom
nicolas-grekas:bgworker-minimal
Open

feat: declared background workers + frankenphp_get_worker_handle()#2398
nicolas-grekas wants to merge 1 commit intophp:mainfrom
nicolas-grekas:bgworker-minimal

Conversation

@nicolas-grekas
Copy link
Copy Markdown
Contributor

@nicolas-grekas nicolas-grekas commented May 7, 2026

Summary

Smallest useful slice of the background-worker work. Adds long-lived
non-HTTP PHP scripts declared via WithWorkerBackground() / background
in the Caddyfile worker block, plus the frankenphp_get_worker_handle()
primitive that gives the script a stream it can park on (stream_select)
to exit gracefully on shutdown / restart.

This carves the minimal surface out of the larger PR #2393 so it is small
enough to review and merge on its own. The richer features built on top
of these primitives are deferred to follow-ups.

What's in

  • PHP API: frankenphp_get_worker_handle(): resource — closes when
    FrankenPHP drains the worker (write end of the stop pipe is closed Go-side).
  • Go API: WithWorkerBackground(), WithWorkerScope(), NextScope,
    SetScopeLabel. num >= 1 is required for bg workers (no lazy-start
    in this build).
  • Lifecycle: boot the script, restart on crash with quadratic backoff,
    halt respawn at max_consecutive_failures. Exit-status 0 = cooperative
    restart, no failure counted.
  • Caddy: background flag inside php_server { workers ... }.
    Per-php_server scope isolation so two server blocks can declare the same
    worker name without colliding. Scope label resolved before Init via the
    cascade in caddy/scopelabel.go (host matcher → first listener address)
    so the very first metric emit on every bg worker carries the right
    m#<scope-label>:<name> prefix.

Deferred (kept out for review surface)

  • frankenphp_ensure_background_worker() and the lazy-start / readiness
    / boot-failure machinery it pulls in.
  • Catch-all (empty-name) workers.
  • Shared-state APIs (frankenphp_set_vars / frankenphp_get_vars).

Test plan

  • TestBackgroundWorkerLifecycle — bg worker boots, touches sentinel,
    parks on stop pipe, Shutdown() returns within 10s.
  • TestBackgroundWorkerCrashRestartsexit(1) on first boot,
    respawned thread runs the success branch.
  • TestBackgroundWorkerWithoutHTTP — bg worker doesn't intercept
    HTTP traffic; a regular request still serves.
  • TestBackgroundWorkerSameNameDifferentScope (internal) — two
    php_server scopes each declare "shared", both Init successfully and
    run independently.
  • TestNextScopeIsDistinct.

@nicolas-grekas
Copy link
Copy Markdown
Contributor Author

nicolas-grekas commented May 8, 2026

FYI I just did the split for now and didn't account for your last review on #2393 yet @AlliBalliBaba
I'm happy this is now half the size.

I woke with this idea for the next step: My app-declares-the-sidekicks requirement could be achieved with a protocol that wouldn't require adding the ensure() function;

in my current proposal we have this:

    # Catch-all bg worker, instantiated lazily by ensure(name)
    worker bin/jobs.php {
        background
        max_threads 16
    }

what we could do is: on bootstrap of frankenphp, we run bin/jobs.php with no argv arguments. This would trigger a signal to tell the script: echo a json with the description of the workers that should be started now. WDYT? Does this need more explanation? I think that'd address the extra complexity caused by lazy-starts.

@dunglas
Copy link
Copy Markdown
Member

dunglas commented May 8, 2026

What's the benefit over the Caddyfile/JSON config we already have? For the record, what you want to achieve is already possible: any PHP script, including workers, can call the Caddy REST API to dynamically tweak the FrankenPHP/Caddy config.

It would be nice to add an example showing how to do that btw.

@nicolas-grekas
Copy link
Copy Markdown
Contributor Author

nicolas-grekas commented May 8, 2026

I didn't know, worth checking if we can use that to let the PHP app start new bgworkers then. Feels strange at first that the app can do that - reconfigure itself. I'd be happy to know more about this and demo it.

@henderkes
Copy link
Copy Markdown
Contributor

what we could do is: on bootstrap of frankenphp, we run bin/jobs.php with no argv arguments. This would trigger a signal to tell the script: echo a json with the description of the workers that should be started now. WDYT? Does this need more explanation? I think that'd address the extra complexity caused by lazy-starts.

I don't know, it sounds hacky to me and we'd still need to implement calling a script and parsing the output. Perhaps we can just start without lazy starts first? My feeling is still that lazy starting doesn't offer any benefit without also auto-scaling down, so I'd be in favour of just shipping background workers at first. We can see how things go and implement general worker orchestration in the php bridge later when we get to task workers.

@henderkes
Copy link
Copy Markdown
Contributor

I didn't know, worth checking if we can use that to let the PHP app start new bgworkers then. Feels strange at first that the app can do that - reconfigure itself. I'd be happy to know more about this and demo it.

{"name":"blub","file_name":"/app/blub.php","num":1}
POST to localhost:2019/config/apps/frankenphp/workers

@nicolas-grekas
Copy link
Copy Markdown
Contributor Author

And how does this behave? I mean: what does this restart? all the HTTP app? How about failures? etc - any link to doc if there is one somewhere?

Adds a minimal background-worker surface: long-lived non-HTTP PHP scripts
declared via WithWorkerBackground() / `background` in the Caddyfile worker
block. Each worker exposes a stop-pipe stream via
frankenphp_get_worker_handle(); on FrankenPHP shutdown / restart Go closes
the write end so the script's stream_select returns EOF and the loop can
exit cleanly. Crash-restart with quadratic backoff, halted by
max_consecutive_failures. Per-php_server scope isolation so two server
blocks can declare the same worker name without colliding.

Deferred to follow-ups (kept out for review surface):
- frankenphp_ensure_background_worker() / lazy-start machinery
- Catch-all (empty-name) workers
- Shared-state APIs (frankenphp_set_vars / frankenphp_get_vars)

What lands:
- frankenphp_get_worker_handle() zif + stop-fd plumbing in C
- WithWorkerBackground() / WithWorkerScope() Go options; num >= 1 required
- backgroundWorkerThread handler: boot, restart with backoff, cap-on-crash,
  graceful exit on stop-fd close
- Per-scope lookup map keyed by user-facing name
- Caddy `background` worker flag + scope-label cascade (host matcher ->
  first listener address) so the metric prefix m#<scope-label>:<name> is
  set before the very first metric emit.

Tests:
- TestBackgroundWorkerLifecycle: bg worker boots, touches sentinel, parks,
  exits within 10s of Shutdown.
- TestBackgroundWorkerCrashRestarts: exit(1) -> respawn -> restarted
  sentinel.
- TestBackgroundWorkerWithoutHTTP: a regular HTTP request alongside a bg
  worker still serves.
- TestBackgroundWorkerSameNameDifferentScope: two servers each declare
  "shared", both run, distinct *worker per scope.
@henderkes
Copy link
Copy Markdown
Contributor

Oh that's to add a worker at runtime. It uses the json format and post appends, patch updates: https://caddyserver.com/docs/api

(and no, it shouldn't be used for this, the application shouldn't have to rely on the admin endpoint being up)

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