Skip to content

feat: background workers (config + ensure)#2393

Open
nicolas-grekas wants to merge 8 commits intophp:mainfrom
nicolas-grekas:sidekicks-config
Open

feat: background workers (config + ensure)#2393
nicolas-grekas wants to merge 8 commits intophp:mainfrom
nicolas-grekas:sidekicks-config

Conversation

@nicolas-grekas
Copy link
Copy Markdown
Contributor

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

feat: background workers (config + ensure)

First half of the split suggested in #2287. Lands a minimum-viable background-worker subsystem: config surface, lifecycle, lazy-start via ensure(), per-php_server scoping, named pools, multi-entrypoint, plus a $_SERVER flag for bg-aware scripts. The worker-to-HTTP shared-state APIs (frankenphp_set_vars / frankenphp_get_vars) and the docs are deferred to a follow-up PR — they're independent and easier to review separately.

What lands

PHP API

  • frankenphp_ensure_background_worker(string|array $name, ?float $timeout = null): void — declares a dependency on one or more bg workers. Lazy-starts the named worker (or pulls from a catch-all) if not already running, then blocks until the worker calls frankenphp_get_worker_handle() (the readiness signal) or the timeout fires. null (the default) falls back to FrankenPHP's internal default deadline; a value <= 0 raises ValueError. The actual default is intentionally not exposed in the signature so it can become tunable later (e.g. via Caddyfile or env) without an API break. Input is validated upfront (ValueError for empty array / empty string / duplicate names / non-positive timeout; TypeError for non-string elements) so a bad batch never leaves a half-spawned set behind. On timeout the error names what didn't happen — a worker that never reached frankenphp_get_worker_handle() gets a self-teaching diagnostic instead of a silent hang.
  • frankenphp_get_worker_handle(): resource — readable stream that signals graceful shutdown. PHP scripts park on stream_select; FrankenPHP closes the write end during drain so select wakes with EOF. Calling this also signals to ensure() that the worker has reached its main loop, so a well-formed bg worker satisfies both contracts with one call.

The follow-up PR will tighten ensure() to also require a first call to frankenphp_set_vars() — same ensure() signature, progressively stronger guarantee, no caller-visible API change.

In CLI mode these functions aren't exposed.

Caddyfile

php_server {
    # HTTP worker (unchanged)
    worker public/index.php { num 4 }

    # Named bg worker, eagerly started
    worker bin/jobs.php {
        background
        name job-runner
        num 1
    }

    # Catch-all bg worker, instantiated lazily by ensure(name)
    worker bin/jobs.php {
        background
        max_threads 16
    }
}
  • background marks a worker as non-HTTP.
  • name pins an exact worker name; declarations without name are catch-alls for lazy-started instances.
  • num on a named bg worker eagerly starts that many instances; num 0 (or omitted) defers start until ensure().
  • max_threads on a catch-all caps how many distinct lazy-started instances it can host.
  • max_consecutive_failures defaults to 6 (same as HTTP workers).
  • max_execution_time is automatically disabled for bg workers.

Go API

  • WithWorkerBackground() marks a worker declaration as background.
  • WithWorkerScope(scope) tags a declaration with an isolation scope.
  • WithRequestScope(scope) tags a request so ensure() from a regular HTTP request resolves to the right block's lookup.
  • NextScope() hands out a fresh Scope value (opaque uint64 under the hood; zero is the global/embed scope). The type is intentionally generic so it can be reused for other per-server contexts (e.g. Mercure hubs, Prometheus labels).

Per-php_server scoping

Each php_server block gets its own scope. The same user-facing worker name can live in multiple blocks without collision; ensure() resolves through the calling thread's scope (worker handler → request context → global).

Pools and multi-entrypoint

  • num > 1 on a named bg worker spawns N threads sharing the same name. Each thread has its own stop pipe so drain can wake them independently.
  • Two named bg workers in the same scope can share an entrypoint file. They keep independent options (env, watch mode, failure policy).

