8000 feat: background workers = non-HTTP workers with shared state by nicolas-grekas · Pull Request #2287 · php/frankenphp · GitHub
[go: up one dir, main page]

Skip to content

feat: background workers = non-HTTP workers with shared state#2287

Open
nicolas-grekas wants to merge 1 commit intophp:mainfrom
nicolas-grekas:sidekicks
Open

feat: background workers = non-HTTP workers with shared state#2287
nicolas-grekas wants to merge 1 commit intophp:mainfrom
nicolas-grekas:sidekicks

Conversation

@nicolas-grekas
Copy link
@nicolas-grekas nicolas-grekas commented Mar 16, 2026

Summary

Background workers are long-running PHP workers that run outside the HTTP cycle. They observe their environment (Redis, DB, filesystem, etc.) and publish configuration that HTTP workers read per-request - enabling real-time reconfiguration without restarts or polling.

PHP API

  • frankenphp_worker_set_vars(array $vars) - publishes config from a background worker (persistent memory, cross-thread)
  • frankenphp_worker_get_vars(string|array $name, float $timeout = 30.0) - reads config from HTTP workers (blocks until first publish, version-cached)
  • frankenphp_worker_get_signaling_stream() - returns a pipe-based stream for stream_select() integration (replaces polling with event-driven shutdown)

Caddyfile configuration

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

    # Named background workers - auto-started at boot
    worker bin/worker.php { background; match config-watcher }
    worker bin/worker.php { background; match image-resizer }

    # Catch-all - lazy-started on first get_vars() with an unlisted name
    worker bin/worker.php { background }
}
  • background marks a worker as non-HTTP
  • match specifies exact worker names (not URL paths); named workers auto-start at boot, before HTTP workers
  • No match = catch-all for lazy-started names via get_vars(). Not declaring a catch-all forbids lazy-started ones.
  • max_threads on catch-all sets a safety cap for lazy-started instances (defaults to 16)
  • num and max_threads rejected on named background workers (pooling is a future feature)
  • max_consecutive_failures defaults to -1 (never panic)
  • Names validated as clean identifiers at config parse time (alphanumeric, hyphens, underscores, colons)

Architecture

  • BackgroundWorkerRegistry per php_server for isolation and at-most-once semantics
  • Persistent memory (pemalloc) with RWMutex for safe cross-thread sharing
  • Version-based caching (varsVersion atomic counter) to skip unchanged data
  • Opcache immutable array zero-copy fast path (IS_ARRAY_IMMUTABLE)
  • Interned string optimizations (ZSTR_IS_INTERNED) - skip copy/free for shared memory strings
  • Rich type support: null, scalars, arrays (nested), enums
  • Signaling stream: pipe-based fd for stream_select() - compatible with amphp/ReactPHP event loops
  • Crash recovery with exponential backoff and automatic restart
  • Graceful shutdown integrated with worker restart lifecycle
  • Thread reservation: background workers get dedicated threads outside the HTTP scaling budget (named: one per match name, catch-all: up to max_threads cap)

Example: Redis config watcher

// bin/config-watcher.php
$redis = new Redis();
$redis->connect('127.0.0.1');

while (!worker_should_stop()) {
    frankenphp_worker_set_vars([
        'maintenance' => (bool) $redis->get('maintenance_mode'),
        'feature_flags' => json_decode($redis->get('features'), true),
    ]);
}
// HTTP worker
$config = frankenphp_worker_get_vars('config-watcher');
if ($config['maintenance']) {
    return new Response('Down for maintenance', 503);
}

Test coverage

14 tests covering: basic vars, at-most-once start, validation, type support (enums, binary-safe strings), multiple background workers, crash restart, signaling stream, worker restart lifecycle, non-background-worker error handling.

All tests pass on PHP 8.2, 8.3, 8.4, and 8.5 with -race.

Documentation

Full docs at docs/background-workers.md.

