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.
b3c1aac to
1cd1ad6
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.
b7ca41b to
9fd6a5f
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.
9fd6a5f to
0368ebf
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.
0368ebf to
7567b02
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.
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.
|
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. |
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