Isolate context under Swoole
Under Swoole one process serves thousands of concurrent requests. A plain per-process array
would let one request’s user_id leak into another’s logs. CoroutineContext binds context
to the coroutine instead, so each request sees only its own.
The problem it solves
ProcessContext stores one array per
process — safe under FPM (one request = one process), unsafe under Swoole (many coroutines
share the process). CoroutineContext uses Swoole\Coroutine::getContext(), which returns
a bag bound to the current coroutine. When the coroutine ends, Swoole destroys the bag
automatically.
Requires ext-swoole
CoroutineContext needs ext-swoole (or ext-openswoole). It’s the only part of the
library that depends on the extension — everything else runs anywhere.
Wire it at the entry point
Build the manager with CoroutineContext, or swap storage on an existing manager before
accepting requests:
use Flytachi\Winter\Logger\LoggerFactory;
use Flytachi\Winter\Logger\Context\CoroutineContext;
// Swoole HTTP server entry point, before the server starts accepting:
LoggerFactory::setContextStorage(new CoroutineContext());
LoggerFactory::setDefaultChannel('http');Building it from scratch instead:
use Flytachi\Winter\Logger\LoggerManager;
use Flytachi\Winter\Logger\Context\CoroutineContext;
$manager = new LoggerManager(
contextStorage: new CoroutineContext(),
channels: [ /* ... */ ],
);stdout is safe under Swoole
Use output: 'stdout' for Swoole channels — Swoole manages HTTP responses through its own
async I/O, so writing to php://stdout never causes the broken-pipe problem that FPM has.
More in Output & broken pipe.
Set context per request
Set fields inside the request handler — they’re visible only to that coroutine:
$server->on('request', function ($request, $response) {
$ctx = LoggerFactory::contextStorage();
$ctx->set('request_id', uniqid('req_'));
$ctx->set('method', $request->server['request_method']);
$ctx->set('path', $request->server['request_uri']);
// Handle request — every log line carries request_id/method/path:
LoggerFactory::getLogger('App\\Handler')->info('handling');
$response->end('ok');
// Optional — Swoole clears the coroutine context when it ends anyway,
// but an explicit clear makes the intent obvious:
$ctx->clear();
});Two concurrent requests each get their own request_id — they never see each other’s fields.
Outside a coroutine
Some code runs before any coroutine exists — e.g. onWorkerStart or server bootstrap.
There, Swoole\Coroutine::getCid() is not positive, so CoroutineContext falls back to a
static array. The logger keeps working during bootstrap without throwing; those fields simply
aren’t coroutine-isolated (there’s no coroutine yet to isolate).
$server->on('workerStart', function () {
// No coroutine here — CoroutineContext uses its fallback array, still works:
LoggerFactory::getLogger('App\\Server')->info('worker started');
});WebSocket & other coroutine events
The same isolation applies to any Swoole callback that runs in its own coroutine — each
onMessage, onConnect, or task fires in a fresh coroutine with its own context bag.
Related
- Request context — the general set/clear workflow
- Context isolation — the internals: reference bags & fallback
- API reference → CoroutineContext