Server variables

  • $_SERVER['FRANKENPHP_WORKER'] carries the resolved worker name (was previously "1"; pre-existing user code that only tests isset(...) keeps working). Catch-all instances see the name they were started under.
  • $_SERVER['FRANKENPHP_WORKER_BACKGROUND'] = true for bg workers — single-key branch for "am I a bg worker?".

Readiness

One readiness channel per worker instance, closed exactly once on the first frankenphp_get_worker_handle() call. ensure() selects on that channel against an abort channel (populated when the worker exhausts max_consecutive_failures during boot) and the per-call deadline. Crash-restarts don't re-arm the signal — a worker that announced "ready" once stays ready for any future ensure() caller, which is the right semantics for a long-lived dependency.

Pre-readiness crashes capture metadata per attempt (entrypoint, exit status, attempt count) on the same state slot, so a timing-out ensure() surfaces a self-teaching diagnostic ("x did not become ready within Xs; last attempt N failed (exit status M, entrypoint …)") instead of just "did not call frankenphp_get_worker_handle()". The follow-up PR adds PG(last_error_message) capture to that record once the C-side helper lands.

For catch-all workers each lazy-spawned name has its own readiness slot, so a stuck foo doesn't keep ensure('bar') waiting; for named pools (num > 1) the threads share one slot and the first to reach frankenphp_get_worker_handle() wins.

