feat: application sidekicks = non-HTTP workers with shared state#2287
feat: application sidekicks = non-HTTP workers with shared state#2287nicolas-grekas wants to merge 1 commit intophp:mainfrom
Conversation
e1655ab to
867e9b3
Compare
|
Interesting approach to parallelism, what would be a concrete use case for only letting information flow one way from the sidekick to the http workers? Usually the flow would be inverted, where a http worker offloads work to a pool of 'sidekick' workers and can optionally wait for a task to complete. |
da54ab8 to
a06ba36
Compare
|
Thank you for the contribution. Interesting idea, but I'm thinking we should merge the approach with #1883. The kind of worker is the same, how they are started is but a detail. @nicolas-grekas the Caddyfile setting should likely be per |
ad71bfe to
05e9702
Compare
|
@AlliBalliBaba The use case isn't task offloading (HTTP->worker), but out-of-band reconfigurability (environment->worker->HTTP). Sidekicks observe external systems (Redis Sentinel failover, secret rotation, feature flag changes, etc.) and publish updated configuration that HTTP workers pick up on their next request; with per-request consistency guaranteed via Task offloading (what you describe) is a valid and complementary pattern, but it solves a different problem. The non-HTTP worker foundation here could support both. @henderkes Agreed that the underlying non-HTTP worker type overlaps with #1883. The foundation (skip HTTP startup/shutdown, immediate readiness, cooperative shutdown) is the same. The difference is the API layer and the DX goals:
Happy to follow up with your proposals now that this is hopefully clarified. |
05e9702 to
8a56d4c
Compare
|
Great PR! Couldn't we create a single API that covers both use case? We try to keep the number of public symbols and config option as small as possible! |
Yes, that's why I'd like to unify the two API's and background implementations into one. Unfortunately the first task worker attempt didn't make it into |
|
The PHP-side API has been significantly reworked since the initial iteration: I replaced The old design used
Key improvements:
Other changes:
|
cb65f46 to
4dda455
Compare
|
Thanks @dunglas and @henderkes for the feedback. I share the goal of keeping the API surface minimal. Thinking about it more, the current API is actually quite small and already general:
The name "sidekick" works as a generic concept: a helper running alongside. The current set_vars/get_vars protocol covers the config-publishing use case. For task offloading (HTTP->worker) later, the same sidekick infrastructure could support:
Same worker type, same So the path would be:
The foundation (non-HTTP threads, cooperative shutdown, crash recovery, per-php_server scoping) is shared. Only the communication primitives differ. WDYT? |
b3734f5 to
ed79f46
Compare
|
|
|
Hmm, it seems they are on some versions, for example here: https://github.com/php/frankenphp/actions/runs/23192689128/job/67392820942?pr=2287#step:10:3614 For the cache, I'm not aware of a Github feature that allow to clear everything unfortunately 🙁 |
fe77b5a to
7556610
Compare
My only worry with this is that "sidekick" implies that there's a "main" character related to it. That's the case here, but wouldn't necessarily be the case for task- or extension workers. Other than the naming, I don't object the api. |
b1da00b to
b7e395e
Compare
b7e395e to
c50cf08
Compare
|
If we want to unify these concepts, the sidekick workers should probably be configured like regular workers and started always from the Caddy config (very messy to let http workers start and stop them). Just a The |
Absolute agree, though I believe it would be good to be able to start them from http code and establish communication through channels of sort.
Funny you say that, I created one over the last few days, but I quickly figured that the Cgo overhead and Go generally being much slower than optimised C made this a bit of a futile attempt. Ristretto backend was ~4x slower than apcu/direct C copying. If we want to implement a FrankenPHP\Store it will have to be well-thought out and implemented in pure C. |
|
All green, ready on my side!🎳 @henderkes Thanks for validating the CGo overhead concern. A proper Glad we agree on starting from PHP! That's the current design: get_vars implicitly starts the sidekick on first call. @AlliBalliBaba About APCu: I want sidekicks to be a core feature of FrankenPHP that people can reliably build on, not something depending on an optional third-party extension with its own bugs and serialization overhead. The sidekick API solves a different problem than a shared store: lifecycle management (start, wait-for-ready, crash recovery, shutdown), blocking first call (no polling), and per-sidekick scoping. String-only is by design; config updates don't need complex types, and explicit serialization is always available.
It's actually clean. Every HTTP worker has a bootstrap phase before entering the frankenphp_handle_request loop. That's where get_vars should be called. At-most-once semantics make it safe: all workers call About naming: my preference goes to "sidekick" precisely because these workers ARE secondary to the main app. A Messenger consumer is a different pattern (standalone queue processor). Sidekicks observe the environment and support the main app. The name is also memorable and distinct from the existing worker concept. Caddy-config-based workers could be added later as a complementary approach for ops-level control, although I'm not sure that'd provide the best DX for app developers. Those will prefer not touching the Caddyfile when adding a new sidekick (eg when installing a third-party-provided one). |
Not saying it must be apcu and it would also be fine to start off with string-only support. Just saying that you can't change the API afterwards without BC break, so the functions/classes added to core should be well-named and well thought out so there's room for extension and optinization.
It is messy since the http-workers can start these background threads at runtime without any mechanism to oversee or stop them. It can become hard to reason about which sidekick workers are currently running. The original concept of 'task workers' had workers call |
Add support for "sidekick" workers: long-running PHP scripts that run outside the HTTP request cycle, observe their environment, and publish configuration to HTTP workers in real time.
This enables patterns like Redis Sentinel discovery, secret rotation, feature flag streaming, and cache invalidation — without polling, TTLs, or redeployment.
New PHP functions
frankenphp_sidekick_get_vars(string|array $name, float $timeout = 30.0): arrayStarts a sidekick and returns its published variables. The first call blocks until the sidekick calls
set_vars()or the timeout expires. Subsequent calls return the latest snapshot immediately. When given an array of names, all sidekicks are started in parallel and vars are returned keyed by name. Works in both worker and non-worker mode.frankenphp_sidekick_set_vars(array $vars): voidPublishes a snapshot of variables from inside a sidekick script. All keys and values must be strings. Each call replaces the entire snapshot atomically. Can only be called from a sidekick context.
frankenphp_sidekick_should_stop(): boolCooperative shutdown check. Sidekick scripts poll this in their event loop to exit gracefully when FrankenPHP shuts down. Can only be called from a sidekick context.
Caddyfile configuration
How it works
Design highlights
get_varsblocks until the sidekick has published its initial stateset_varsreplaces all vars at once — no partial state$_SERVERinjectionset_varsandshould_stopthrow if not called from a sidekickget_varsfrom multiple HTTP workers — only one starts the sidekickget_vars(['a', 'b'])starts all sidekicks concurrentlyphp_serverscoping: eachphp_serverblock has its ownSidekickRegistry— different apps on the same Caddy instance are fully isolatedfunction_exists('frankenphp_sidekick_get_vars')lets the same code work with or without FrankenPHPget_varsworks from any PHP script served byphp_serverbin/consolecompatible: sidekick name is available as$_SERVER['argv'][1]and$_SERVER['FRANKENPHP_SIDEKICK_NAME']Runtime behavior
SCRIPT_FILENAMEis set correctly for non-.phpentrypoints#!/usr/bin/env php) are silently skipped