Skip to content

Commit 397770c

Browse files
refactor: rename PHP API and widen state types
Renames: - frankenphp_worker_set_vars - frankenphp_set_worker_state - frankenphp_worker_get_vars - frankenphp_get_worker_state - frankenphp_worker_get_signaling_stream - frankenphp_get_worker_handle - Internal: varsPtr - statePtr, varsVersion - stateVersion, etc. Type widening: - set_worker_state accepts mixed (null, scalars, arrays, enums) - get_worker_state returns mixed - Non-array values are internally wrapped in a single-element array for uniform persistent storage, and unwrapped on read.
1 parent 83b6757 commit 397770c

38 files changed

Lines changed: 248 additions & 180 deletions

background_worker.go

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -271,15 +271,15 @@ func getLookup(thread *phpThread) *backgroundWorkerLookup {
271271
return nil
272272
}
273273

274-
// go_frankenphp_worker_get_vars starts background workers if needed, waits for them
274+
// go_frankenphp_get_worker_state starts background workers if needed, waits for them
275275
// to be ready, takes read locks, copies vars via C helper, and releases locks.
276276
// All locking/unlocking happens within this single Go call.
277277
//
278278
// callerVersions/outVersions: if callerVersions is non-nil and all versions match,
279279
// the copy is skipped entirely (returns 1). outVersions receives current versions.
280280
//
281-
//export go_frankenphp_worker_get_vars
282-
func go_frankenphp_worker_get_vars(threadIndex C.uintptr_t, names **C.char, nameLens *C.size_t, nameCount C.int, timeoutMs C.int, returnValue *C.zval, callerVersions *C.uint64_t, outVersions *C.uint64_t) *C.char {
281+
//export go_frankenphp_get_worker_state
282+
func go_frankenphp_get_worker_state(threadIndex C.uintptr_t, names **C.char, nameLens *C.size_t, nameCount C.int, timeoutMs C.int, returnValue *C.zval, callerVersions *C.uint64_t, outVersions *C.uint64_t) *C.char {
283283
thread := phpThreads[threadIndex]
284284
lookup := getLookup(thread)
285285
if lookup == nil {
@@ -330,7 +330,7 @@ func go_frankenphp_worker_get_vars(threadIndex C.uintptr_t, names **C.char, name
330330
outVSlice := unsafe.Slice(outVersions, n)
331331
allMatch := true
332332
for i, sk := range sks {
333-
v := sk.varsVersion.Load()
333+
v := sk.stateVersion.Load()
334334
outVSlice[i] = C.uint64_t(v)
335335
if uint64(callerVSlice[i]) != v {
336336
allMatch = false
@@ -341,20 +341,24 @@ func go_frankenphp_worker_get_vars(threadIndex C.uintptr_t, names **C.char, name
341341
}
342342
}
343343

344-
// Take all read locks, collect pointers, copy via C helper, then release
344+
// Take all read locks, collect pointers and scalar flags, copy via C helper, then release
345345
ptrs := make([]unsafe.Pointer, n)
346+
scalarFlags := make([]C.int, n)
346347
for i, sk := range sks {
347348
sk.mu.RLock()
348-
ptrs[i] = sk.varsPtr
349+
ptrs[i] = sk.statePtr
350+
if sk.stateIsScalar {
351+
scalarFlags[i] = 1
352+
}
349353
}
350354

351-
C.frankenphp_worker_copy_vars(returnValue, C.int(n), names, nameLens, (*unsafe.Pointer)(unsafe.Pointer(&ptrs[0])))
355+
C.frankenphp_worker_copy_state(returnValue, C.int(n), names, nameLens, (*unsafe.Pointer)(unsafe.Pointer(&ptrs[0])), &scalarFlags[0])
352356

353357
// Write versions while locks are still held
354358
if outVersions != nil {
355359
outVSlice := unsafe.Slice(outVersions, n)
356360
for i, sk := range sks {
357-
outVSlice[i] = C.uint64_t(sk.varsVersion.Load())
361+
outVSlice[i] = C.uint64_t(sk.stateVersion.Load())
358362
}
359363
}
360364

@@ -365,21 +369,22 @@ func go_frankenphp_worker_get_vars(threadIndex C.uintptr_t, names **C.char, name
365369
return nil
366370
}
367371

368-
//export go_frankenphp_worker_set_vars
369-
func go_frankenphp_worker_set_vars(threadIndex C.uintptr_t, varsPtr unsafe.Pointer, oldPtr *unsafe.Pointer) *C.char {
372+
//export go_frankenphp_set_worker_state
373+
func go_frankenphp_set_worker_state(threadIndex C.uintptr_t, statePtr unsafe.Pointer, isScalar C.int, oldPtr *unsafe.Pointer) *C.char {
370374
thread := phpThreads[threadIndex]
371375

372376
bgHandler, ok := thread.handler.(*backgroundWorkerThread)
373377
if !ok || bgHandler.worker.backgroundWorker == nil {
374-
return C.CString("frankenphp_worker_set_vars() can only be called from a background worker")
378+
return C.CString("frankenphp_set_worker_state() can only be called from a background worker")
375379
}
376380

377381
sk := bgHandler.worker.backgroundWorker
378382

379383
sk.mu.Lock()
380-
*oldPtr = sk.varsPtr
381-
sk.varsPtr = varsPtr
382-
sk.varsVersion.Add(1)
384+
*oldPtr = sk.statePtr
385+
sk.statePtr = statePtr
386+
sk.stateIsScalar = isScalar != 0
387+
sk.stateVersion.Add(1)
383388
sk.mu.Unlock()
384389

385390
sk.readyOnce.Do(func() {

bg_worker_vars.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ static bool bg_worker_is_immutable(HashTable *ht) {
2525
}
2626

2727
/* Free a stored vars pointer only if it's a persistent copy (not immutable). */
28-
static void bg_worker_free_stored_vars(void *ptr) {
28+
static void bg_worker_free_stored_state(void *ptr) {
2929
if (ptr != NULL) {
3030
HashTable *ht = (HashTable *)ptr;
3131
if (!bg_worker_is_immutable(ht)) {
@@ -38,7 +38,7 @@ static void bg_worker_free_stored_vars(void *ptr) {
3838

3939
/* Copy or reference a stored vars pointer to request memory.
4040
* Immutable arrays are returned as zero-copy references. */
41-
static void bg_worker_read_stored_vars(zval *dst, void *ptr) {
41+
static void bg_worker_read_stored_state(zval *dst, void *ptr) {
4242
HashTable *ht = (HashTable *)ptr;
4343
if (bg_worker_is_immutable(ht)) {
4444
ZVAL_ARR(dst, ht); /* zero-copy: immutable = safe to share */

docs/background-workers.md

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ They observe their environment and publish configuration that HTTP [workers](wor
66
## How It Works
77

88
1. A background worker runs its own event loop (subscribe to Redis, watch files, poll an API...)
9-
2. It calls `frankenphp_worker_set_vars()` to publish a snapshot of key-value pairs
10-
3. HTTP workers call `frankenphp_worker_get_vars()` to read the latest snapshot
11-
4. The first `get_vars()` call blocks until the background worker has published - no startup race condition
9+
2. It calls `frankenphp_set_worker_state()` to publish a snapshot of key-value pairs
10+
3. HTTP workers call `frankenphp_get_worker_state()` to read the latest snapshot
11+
4. The first `get_worker_state()` call blocks until the background worker has published - no startup race condition
1212

1313
## Configuration
1414

@@ -27,51 +27,52 @@ example.com {
2727
name feature-flags
2828
}
2929
30-
# Catch-all - handles any unlisted name via get_vars()
30+
# Catch-all - handles any unlisted name via get_worker_state()
3131
worker /app/bin/console {
3232
background
3333
}
3434
}
3535
}
3636
```
3737

38-
- **Named** (with `name`): lazy-started on first `get_vars()` call, or auto-started at boot if `num 1` is set.
38+
- **Named** (with `name`): lazy-started on first `get_worker_state()` call, or auto-started at boot if `num 1` is set.
3939
- **Catch-all** (no `name`): also lazy-started. Use `max_threads` to cap how many can be created (defaults to 16). Not declaring a catch-all forbids unlisted names.
4040
- Each `php_server` block has its own isolated scope - two blocks can use the same worker names without conflict.
4141
- `max_consecutive_failures`, `env`, and `watch` work the same as HTTP workers.
4242

4343
## PHP API
4444

45-
### `frankenphp_worker_get_vars(string|array $name, float $timeout = 30.0): array`
45+
### `frankenphp_get_worker_state(string|array $name, float $timeout = 30.0): mixed`
4646

47-
Starts a background worker (at-most-once) and returns its published variables.
47+
Starts a background worker (at-most-once) and returns its published state.
4848

4949
```php
50-
$redis = frankenphp_worker_get_vars('redis-watcher');
50+
$redis = frankenphp_get_worker_state('redis-watcher');
5151
// ['MASTER_HOST' => '10.0.0.1', 'MASTER_PORT' => 6379]
5252

53-
$all = frankenphp_worker_get_vars(['redis-watcher', 'feature-flags']);
53+
$all = frankenphp_get_worker_state(['redis-watcher', 'feature-flags']);
5454
// ['redis-watcher' => [...], 'feature-flags' => [...]]
5555
```
5656

57-
- First call blocks until the background worker calls `set_vars()` or the timeout expires
57+
- First call blocks until the background worker calls `set_worker_state()` or the timeout expires
5858
- Subsequent calls return the latest snapshot immediately
5959
- Within a single HTTP request, repeated calls with the same name return the same cached array - `===` comparisons are O(1)
6060
- Throws `RuntimeException` on timeout, missing entrypoint, or background worker crash
6161
- Works in both worker and non-worker mode
6262

63-
### `frankenphp_worker_set_vars(array $vars): void`
63+
### `frankenphp_set_worker_state(mixed $state): void`
6464

65-
Publishes a snapshot of key-value pairs from inside a background worker.
66-
Each call **replaces** the entire snapshot atomically.
65+
Publishes state from inside a background worker.
66+
Each call **replaces** the entire state atomically.
6767
If the new value is identical (`===`) to the previous one, the call is a no-op.
6868

69-
Supported types: `null`, `bool`, `int`, `float`, `string`, `array` (nested), and **enums**.
69+
State can be `null`, a scalar (`bool`, `int`, `float`, `string`), an `array` (nested), or an **enum**.
70+
Objects, resources, and references are rejected.
7071

7172
- Throws `RuntimeException` if not called from a background worker context
7273
- Throws `ValueError` if values contain objects, resources, or references
7374

74-
### `frankenphp_worker_get_signaling_stream(): resource`
75+
### `frankenphp_get_worker_handle(): resource`
7576

7677
Returns a readable stream for receiving signals from FrankenPHP.
7778
On shutdown or restart, the stream is closed - `fgets()` returns `false` (EOF).
@@ -81,7 +82,7 @@ Use `stream_select()` to wait between iterations instead of `sleep()`:
8182
function background_worker_should_stop(float $timeout = 0): bool
8283
{
8384
static $stream;
84-
$stream ??= frankenphp_worker_get_signaling_stream();
85+
$stream ??= frankenphp_get_worker_handle();
8586
$s = (int) $timeout;
8687

8788
return match (@stream_select(...[[$stream], [], [], $s, (int) (($timeout - $s) * 1e6)])) {
@@ -118,7 +119,7 @@ function run_config_watcher(): void
118119
$redis->pconnect('127.0.0.1');
119120

120121
do {
121-
frankenphp_worker_set_vars([
122+
frankenphp_set_worker_state([
122123
'maintenance' => (bool) $redis->get('maintenance_mode'),
123124
'feature_flags' => json_decode($redis->get('features'), true),
124125
]);
@@ -134,23 +135,23 @@ to integrate the signaling stream into the event loop:
134135
```php
135136
function run_redis_watcher(): void
136137
{
137-
$signalingStream = frankenphp_worker_get_signaling_stream();
138+
$signalingStream = frankenphp_get_worker_handle();
138139
$sentinel = Amp\Redis\createRedisClient('tcp://sentinel-host:26379');
139140

140141
$subscription = $sentinel->subscribe('+switch-master');
141142

142143
Amp\async(function () use ($subscription) {
143144
foreach ($subscription as $message) {
144145
[$name, $oldIp, $oldPort, $newIp, $newPort] = explode(' ', $message);
145-
frankenphp_worker_set_vars([
146+
frankenphp_set_worker_state([
146147
'MASTER_HOST' => $newIp,
147148
'MASTER_PORT' => (int) $newPort,
148149
]);
149150
}
150151
});
151152

152153
$master = $sentinel->rawCommand('SENTINEL', 'get-master-addr-by-name', 'mymaster');
153-
frankenphp_worker_set_vars([
154+
frankenphp_set_worker_state([
154155
'MASTER_HOST' => $master[0],
155156
'MASTER_PORT' => (int) $master[1],
156157
]);
@@ -174,7 +175,7 @@ $app = new App();
174175
$app->boot();
175176

176177
while (frankenphp_handle_request(function () use ($app) {
177-
$config = frankenphp_worker_get_vars('config-watcher');
178+
$config = frankenphp_get_worker_state('config-watcher');
178179

179180
$_SERVER += ['APP_REDIS_HOST' => $config['MASTER_HOST'], 'APP_REDIS_PORT' => $config['MASTER_PORT']];
180181
$app->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER);
@@ -186,8 +187,8 @@ while (frankenphp_handle_request(function () use ($app) {
186187
### Graceful Degradation
187188

188189
```php
189-
if (function_exists('frankenphp_worker_get_vars')) {
190-
$config = frankenphp_worker_get_vars('config-watcher');
190+
if (function_exists('frankenphp_get_worker_state')) {
191+
$config = frankenphp_get_worker_state('config-watcher');
191192
} else {
192193
$config = ['MASTER_HOST' => getenv('REDIS_HOST') ?: '127.0.0.1'];
193194
}
@@ -201,5 +202,5 @@ if (function_exists('frankenphp_worker_get_vars')) {
201202
- `$_SERVER['FRANKENPHP_WORKER_NAME']` is set for all workers (HTTP and background)
202203
- `$_SERVER['FRANKENPHP_WORKER_BACKGROUND']` is `true` for background workers, `false` for HTTP workers
203204
- `$_SERVER['argv']` = `[entrypoint, name]` in background workers (for `bin/console` compatibility)
204-
- Crash recovery with automatic restart and exponential backoff. During the restart window, `get_vars` returns the last published data (stale but available). A warning is logged on crash (`background worker exited, restarting`).
205+
- Crash recovery with automatic restart and exponential backoff. During the restart window, `get_worker_state` returns the last published data (stale but available). A warning is logged on crash (`background worker exited, restarting`).
205206
- On shutdown/restart: the signaling stream is closed (EOF). Workers have 5 seconds to exit. Stuck workers are force-killed on Linux and Windows.

0 commit comments

Comments
 (0)