vova3 Jun 2026 05:22

Moved a service from PHP-FPM to Hyperf and hit a nasty bug. We had a class holding the current user in a static property, set early in the request lifecycle. Under FPM that was fine because each request is a fresh process. Under Swoole the worker is long-lived, so request B sometimes saw request A user because the static value survived between requests and coroutines interleave.

I know the fix is Hyperf\Context\Context, but I want to understand the boundary. What exactly is safe to keep as static or in a singleton, and what must go into the coroutine context? Anything that looks like per-request state but is sneaky?

Replies (5)
dmitry_kv3 Jun 2026 06:02

Rule of thumb: anything that represents the current request or current user must live in Context, keyed by nothing because Context is already scoped to the coroutine id. Anything stateless and shareable, like a configured client or a repository with no mutable per-request fields, is fine as a singleton. The trap is the in-between: a service that is mostly stateless but caches one request-specific thing in a property.

0
jnovak3 Jun 2026 06:52

Context::set and Context::get are tied to the current coroutine, and child coroutines do not automatically inherit the parent context. If you spawn a coroutine with go() and expect it to see the user you set in the parent, it will not, you have to copy it across. Hyperf has Coroutine::create with context copying helpers for exactly this. Bit me once with logging context vanishing inside a go() block.

0
wzhang3 Jun 2026 08:12

Watch out for the database connection and transaction state too. If you start a transaction and then fan out work into child coroutines, those children get their own connection from the pool and are not inside your transaction. People assume the transaction is ambient like under FPM. It is not. Keep all writes of one transaction on one coroutine.

0
vova3 Jun 2026 09:32

This is the clarity I needed. The mental model that helped: a singleton is shared across every coroutine in the worker forever, so it must be immutable after construction. Per-request state goes in Context. And child coroutines start with an empty context unless I copy it. Refactored the user holder to Context and the bleed stopped instantly.

0
mykolap3 Jun 2026 11:02

One more sneaky one: ORM model events and global scopes that read from a static. Also static caches like a memoized config keyed by something request-specific. A quick audit trick is to grep for static properties and static $ in your services and ask of each one whether two simultaneous requests could disagree about its value. If yes, it moves to Context.

0
Write a reply
Markdown. ```php blocks are runnable.