Package · logger

Context isolation

Request-scoped context is the feature most likely to cause a subtle bug in production — a user_id from one request appearing in another’s logs. This is why the isolation strategy is a swappable object, and how each implementation actually works.

The hazard

You want to set request_id once and have it ride along on every log line for that request. The ContextInjectingProcessor reads a snapshot of the current storage on every record. So the question is: what does “current” mean? Get that wrong and you either lose the field or — worse — leak it into a concurrent request.

The answer depends entirely on the runtime’s concurrency model, which the library refuses to detect. So it delegates to a ContextStorage object supplied by the framework.

ProcessContext — a plain array

Under FPM and CLI, concurrency happens across processes, never inside one. Each FPM request is a fresh worker process; each CLI invocation is one process. So a flat array field is already perfectly isolated — nothing else shares this process’s memory.

php
final class ProcessContext implements ContextStorage
{
  private array $data = [];

  public function set(string $key, mixed $value): void { $this->data[$key] = $value; }
  public function all(): array { return $this->data; }
  public function clear(): void { $this->data = []; }
  // ...
}

The catch is process reuse. FPM recycles workers across requests, and a daemon loops in one process forever. The array survives, so stale fields survive with it. That’s the whole reason clear() exists and why the docs insist on calling it at the end of each unit of work.

Not safe under Swoole

Swoole runs many coroutines in one process. They’d all share this one array — request A’s set() would be visible to request B. ProcessContext is unsafe there by construction; use CoroutineContext.

CoroutineContext — a bag per coroutine

Swoole gives each coroutine its own context object via Swoole\Coroutine::getContext(), which returns an ArrayObject that Swoole destroys when the coroutine ends. CoroutineContext stores its fields inside that object, under a private key, so two concurrent requests read and write different bags.

The interesting part is bag() — it returns the array by reference so mutations land in the coroutine’s own object rather than a copy:

php
private function &bag(): array
{
  if ($this->inCoroutine()) {
      $ctx = \Swoole\Coroutine::getContext();
      if ($ctx !== null) {
          if (!isset($ctx[self::KEY])) {
              $ctx[self::KEY] = [];
          }
          return $ctx[self::KEY];
      }
  }
  return $this->fallback;
}

set(), forget(), and all() all route through bag(), so they’re automatically coroutine-local when a coroutine is active. When it isn’t, they fall through to $fallback.

The out-of-coroutine fallback

Not all code runs inside a coroutine. Server bootstrap, onWorkerStart, or plain CLI mode have no coroutine — Swoole\Coroutine::getCid() returns a non-positive value there:

php
private function inCoroutine(): bool
{
  return class_exists(\Swoole\Coroutine::class, false)
      && \Swoole\Coroutine::getCid() > 0;
}

In that state bag() returns the static $fallback array. The logger keeps working during bootstrap without throwing; those fields simply aren’t coroutine-isolated — there’s no coroutine yet to isolate them from. The class_exists(..., false) check (no autoload) means the same class is safe to instantiate even where Swoole isn’t loaded, though you’d only ever pick it when it is.

Why clear() differs by runtime

text
FPM       — process dies after each request      → clear() optional (shutdown hook is tidy)
CLI once  — process dies after the job            → clear() unnecessary
CLI daemon— one process, many iterations          → clear() REQUIRED per iteration
Swoole    — coroutine bag auto-destroyed by Swoole → clear() optional (explicit = clearer intent)

The rule of thumb: if the execution unit outlives one job, clear between jobs. The two storages make that safe in different ways — one resets an array you own, the other leans on Swoole’s coroutine teardown.

See also