@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 4 times, most recently from e1655ab to 867e9b3 Compare March 16, 2026 20:26
@AlliBalliBaba
Copy link
Contributor
AlliBalliBaba commented Mar 16, 2026

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.

@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 2 times, most recently from da54ab8 to a06ba36 Compare March 16, 2026 21:45
@henderkes
Copy link
Contributor

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 php_server, not a global setting.

@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 7 times, most recently from ad71bfe to 05e9702 Compare March 17, 2026 08:03
@nicolas-grekas
Copy link
Author
nicolas-grekas commented Mar 17, 2026

@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 $_SERVER injection. No polling, no TTLs, no redeployment.

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:

  • Minimal FrankenPHP config: a single sidekick_entrypoint in php_server(thanks for the idea). No need to declare individual workers in the Caddyfile. The PHP app controls which sidekicks to start via frankenphp_sidekick_start(), keeping the infrastructure config simple.

  • Graceful degradability: apps should work correctly with or without FrankenPHP. The same codebase should work on FrankenPHP (with real-time reconfiguration) and on traditional setups (with static or always refreshed config).

  • Nice framework integration: the sidekick_entrypoint pointing to e.g. bin/console means sidekicks are regular framework commands, making them easy to develop.

Happy to follow up with your proposals now that this is hopefully clarified.
I'm going to continue on my own a bit also :)

@dunglas
Copy link
Member
dunglas commented Mar 17, 2026

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!

@henderkes
Copy link
Contributor

@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:

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 main, but perhaps @AlliBalliBaba can use his experience with the previous PR to influence this one. I'd be more in favour of a general API, than a specific sidecar one.

@nicolas-grekas
Copy link
Author

The PHP-side API has been significantly reworked since the initial iteration: I replaced $_SERVER injection with explicit get_vars/set_vars protocol.

The old design used frankenphp_set_server_var() to inject values into $_SERVER implicitly. The new design uses an explicit request/response model:

  • frankenphp_sidekick_set_vars(array $vars): called from the sidekick to publish a complete snapshot atomically
  • frankenphp_sidekick_get_vars(string|array $name, float $timeout = 30.0): array: called from HTTP workers to read the latest vars

Key improvements:

  • No race condition on startup: get_vars blocks until the sidekick has called set_vars. The old design had a race where HTTP requests could arrive before the sidekick had published its values.
  • Strict context enforcement: set_vars and should_stop throw RuntimeException if called from a non-sidekick context.
  • Atomic snapshots: set_vars replaces all vars at once. No partial state possible
  • Parallel start: get_vars(['redis-watcher', 'feature-flags']) starts all sidekicks concurrently, waits for all, returns vars keyed by name.
  • Works in both worker and non-worker mode: get_vars works from any PHP script served by php_server, not just from frankenphp_handle_request() workers.

Other changes:

  • sidekick_entrypoint moved from global frankenphp block to per-php_server (as @henderkes suggested)
  • Removed the $argv parameter: the sidekick name is the command, passed as $_SERVER['argv'][1]
  • set_vars is restricted to sidekick context only (throws if called from HTTP workers)
  • get_vars accepts string|array: when given an array, all sidekicks start in parallel
  • Atomic snapshots: set_vars replaces all vars at once, no partial state
  • Binary-safe values (null bytes, UTF-8)

@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 3 times, most recently from cb65f46 to 4dda455 Compare March 17, 2026 10:46
@nicolas-grekas
Copy link
Author

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:

  • 1 Caddyfile setting: sidekick_entrypoint (per php_server)
  • 3 PHP functions: get_vars, set_vars, should_stop

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:

  • frankenphp_sidekick_send_task(string $name, mixed $payload): mixed
  • frankenphp_sidekick_receive_task(): mixed

Same worker type, same sidekick_entrypoint, same should_stop(). Just a different communication pattern added on top. No new config, no new worker type.

