feat: background workers (config + ensure)#2393
feat: background workers (config + ensure)#2393nicolas-grekas wants to merge 1 commit intophp:mainfrom
Conversation
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.
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.
dfc0a26 to
2632517
Compare
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.
There was a problem hiding this comment.
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_serverscoping 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
$_SERVERbg 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.
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.
2632517 to
f6b0445
Compare
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.
98e9ae9 to
7c9379b
Compare
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.
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.
7c9379b to
81cf9d0
Compare
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.
81cf9d0 to
5fadf66
Compare
dunglas
left a comment
There was a problem hiding this comment.
First batch. I didn't finish the review yet.
0491b19 to
38f008e
Compare
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.
38f008e to
19a5489
Compare
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.
7567b02 to
67a1a32
Compare
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.
67a1a32 to
d88badf
Compare
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.
d74a07c to
463815d
Compare
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.
463815d to
f3a86b7
Compare
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.
f3a86b7 to
d6d244d
Compare
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.
d6d244d to
c7cf7a2
Compare
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.
c7cf7a2 to
29e7c8e
Compare
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.
29e7c8e to
d91fcaf
Compare
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.
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.
|
I merged the metrics concern into this PR since it's only for bgworkers. Here is the description I added above: Background workers report under a prefixed Prometheus worker label so two Format: HTTP-worker metric labels are unchanged. |
|
Yes, I like that design! Let me know when this is ready for review. |
Adds background workers — long-lived non-HTTP PHP scripts that share
the FrankenPHP runtime with HTTP workers but stay outside the request
cycle. Implements:
- WithWorkerBackground option / Caddyfile `worker { background }`
declares a worker as a bg worker. $_SERVER['FRANKENPHP_WORKER']
carries the user-facing worker name, $_SERVER['FRANKENPHP_WORKER_BACKGROUND']
is true for bg workers so scripts can branch without checking every
function independently.
- frankenphp_ensure_background_worker(string|array $name, ?float $timeout)
lazy-starts a named bg worker (num=0 declarations stay parked until
ensure() is called) or matches a catch-all declaration to spawn a
named instance. Accepts an array of names sharing one deadline.
Two-mode: fail-fast in HTTP-worker bootstrap so a broken dependency
surfaces at boot; tolerant inside requests so the restart cycle can
recover from transient boot failures.
- Per-php_server scope isolation: each php_server block gets its own
Scope (opaque uint64). Workers in distinct scopes can share a name
without colliding. The Caddy module resolves a human-friendly label
via cascade (route host matcher -> user-set Caddy server name ->
first listener address) and registers it via SetScopeLabel so future
metric/log emitters can render server="api.example.com".
- Catch-all dispatch: a name-less bg worker declaration matches any
ensure() name at runtime. max_threads on a catch-all caps how many
distinct lazy-started instance names it can host (default 16).
- bg-worker bootstrap routes the runtime name through the CGI pipeline
so $_SERVER['FRANKENPHP_WORKER'] reflects the user-facing name on
every request, not just bg-worker boot.
- Bg workers expose a stop pipe (frankenphp_get_worker_handle) so PHP
scripts can park on stream_select and exit gracefully when
FrankenPHP drains.
- max_consecutive_failures cap fails Init fast (HTTP-worker mode) or
shuts the bg-worker thread down cleanly, with a deterministic abort
message instead of a generic ensure() timeout.
Tests cover: Caddyfile + Go-API declarations, ensure() lazy-start,
catch-all dispatch + cap, batch ensure with shared deadline, error
paths (undeclared name, boot failure metadata, type-validation), per-
php_server scope isolation including same-named workers in distinct
scopes.
| if worker.maxConsecutiveFailures >= 0 && handler.failureCount >= worker.maxConsecutiveFailures { | ||
| isSingleInstance := worker.bg.catchAllNames != nil || worker.num == 0 | ||
| if isSingleInstance && handler.backgroundReady != nil { | ||
| handler.backgroundReady.abort(fmt.Errorf("background worker %s exceeded max_consecutive_failures (%d, last exit status %d)", worker.fileName, worker.maxConsecutiveFailures, exitStatus)) | ||
| worker.invalidateBackgroundEntry(runtimeName) | ||
| } | ||
| if startupFailChan != nil && !watcherIsEnabled { | ||
| startupFailChan <- fmt.Errorf("too many consecutive failures: background worker %s keeps crashing", worker.fileName) | ||
| handler.thread.state.Set(state.ShuttingDown) | ||
| return | ||
| } | ||
| if globalLogger.Enabled(globalCtx, slog.LevelWarn) { | ||
| globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "background worker exceeded max_consecutive_failures, stopping respawn", slog.String("worker", runtimeName), slog.Int("thread", handler.thread.threadIndex), slog.Int("failures", handler.failureCount)) | ||
| } | ||
| handler.thread.state.Set(state.ShuttingDown) | ||
| return | ||
| } |
There was a problem hiding this comment.
Not sure I like this flow. If a background worker enters a 'failed state' and is eager started, we should just fail on the startup channel.
The issue with lazily started threads is that there's not really a good way to handle a failure state, the server is just broken in this case. You should either panic or retry endlessly, shutting down the thread doesn't really solve anything.
There was a problem hiding this comment.
Not having a clear point to catch a failures is by design, which is also why I still dislike the lazy starts in general.
Best thing to do here is just repeatedly notifying the user in the logs that the background worker failed.
| drain() | ||
| // scopedWorker returns the *worker this handler runs (HTTP / bg | ||
| // workers), or nil for non-worker handlers. | ||
| scopedWorker() *worker |
There was a problem hiding this comment.
Maybe better to just get the scope directly:
| scopedWorker() *worker | |
| scope() scope |
| var workerName string | ||
| var isBackgroundWorker bool | ||
| if bgHandler, ok := thread.handler.(*backgroundWorkerThread); ok { | ||
| workerName = strings.TrimPrefix(bgHandler.runtimeName, "m#") | ||
| isBackgroundWorker = true | ||
| } else if fc.worker != nil { | ||
| workerName = strings.TrimPrefix(fc.worker.name, "m#") | ||
| isBackgroundWorker = fc.worker.bg != nil | ||
| } |
There was a problem hiding this comment.
Don't you just need the second part of that if-statement? fc should always be assigned to the worker.
| var workerName string | |
| var isBackgroundWorker bool | |
| if bgHandler, ok := thread.handler.(*backgroundWorkerThread); ok { | |
| workerName = strings.TrimPrefix(bgHandler.runtimeName, "m#") | |
| isBackgroundWorker = true | |
| } else if fc.worker != nil { | |
| workerName = strings.TrimPrefix(fc.worker.name, "m#") | |
| isBackgroundWorker = fc.worker.bg != nil | |
| } | |
| var workerName string | |
| var isBackgroundWorker bool | |
| fc.worker != nil { | |
| workerName = strings.TrimPrefix(fc.worker.name, "m#") | |
| isBackgroundWorker = fc.worker.bg != nil | |
| } |
| type backgroundWorkerExtras struct { | ||
| // ready is shared by named workers and a catch-all's eager pool; | ||
| // lazy-spawned catch-all instances each get their own slot in | ||
| // catchAllNames. | ||
| ready *backgroundWorkerState | ||
|
|
||
| // catchAllNames != nil marks this *worker as a scope's catch-all | ||
| // template. Lazy-spawned threads register here, up to catchAllCap. | ||
| catchAllCap int | ||
| catchAllMu sync.Mutex | ||
| catchAllNames map[string]*backgroundWorkerState | ||
|
|
||
| // lazyMu/lazyStarted gate the first thread spawn for a num=0 named | ||
| // bg worker. Unused for eager (num > 0) or catch-all templates. | ||
| lazyMu sync.Mutex | ||
| lazyStarted bool | ||
| } | ||
|
|
There was a problem hiding this comment.
Couldn't you just put all of this into the backgroundWorkerState?
|
Still feels a bit too complex, will have to do more reviewing later. |
|
Just to clarify: something like this is still how you envision the end result with background workers? # http-worker.php
frankenphp_ensure_background_worker(['something', 'something else']);
frankenphp_handle_request(function(){
$vars = frankenphp_get_vars('something')
....
})# bg-worker.php
$whatToDo = $_SERVER['FRANKENPHP_WORKER_NAME');
switch($whatToDo){
case 'somehting';
# ...
frankenphp_set_vars([...]);
case 'something else':
# ...
frankenphp_set_vars([...]);
}
$handle = frankenphp_get_handle();
# put the handle into an async event loop react/amp |
|
@AlliBalliBaba correct! That being said: I agree the PR is complex. I'm going to submit one with lazy-start removed and declared-only workers. This means no I'll still want to discuss the other steps to eventually reach the state in sidekicks/#2287 - feature/DX-wise. But we'll also have more steps to discuss as progress is made. |
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_serverscoping, named pools, multi-entrypoint, plus a$_SERVERflag 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 callsfrankenphp_get_worker_handle()(the readiness signal) or the timeout fires.null(the default) falls back to FrankenPHP's internal default deadline; a value<= 0raisesValueError. 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 (ValueErrorfor empty array / empty string / duplicate names / non-positive timeout;TypeErrorfor 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 reachedfrankenphp_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 onstream_select; FrankenPHP closes the write end during drain soselectwakes with EOF. Calling this also signals toensure()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 tofrankenphp_set_vars()— sameensure()signature, progressively stronger guarantee, no caller-visible API change.In CLI mode these functions aren't exposed.
Caddyfile
backgroundmarks a worker as non-HTTP.namepins an exact worker name; declarations withoutnameare catch-alls for lazy-started instances.numon a named bg worker eagerly starts that many instances;num 0(or omitted) defers start untilensure().max_threadson a catch-all caps how many distinct lazy-started instances it can host.max_consecutive_failuresdefaults to 6 (same as HTTP workers).max_execution_timeis 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 soensure()from a regular HTTP request resolves to the right block's lookup.NextScope()hands out a freshScopevalue (opaqueuint64under 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_serverscopingEach
php_serverblock 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).Metrics
Background workers report under a prefixed Prometheus
workerlabel so twophp_serverblocks declaring same-named bg workers stay on distinct series:Format:
m#<scope-label>:<worker-name>.<scope-label>resolves per-php_serverblock via a cascade (first host of the route's host matcher → first listener address). Catch-all instances substitute the<worker-name>half with the name passed toensure(). Embed mode (no Caddy module) leaves the label empty:m#:job-runner— uniform regexm#([^:]*):(.+)parses both. Them#prefix matches the existing module-worker convention.HTTP-worker metric labels are unchanged.
Pools and multi-entrypoint
num > 1on a named bg worker spawns N threads sharing the same name. Each thread has its own stop pipe so drain can wake them independently.Server variables
$_SERVER['FRANKENPHP_WORKER']carries the resolved worker name (was previously"1"; pre-existing user code that only testsisset(...)keeps working). Catch-all instances see the name they were started under.$_SERVER['FRANKENPHP_WORKER_BACKGROUND'] = truefor 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 exhaustsmax_consecutive_failuresduring boot) and the per-call deadline. Crash-restarts don't re-arm the signal — a worker that announced "ready" once stays ready for any futureensure()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 ("xdid not become ready within Xs; last attempt N failed (exit status M, entrypoint …)") instead of just "did not callfrankenphp_get_worker_handle()". The follow-up PR addsPG(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
foodoesn't keepensure('bar')waiting; for named pools (num > 1) the threads share one slot and the first to reachfrankenphp_get_worker_handle()wins.Lifecycle
max_consecutive_failuresaborts startup if hit during the boot phase.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)).frankenphp_set_vars-driven readiness signal that letsensure()block until a worker has bootstrapped (turning fire-and-forget into a stronger contract without an API change).docs/background-workers.mdreference.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
toucha path provided via env) instead of cross-thread observation, since this PR has no shared-state API yet:TestBackgroundWorkerLifecycle/TestBackgroundWorkerCrashRestarts/TestBackgroundWorkerWithoutHTTPTestBackgroundWorkerRestartForceKillsStuckThread(force-kill drill on a bg worker stuck insleep(60))TestEnsureBackgroundWorkerNamedLazy/TestEnsureBackgroundWorkerCatchAll/TestEnsureBackgroundWorkerCatchAllCap/TestEnsureBackgroundWorkerUndeclaredTestNextBackgroundWorkerScopeIsDistinct/TestBackgroundWorkerSameNameDifferentScope/TestBackgroundWorkerCatchAllPerScopeTestBackgroundWorkerPool/TestBackgroundWorkerMultiEntrypointTestEnsureBackgroundWorkerBatch(and the three validation-error variants)TestBackgroundWorkerBgFlagTest plan
go test ./...cleanRestartWorkers(), observe re-spawnphp_serverblocks with same-named bg workers, verify they don't collide