Lifecycle

  • Crash recovery: quadratic backoff capped at 1s; max_consecutive_failures aborts startup if hit during the boot phase.
  • Graceful shutdown: stop pipe closes (EOF), workers exit cleanly. After a 30s grace period, a best-effort force-kill drill (per feat: cross-platform force-kill primitive for stuck PHP threads #2365) interrupts threads stuck in blocking syscalls.

What's deferred

A follow-up PR adds:

  • frankenphp_set_vars(array $vars): void — publish persistent vars from a bg worker.
  • frankenphp_get_vars(string $name): array — pure read, with generational cache so repeated calls within a request return the same array instance (=== is O(1)).
  • A frankenphp_set_vars-driven readiness signal that lets ensure() block until a worker has bootstrapped (turning fire-and-forget into a stronger contract without an API change).
  • The full docs/background-workers.md reference.

That split keeps the surfaces independent: this PR is the lifecycle/wiring; the follow-up is the data plane.

Tests

End-to-end tests use file sentinels (workers touch a path provided via env) instead of cross-thread observation, since this PR has no shared-state API yet:

  • TestBackgroundWorkerLifecycle / TestBackgroundWorkerCrashRestarts / TestBackgroundWorkerWithoutHTTP
  • TestBackgroundWorkerRestartForceKillsStuckThread (force-kill drill on a bg worker stuck in sleep(60))
  • TestEnsureBackgroundWorkerNamedLazy / TestEnsureBackgroundWorkerCatchAll / TestEnsureBackgroundWorkerCatchAllCap / TestEnsureBackgroundWorkerUndeclared
  • TestNextBackgroundWorkerScopeIsDistinct / TestBackgroundWorkerSameNameDifferentScope / TestBackgroundWorkerCatchAllPerScope
  • TestBackgroundWorkerPool / TestBackgroundWorkerMultiEntrypoint
  • TestEnsureBackgroundWorkerBatch (and the three validation-error variants)
  • TestBackgroundWorkerBgFlag

Test plan

  • go test ./... clean
  • Sanitizers (asan, msan) clean
  • Manual: declare a named bg worker, observe its sentinel; restart it via RestartWorkers(), observe re-spawn
  • Manual: declare two php_server blocks with same-named bg workers, verify they don't collide

nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 4, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 4, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
@nicolas-grekas nicolas-grekas force-pushed the sidekicks-config branch 2 times, most recently from dfc0a26 to 2632517 Compare May 4, 2026 18:39
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 4, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
@dunglas dunglas requested a review from Copilot May 5, 2026 04:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a first-cut “background worker” subsystem to FrankenPHP, including Go/C/PHP APIs and Caddyfile configuration, to run long-lived non-HTTP PHP scripts with scoped name resolution and lazy start via ensure().

Changes:

  • Introduces background-worker declarations (Caddy + Go options) with per-php_server scoping and lazy-start support (frankenphp_ensure_background_worker).
  • Adds a background worker thread handler with drain signaling via a stop-pipe exposed to PHP (frankenphp_get_worker_handle) and crash-restart backoff.
  • Adds end-to-end tests and PHP fixtures validating lifecycle, crash restart, scoping, pools, batch ensure validation, and $_SERVER bg flag injection.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
worker.go Tracks background-worker metadata on workers; builds per-scope lookups; starts bg threads with a dedicated handler.
threadbackgroundworker.go New thread handler implementing background worker lifecycle, drain behavior, and restart/backoff.
background_worker.go New background-worker registry/lookup system, scoping, and ensure() lazy-start implementation.
frankenphp.c Adds TLS state for bg workers, stop-pipe primitives, PHP functions frankenphp_ensure_background_worker and frankenphp_get_worker_handle, and injects $_SERVER flags.
frankenphp.h Exposes C primitives for bg worker name + stop-pipe operations.
frankenphp.go Reserves thread budget for background workers and resets bg lookup globals on shutdown.
options.go Adds Go WorkerOptions WithWorkerBackground and WithWorkerBackgroundScope.
requestoptions.go Adds RequestOption WithRequestBackgroundScope to scope ensure() resolution per request.
context.go Stores the request’s background scope in frankenPHPContext.
phpthread.go Invokes handler drain() during shutdown to wake bg workers blocked in C calls.
frankenphp.stub.php Adds PHP stubs/docs for frankenphp_ensure_background_worker and frankenphp_get_worker_handle.
frankenphp_arginfo.h Adds arginfo for new PHP functions (but currently with a placeholder stub hash).
caddy/workerconfig.go Adds background worker subdirective; rejects unsupported directives (e.g., match) for bg workers.
caddy/module.go Assigns a unique background-worker scope per php_server and tags requests/workers with it.
caddy/app.go Wires Caddy config to Go via WithWorkerBackground().
background_worker_test.go Integration tests for bg worker lifecycle, crash restart, and ensuring bg workers don’t intercept HTTP.
background_worker_scope_test.go Tests scope isolation and catch-all behavior per scope.
background_worker_pool_test.go Tests named pool workers and multi-entrypoint support.
background_worker_internal_test.go Adds force-kill integration test, but it is currently unconditionally skipped.
background_worker_ensure_test.go Tests ensure() for named lazy workers, catch-all, caps, and undeclared errors.
background_worker_batch_test.go Tests array-form ensure() and its validation error cases, plus bg flag injection.
testdata/background-worker.php Fixture: long-lived bg worker that touches a sentinel and blocks on stop pipe.
testdata/background-worker-stuck.php Fixture: intentionally stuck worker (sleep) for force-kill testing.
testdata/background-worker-pool.php Fixture: pool worker writes unique sentinels per thread then blocks.
testdata/background-worker-named.php Fixture: writes per-name sentinel based on FRANKENPHP_WORKER_NAME.
testdata/background-worker-crash.php Fixture: crash-once-then-succeed to validate crash-restart behavior.
testdata/background-worker-bg-flag.php Fixture: writes the exact PHP value of FRANKENPHP_WORKER_BACKGROUND.
testdata/background-worker-batch-errors.php HTTP fixture exercising batch ensure() validation paths.
testdata/background-worker-batch-ensure.php HTTP fixture ensuring multiple workers in a single batch call.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread frankenphp.c
Comment thread frankenphp.c Outdated
Comment thread background_worker.go Outdated
Comment thread background_worker.go Outdated
Comment thread threadbackgroundworker.go Outdated
Comment thread frankenphp_arginfo.h Outdated
Comment thread background_worker_internal_test.go Outdated
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
@nicolas-grekas nicolas-grekas force-pushed the sidekicks-config branch 2 times, most recently from 98e9ae9 to 7c9379b Compare May 5, 2026 07:35
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
Comment thread background_worker.go Outdated
Comment thread bgworker.go Outdated
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
Copy link
Copy Markdown
Member

@dunglas dunglas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First batch. I didn't finish the review yet.

Comment thread bgworker.go
Comment thread background_worker.go Outdated
Comment thread bgworker.go Outdated
Comment thread caddy/workerconfig.go Outdated
Comment thread context.go Outdated
Comment thread frankenphp.c Outdated
Comment thread background_worker_batch_test.go Outdated
Comment thread background_worker_batch_test.go Outdated
Comment thread background_worker_batch_test.go Outdated
Comment thread background_worker_batch_test.go Outdated
@nicolas-grekas nicolas-grekas force-pushed the sidekicks-config branch 2 times, most recently from 0491b19 to 38f008e Compare May 5, 2026 17:31
Comment thread frankenphp.c
Comment thread frankenphp.go
Comment on lines +174 to +187
extra := w.num
if extra < 1 {
extra = 1
}
if opt.workers[i].maxThreads > extra {
extra = opt.workers[i].maxThreads
}
reservedThreads += extra
// Register the expected worker count for metrics too: without
// this, a bg-worker-only deployment never initialises
// totalWorkers, and StartWorker calls become silent no-ops.
metrics.TotalWorkers(w.name, extra)
continue
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of the reservedThreads logic, can't you just do something like this instead?

Suggested change
extra := w.num
if extra < 1 {
extra = 1
}
if opt.workers[i].maxThreads > extra {
extra = opt.workers[i].maxThreads
}
reservedThreads += extra
// Register the expected worker count for metrics too: without
// this, a bg-worker-only deployment never initialises
// totalWorkers, and StartWorker calls become silent no-ops.
metrics.TotalWorkers(w.name, extra)
continue
}
if w.num == 0 {
opt.workers[i].num = 1
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that'd break the design of num 0 meaning "lazy start" IIUC.

Comment thread worker.go Outdated
Comment thread bgworker.go Outdated
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
Introduce background workers via WithWorkerBackground() (Go API) and the
Caddyfile `background` token on workers. Background workers share the
PHP runtime with HTTP threads but don't serve HTTP requests. They expose
a stop pipe (frankenphp_get_worker_handle()) so PHP scripts can park on
stream_select and exit gracefully when FrankenPHP drains. The handler
auto-restarts the worker on crash with quadratic backoff capped at 1s.

The bg worker name is global in this commit; follow-ups will add
ensure(), per-php_server scoping, pools, and shared-state APIs
(set_vars/get_vars).
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 5, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
@nicolas-grekas
Copy link
Copy Markdown
Contributor Author

nicolas-grekas commented May 6, 2026

FTR I made two more changes:

  • the "ready" state is now reached only when a bg worker calls get_worker_handle(). In the final step, this will require calling set_vars() in addition
  • I made the $timeout argument of the ensure() function default to null, so that we don't hardcode any defaults in the signature of the function. This opens making this default configurable in the future (either php.ini, or caddyfile or etc.)

Adds frankenphp_ensure_background_worker(string $name): void on top of the
minimal background worker from the previous commit. Fire-and-forget: the
function lazy-starts the named worker if it is not already running and
returns once a thread has been launched, without waiting for the PHP
script to reach any particular state (no readiness signal exists in this
build; that arrives with the set_vars/get_vars step that follows).

Registry + lookup layer:
- backgroundWorkerRegistry tracks the template options (env, watch,
  maxConsecutiveFailures, requestOptions) from one declaration plus the
  live worker instances spawned from it. Catch-all registries carry a
  maxWorkers cap.
- backgroundWorkerLookup holds a name->registry map plus a single catch-
  all slot. resolve() falls back to catch-all when the name is not
  declared.

Catch-all dispatch:
- A name-less background-worker declaration matches any ensure() name at
  runtime. max_threads on a catch-all is the cap on how many distinct
  lazy-started instance names it can host (default 16). Caddyfile no
  longer requires "name" on background workers, and accepts max_threads
  > 1 on the catch-all (still rejected on named bg workers).

Named lazy path:
- A num=0 named declaration registers the worker struct at init but
  defers thread attach until ensure() schedules it. ensure() reuses the
  existing struct via workersByName instead of creating a duplicate.

calculateMaxThreads now reserves per-bg-worker thread budget separately
from HTTP-worker counts and scales catch-all reservations with the
declared max_threads, so lazy starts always have a slot to schedule
into. metrics.TotalWorkers is registered for bg workers so StartWorker
calls in the bg-worker thread aren't silent no-ops in bg-only deployments.

$_SERVER['FRANKENPHP_WORKER_NAME'] is now populated for background
workers so catch-all instances can tell which name they were started
under (lets sentinel-based tests distinguish job-a from job-b).

Tests (background_worker_ensure_test.go) cover:
- ensure() on a declared num=0 named worker lazy-starts it
- ensure() on a name matched by catch-all spawns from the catch-all
  template; two distinct names produce two independent instances
- ensure() with no catch-all and an undeclared name returns the config
  error
- catch-all max_threads cap rejects the (cap+1)th distinct name
Adds a BackgroundScope opaque type (int under the hood; obtain values
via NextBackgroundWorkerScope) so each php_server block gets its own
isolation boundary for background workers. Zero is the global/embed
scope.

- backgroundLookups map[BackgroundScope]*backgroundWorkerLookup
  replaces the single global backgroundLookup. Each scope has its own
  named registry + catch-all so two blocks can declare bg workers with
  the same user-facing name without colliding.

- buildBackgroundWorkerLookups iterates declarations into their scope's
  lookup; each declaration still owns its own registry. registry.declared
  remembers the *worker for a named declaration so lazy-start (num=0)
  reuses it without scanning the global workersByName map (which is not
  scope-aware for bg workers).

- getLookup(thread) resolves the active scope from the calling thread:
  worker handler -> request context -> global (0). Scopes that declared
  their own workers stay strictly isolated; an empty scope falls through
  to the global lookup so embed-mode workers stay reachable.

- Go options: WithWorkerBackgroundScope tags a declaration; the new
  WithRequestBackgroundScope tags a request so ensure() from a regular
  HTTP request resolves to the right block's lookup.

- Caddy wiring: FrankenPHPModule.Provision allocates one scope per
  module instance (idempotent across re-provisions) and threads it into
  worker declarations and ServeHTTP.

- workersByName collision check now skips bg workers; they resolve via
  their scope's lookup, so the same PHP-visible name can appear in two
  scopes without tripping the duplicate guard.

- C side: go_frankenphp_ensure_background_worker now takes the calling
  thread index so getLookup can resolve the scope from the active
  handler / request context.

Tests:

- TestNextBackgroundWorkerScopeIsDistinct: counter hands out unique
  non-zero scopes.
- TestBackgroundWorkerSameNameDifferentScope: two named bg workers with
  the same user-facing name in distinct scopes both Init successfully
  and own distinct registries.
- TestBackgroundWorkerCatchAllPerScope: ensure() in scope A consumes
  scope A's catch-all only; scope B's catch-all stays empty. Verified
  by inspecting the per-scope lookup and the live workers slice via
  package-internal access.

Deferred to follow-ups: pools (num > 1 per named worker, max_threads > 1
for named workers), multiple declarations sharing one entrypoint file
in one scope, FRANKENPHP_WORKER_BACKGROUND server flag, batch ensure.
Two small, related polish steps on the bg-worker surface, landing
together:

- frankenphp_ensure_background_worker now accepts string|array. The
  array form lazy-starts every named worker fire-and-forget, with the
  same semantics as the single-string call repeated N times. Input is
  validated up-front: empty arrays raise ValueError, non-string
  elements raise TypeError, empty-string and duplicate names raise
  ValueError. Validation happens before any worker is started so a bad
  input never leaves a half-spawned batch behind.

- $_SERVER['FRANKENPHP_WORKER_BACKGROUND'] = true in background worker
  scripts, alongside the existing FRANKENPHP_WORKER_NAME wiring. Gives
  scripts a single-key branch for "am I a bg worker?" without having
  to probe other frankenphp_* helpers. Set unconditionally for bg
  workers (catch-all instances with no declared name still see the
  flag, just no name).

- TestEnsureBackgroundWorkerBatch: ensure(['a','b','c']) starts three
  catch-all-resolved instances; assert three per-name sentinels appear.
- TestEnsureBackgroundWorkerBatchEmpty: [] raises ValueError. Driven
  through a PHP fixture that catches the throwable since the
  validation lives in the Zend parameter-parsing path.
- TestEnsureBackgroundWorkerBatchNonString: ['ok-name', 42] raises
  TypeError, same fixture pattern.
- TestEnsureBackgroundWorkerBatchDuplicate: ['dup','dup'] raises
  ValueError (duplicate names rejected, not silently deduped).
- TestBackgroundWorkerBgFlag: bg worker writes var_export() of
  $_SERVER['FRANKENPHP_WORKER_BACKGROUND'] to a sentinel; assert the
  exact value is the bool true.
Drops backgroundWorkerRegistry and its registry.workers/registry.declared
indirection; the per-scope lookup now resolves directly to the *worker
that owns each name, and the catch-all *worker hosts all of its
lazy-spawned threads in catchAllNames keyed by the runtime name.

worker struct gains:
- catchAllCap / catchAllMu / catchAllNames for the catch-all template
  (was registry.maxWorkers / registry.mu / registry.workers);
- bgLazyStartMu / bgLazyStarted to gate the first-thread spawn for
  num=0 named declarations.

backgroundWorkerThread gains runtimeName + backgroundReady so a single
catch-all *worker can host multiple per-name threads, each carrying its
own metric label, $_SERVER['FRANKENPHP_WORKER_NAME'] and readiness slot.
setupScript trims any "m#" prefix at the PHP boundary, so module-worker
metric labels stay m#-prefixed (unchanged) while PHP sees the user-facing
name.

ensure() picks named via lookup.byName (lazy-starting the first thread
under bgLazyStartMu when num=0) or catch-all via lookup.catchAll, locks
catchAllMu across the cap check + thread reservation + entry publication
so concurrent callers can't observe a phantom registration on a failed
allocation.

User-visible behaviour preserved: per-scope isolation, per-name $_SERVER
worker name, per-name metric buckets, max_threads cap on catch-alls,
named pool semantics, multi-entrypoint declarations.
Surfaces FRANKENPHP_WORKER_NAME and FRANKENPHP_WORKER_BACKGROUND in
$_SERVER through the same cgi pipeline as every other CGI variable
(go_register_server_variables -> frankenphp_register_server_vars),
instead of an after-the-fact injection block in the bg-worker main loop.

frankenphp_server_vars gains worker_name / worker_name_len /
is_background_worker. cgi.go populates them from fc.worker (with the
"m#" module prefix already stripped on the Go side). The
frankenphp_register_server_vars function emits the two keys when
populated.

Side effects:

- HTTP workers now also expose FRANKENPHP_WORKER_NAME, matching
  AlliBalliBaba's review note: "HTTP workers probably would also
  benefit from exposing the worker name".
- The worker_name + worker_name_len TLS slots are gone; data flows
  request-by-request through the struct.
- frankenphp_set_worker_name(name, len, bool) collapses to
  frankenphp_set_background_worker(bool) since the name no longer needs
  to cross the FFI for that side-effect (it just toggles the
  is_background_worker TLS, disarms max_execution_time, and arms the
  stop pipe).
- The main-loop injection block in the worker thread is dropped; only
  the zend_unset_timeout() call survives (still needed to disarm the
  timer that php_request_startup re-arms on each iteration).

Behaviour preserved on every prior axis: bg workers see
FRANKENPHP_WORKER_BACKGROUND === true and FRANKENPHP_WORKER_NAME, named
HTTP workers see FRANKENPHP_WORKER_NAME, non-worker requests see neither.
The bg-worker subsystem had heavier doc/inline comments than the rest of
the codebase. Trims them to match the surrounding terseness (typically
1-3 lines, godoc on exported only when non-obvious): drops paragraph-
length expansions, redundant restatements of what the code does, and
context that's already evident from the function signature / call site.
Net: 120 fewer comment lines, no semantic changes.
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 6, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
Comment thread bgworkerbatch_test.go
Comment thread background_worker_ensure_test.go Outdated
Comment thread background_worker_ensure_test.go Outdated
Comment thread background_worker_ensure_test.go Outdated
Comment thread bgworkerensure_test.go
Comment thread bgworker.go Outdated
Comment thread caddy/module.go Outdated
Comment thread frankenphp.c Outdated
Comment thread frankenphp.stub.php Outdated
Comment thread frankenphp.stub.php Outdated
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 6, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
- BackgroundScope -> Scope (and NextScope / WithRequestScope /
  WithWorkerScope), keeping the type opaque + reusable for other
  per-server isolation contexts (Mercure hubs, future Prometheus
  labels). No behaviour change.
- $_SERVER['FRANKENPHP_WORKER'] now carries the user-facing worker
  name instead of "1". The doc has always told callers to rely on
  presence, not value. Drops the parallel FRANKENPHP_WORKER_NAME
  injection.
- backgroundWorkerReady.abortErr keeps the full error (not just its
  message), so the wrap chain survives into ensure() callers.
- Cap-error wording: drop the Caddyfile-specific "max_threads on the
  catch-all" phrasing for "increase max threads or declare it as a
  named worker", to read cleanly when the user is on the JSON config.
- Constants merged into a single block; getLookup folds the worker /
  background-worker handler cases via a small anonymous interface
  (Go's type switch can't fan out type-asserted access otherwise).
- Stub doc nits: third-person godoc ("Declares" / "Returns").
- Test ergonomics: requireSentinelEventually wraps the duplicated
  Eventually glue; require.NoFileExists, require.ErrorContains,
  assert.WithinDuration replace ad-hoc combinations.
- Renamed background_worker_*_test.go to bgworker*_test.go to match
  the bgworker.go source file (no underscores in file-name prefixes).
nicolas-grekas added a commit to nicolas-grekas/frankenphp that referenced this pull request May 6, 2026
Adds the worker-to-HTTP shared-state surface deferred from the
config+ensure split (php#2393):

- frankenphp_set_vars(array $vars): void publishes a snapshot from a
  background worker. Persistent (pemalloc) memory, RWMutex-protected,
  cross-thread safe. Skips work when data is identical (=== check).
- frankenphp_get_vars(string $name): array reads the latest snapshot.
  Pure read; throws if the worker is not running or has not published
  yet.
- ensure_background_worker now blocks until the named worker has called
  set_vars at least once (the readiness signal). The fire-and-forget
  semantics from the config-only PR become a stronger contract here
  with no API change visible to callers.
- Two-mode ensure: fail-fast in HTTP-worker bootstrap (before
  frankenphp_handle_request) so a broken dependency surfaces at boot
  rather than serving degraded traffic; tolerant inside requests so
  the restart-with-backoff cycle can recover from transient boot
  failures.
- Boot-failure capture: the worker's last PHP error (message, file,
  line, exit status) is recorded so ensure() can throw a descriptive
  RuntimeException on timeout.

The persistent storage path uses opcache-immutable arrays (zero-copy
share), interned strings (no copy), and rich type support: null,
scalars, arrays (nested), enums.

Tests cover happy-path roundtrips, type coverage, ensure() blocking
on first set_vars, fail-fast vs tolerant modes, boot-failure
reporting, and the catch-all + scope interactions with vars.
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.

5 participants