So the path would be:

  1. This PR: sidekicks with set_vars/get_vars (config publishing)
  2. Future PR: add send_task/receive_task (task offloading), reusing the same non-HTTP worker foundation

The foundation (non-HTTP threads, cooperative shutdown, crash recovery, per-php_server scoping) is shared. Only the communication primitives differ.

WDYT?

@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 4 times, most recently from b3734f5 to ed79f46 Compare March 17, 2026 11:48
@nicolas-grekas
Copy link
Author
nicolas-grekas commented Mar 17, 2026

I think the failures are unrelated - a cache reset would be needed. Any help on this topic?

@alexandre-daubois
Copy link
Member
alexandre-daubois commented Mar 17, 2026

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 🙁

@nicolas-grekas
Copy link
Author

Some important changes to note from previous iteration:

  • I replaced frankenphp_sidekick_should_stop() with frankenphp_sidekick_get_signaling_stream():

Instead of polling a boolean, sidekicks now get a stream resource that becomes readable on shutdown. This integrates with stream_select() for interruptible waits. No more sleep() that blocks shutdown for up to N seconds. The API is still 3 functions: get_vars, set_vars, get_signaling_stream. This enables fully reactive sidekick (eg built on amphp). This should be also used for the "tasks" use case, just with a different stream payload (it's "stop\n" today).

  • set_vars now accepts nested scalars, arrays, and enums, not just strings[]:

Values can be null, bool, int, float, string, nested arrays, and PHP enums. Objects, resources, and references are rejected with a ValueError. Missing enum class/case on get_vars throws LogicException.

This allows richer data types without resorting to implicit serialize - see reasons above for why I think this would be a bad thing. FTR, Symfony 8.1 will provide a DeepCloner class to export any serializable PHP value into an array. Perfect fit for best performance. See symfony/symfony#63695. Explicit serialize() will always remain an option for ppl that can't use DeepCloner.

  • Sidekicks restart on worker restart:

This wasn't fully implemented, now: when RestartWorkers() is called (e.g., for opcache reload or file changes), sidekick threads are shut down and their registry entries are cleaned up. The next get_vars() call starts a fresh sidekick. This ensures sidekicks never run stale code after a worker restarts.

  • Performance: precomputed string hashes:

When copying persistent vars to request memory in get_vars, string key hashes are copied from the persistent copy, avoiding recomputation.

Doc updated.

I think this covers the sidekick use case really well.

And indeed, as @henderkes noted, I want sidekick to be fully reactive. No mandatory polling. Same for tasks BTW. I'm going to give them a try now.

@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 2 times, most recently from 6e422fa to 85bd7d9 Compare March 19, 2026 18:19
@nicolas-grekas nicolas-grekas force-pushed the sidekicks branch 2 times, most recently from aa19e57 to 875337e Compare March 20, 2026 11:19
@nicolas-grekas
Copy link
Author
nicolas-grekas commented Mar 20, 2026

Back to get_vars/set_vars only now. After going forward with tasks (and reserving them for another PR), I confirm this design for vars works really well and is complementary to tasks.

Some important changes since the last update:

  • Performance: immutable array zero-copy

When opcache produces an IS_ARRAY_IMMUTABLE array (e.g. from a const or a literal), set_vars stores the pointer directly — no pemalloc copy. get_vars returns it without copying either. This is the fastest path: zero allocation on both sides.

  • Performance: interned string optimization

Strings that live in shared memory (ZSTR_IS_INTERNED) — class names, enum cases, literal keys — skip copy/free in all persistent zval helpers. This applies to persist, free, request-copy, and to HashTable keys.

  • Performance: version-based cache

Each set_vars call increments a varsVersion atomic counter. get_vars passes the caller's last-known version; if it matches, the RLock + copy is skipped entirely. Callers that poll in a loop pay nothing when the data hasn't changed.

  • Bug fixes

Data race on sidekickStopFdWrite (multiple HTTP workers writing) → changed to atomic.Int32. Use-after-free in get_vars (RLock released before C copy completed) → RLock now held for entire copy, released via go_frankenphp_sidekick_release_vars. Nested get_vars calls overwrote the locked vars entry → changed to a stack. Stale stop fd not invalidated on teardo 8000 wn → Store(-1) in tearDownWorkerScript. Defensive requestOptions slice copy to avoid mutating shared state.

  • Refactoring

Extracted sidekick.go from worker.go — registry, state, startSidekick, and all vars CGo exports in their own file. Extracted sidekick_test.go for unit tests. Consolidated C pipe writes into a single platform-aware sidekick_pipe_write_impl helper.

Error messages now consistently mention enums in validation errors.

@nicolas-grekas nicolas-grekas changed the title feat: application sidekicks = non-HTTP workers with shared state feat: background workers = non-HTTP workers with shared state Mar 20, 2026
@nicolas-grekas
Copy link
Author

Thanks for all the feedback so far. I heard the naming concern and explored renaming "sidekick" to "worker". I also wondered about auto-starting and pre-declaring workers.
I think I found a sweet spot that should account for both your views and mines. This update is significant in terms of public surface (internally it's an increment).

So here we are:

  • Renamed "sidekick" to "background worker"

The sidekick directive and all frankenphp_sidekick_* PHP functions are gone. Background workers are now configured as regular workers with a background flag:

php_server {
    worker public/index.php { num 4 }
    worker bin/worker.php { background; match config-watcher }
    worker bin/worker.php { background }  # catch-all for dynamic names
}

PHP API is now frankenphp_worker_set_vars, frankenphp_worker_get_vars, frankenphp_worker_get_signaling_stream. Server var is FRANKENPHP_WORKER_NAME. No "sidekick" remains anywhere in the codebase.

  • match for background workers = exact worker name

Same keyword as HTTP workers, with this difference: HTTP workers match on URL path, background workers match on worker name. Named background workers are auto-started at boot. A worker without match is a catch-all for lazy-started names. Not declaring a catch-all background worker forbids lazy-started ones.

Name values are validated at config parse time - only alphanumeric, hyphens, underscores, and colons allowed. Typos like match config-* fail early.

  • Config validation

num and max_threads are rejected on named background workers (pooling is a feature that could make sense, but for tasks, not just config-watchers). max_threads is allowed on catch-all workers as a safety cap for lazy-started instances (defaults to 16). max_consecutive_failures defaults to -1 (never panic). Background workers are excluded from HTTP routing and from the duplicate filename check.

  • Auto-start at boot

Named background workers (those with match) are started automatically during Init(), before HTTP workers. This gives them a head start on set_vars so the first HTTP request is less likely to block on get_vars.

  • Thread reservation

Background workers get dedicated thread slots outside the global max_threads budget. They don't compete with HTTP auto-scaling. For named workers, one thread per match name is reserved. For catch-all workers, the max_threads safety cap determines the reservation. A clear error message is shown when the budget is consumed.

Docs updated at docs/background-workers.md, I invite you to have a look at it for more details.

PR description above also updated. 🚀

Comment on lines +3 to +14
function background_worker_should_stop(float $timeout = 0): bool
{
static $signalingStream;
$signalingStream ??= frankenphp_worker_get_signaling_stream();
$s = (int) $timeout;

return match (@stream_select(...[[$signalingStream], [], [], $s, (int) (($timeout - $s) * 1e6)])) {
0 => false, // timeout
false => true, // error (pipe closed) = stop
default => "stop\n" === fgets($signalingStream),
};
}
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO doing this via stream/pipe instead of with just a go channel still feels too clunky. As I understood the idea is to be able to integrate the stream into the Amp/React event-loop.

As @henderkes mentioned, not sure where async PHP is going in the near future and even resources might go away in a future PHP version. So I'd rather have this abstracted away into a single function call somehow.

Easiest to do would be just selecting over the worker drain channel a sleep timeout, but I understand that will block the entire thread. If we really want to support an async select, I'd rather have a dedicated way to let the 2 event loops communicate, that is generic enough so we can change it later on. Haven't though too much about how that would look like yet.

Comment on lines +159 to +176
reservedThreads := 0
for _, w := range f.Workers {
if !w.Background {
continue
}
if len(w.MatchPath) > 0 {
// Named: one thread per match name
reservedThreads += len(w.MatchPath)
} else {
// Catch-all: reserve up to the safety cap (default 16)
maxW := w.MaxThreads
if maxW <= 0 {
maxW = 16
}
reservedThreads += maxW
}
}
if reservedThreads > 0 {
Copy link
Contributor
@AlliBalliBaba AlliBalliBaba Mar 20, 2026

Choose a reason for hiding this comment

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

Instead of explicitly reserving threads, couldn't we just set the default to
num_threads 0
max_threads 1

Then the workers would just behave as any other worker in terms of thread number and it would be possible to have the thread running instantly or start multiple of the same worker.

num_threads 0 => worker starts lazily
num_threads 1 => worker starts immediately
num_threads 2 => multiple threads (for future use cases)

Comment on lines +1032 to +1046
char *error = go_frankenphp_start_background_worker(
thread_index, Z_STRVAL_P(names), Z_STRLEN_P(names));
if (error) {
zend_throw_exception(spl_ce_RuntimeException, error, 0);
free(error);
RETURN_THROWS();
}

char *name_ptr = Z_STRVAL_P(names);
size_t name_len_val = Z_STRLEN_P(names);
void *vars_ptr = NULL;
unsigned long out_version = 0;
error = go_frankenphp_worker_wait_and_get(thread_index, &name_ptr,
&name_len_val, 1, timeout_ms,
&vars_ptr, NULL, &out_version);
Copy link
Contributor

Choose a reason for hiding this comment

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

You can probably combine go_frankenphp_start_background_worker and go_frankenphp_worker_wait_and_get in one function call.

free(error);
RETURN_THROWS();
}
bg_worker_free_stored_vars(old);
Copy link
Contributor
@AlliBalliBaba AlliBalliBaba Mar 20, 2026

Choose a reason for hiding this comment

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

Freeing these persistent zvals is probably unsafe since other threads might still be referencing them. Not sure you can make this fully safe and-non leaky with persistent vals.

The safe solution is to probably copy the zval directly from thread-local memory to thread-local memory.

Hmm actually not entirely sure, would need to look into how the parallel extension does this.

Comment on lines +164 to +171
func (registry *BackgroundWorkerRegistry) startAutoWorkers() error {
for _, name := range registry.autoStartNames {
if err := startBackgroundWorkerWithRegistry(registry, name); err != nil {
return fmt.Errorf("failed to auto-start background worker %q: %w", name, err)
}
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I fully understand the 'auto start names'. Couldn't you just re-use the worker name itself? Also would be nice to reuse num and max_threads to determine which workers should be started immediately or lazily as mentioned before.

Comment on lines +242 to +261
timeout := time.Duration(timeoutMs) * time.Millisecond
done := make(chan error, 1)
go func() {
timer := time.NewTimer(timeout)
defer timer.Stop()
for i, sk := range sks {
select {
case <-sk.ready:
// background worker has called set_vars
case <-timer.C:
done <- fmt.Errorf("timeout waiting for background worker: %s", goNames[i])
return
}
}
done <- nil
}()

if err := <-done; err != nil {
return C.CString(err.Error())
}
Copy link
Contributor
@AlliBalliBaba AlliBalliBaba Mar 20, 2026

Choose a reason for hiding this comment

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

Couldn't you just read the variables directly as long as they have been set once (RWLock)? Would be a lot more efficient than channels+goroutines.

A timeout should just be necessary the first time when starting lazily.

If a background worker only gets read after first calling set_var, you just need to check if they are ready or wait for their ready state.

Comment on lines +280 to +292
func go_frankenphp_worker_release_vars(threadIndex C.uintptr_t) {
idx := int(threadIndex)
stack := lockedVarsStacks[idx]
if len(stack) == 0 {
return
}
sks := stack[len(stack)-1]
lockedVarsStacks[idx] = stack[:len(stack)-1]

for _, sk := range sks {
sk.mu.RUnlock()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally locking/unlocking would happen in a single go function call, this feels a bit convoluted.
C->go->C is fine to do btw.

Comment on lines +299 to +301
if !ok || handler.worker.httpEnabled || handler.worker.backgroundWorker == nil {
return C.CString("frankenphp_worker_set_vars() can only be called from a background worker")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if HTTP workers could have their own global variables, there probably are possible use cases. Maybe calling get_vars without a worker name gets the vars of the current worker.

get_vars potentially lazily starting workers instead of just returning null makes this more awkward though.

Comment on lines +1098 to +1107
char **name_ptrs = emalloc(sizeof(char *) * name_count);
size_t *name_lens_arr = emalloc(sizeof(size_t) * name_count);
void **vars_ptrs = emalloc(sizeof(void *) * name_count);
int idx = 0;
ZEND_HASH_FOREACH_VAL(ht, val) {
name_ptrs[idx] = Z_STRVAL_P(val);
name_lens_arr[idx] = Z_STRLEN_P(val);
idx++;
}
ZEND_HASH_FOREACH_END();
Copy link
Contributor
@AlliBalliBaba AlliBalliBaba Mar 20, 2026

Choose a reason for hiding this comment

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

I wonder if we should even allow getting multiple global variables at once. Makes all of this a lot more complex. Probably could make this PR a lot smaller by just allowing one get_var per function call, better to start out easy.

Multiple function calls might even be more efficient since you have to do less allocations. You can't start bg workers in parallel in that case though, at least not lazily.

Comment on lines +1805 to +1825
if (worker_bg_name != NULL) {
CG(skip_shebang) = 1;

zend_is_auto_global_str("_SERVER", sizeof("_SERVER") - 1);
zval *server = &PG(http_globals)[TRACK_VARS_SERVER];
if (server && Z_TYPE_P(server) == IS_ARRAY) {
zval argv_array;
array_init(&argv_array);
add_next_index_string(&argv_array, file_name);
add_next_index_string(&argv_array, worker_bg_name);

zval argc_zval;
ZVAL_LONG(&argc_zval, 2);

zend_hash_str_update(Z_ARRVAL_P(server), "argv", sizeof("argv") - 1,
&argv_array);
zend_hash_str_update(Z_ARRVAL_P(server), "argc", sizeof("argc") - 1,
&argc_zval);
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you actually need the worker name in argv and argc here if it's already in $_SERVER? What do you need the filename for?

Comment on lines +106 to +108
opts = append(opts, WithRequestPreparedEnv(PreparedEnv{
"FRANKENPHP_WORKER_NAME\x00": worker.name,
}))
Copy link
Contributor

Choose a reason for hiding this comment

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

We could probably add FRANKENPHP_WORKER_NAME to all workers, not just background workers. You could even initialize key and value here as persistent zend_strings, so the registration cost is negligible.

}

if !worker.httpEnabled {
handler.thread.state.Set(state.Ready)
Copy link
Contributor
@AlliBalliBaba AlliBalliBaba Mar 20, 2026

Choose a reason for hiding this comment

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

Probably should reach ready state in markBackgroundReady instead of here. Allows better control over when the bg worker is actually ready.

onThreadReady func(int)
onThreadShutdown func(int)
queuedRequests atomic.Int32
httpEnabled bool
Copy link
Contributor

Choose a reason for hiding this comment

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

isBackgroundWorker probably makes more sense at this point.

Suggested change
httpEnabled bool
isBackgroundWorker bool

@AlliBalliBaba
Copy link
Contributor

Didn't get through everything, might continue at a later point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

0