Изоляция контекста
Контекст уровня запроса — та фича, что с наибольшей вероятностью вызовет тонкий баг в
проде: user_id из одного запроса всплывает в логах другого. Вот почему стратегия изоляции —
это заменяемый объект, и как реально работает каждая реализация.
Опасность
Вы хотите задать request_id один раз и чтобы он ехал в каждой строке лога этого запроса.
ContextInjectingProcessor читает снимок текущего хранилища на каждой записи. Значит вопрос:
что означает «текущее»? Ошибитесь — и вы либо потеряете поле, либо — хуже — протечёте его в
конкурентный запрос.
Ответ целиком зависит от модели конкурентности рантайма, которую библиотека отказывается
определять. Поэтому она делегирует объекту
ContextStorage, поставляемому фреймворком.
ProcessContext — обычный массив
Под FPM и CLI конкурентность происходит между процессами, а не внутри одного. Каждый FPM-запрос — свежий воркер-процесс; каждый вызов CLI — один процесс. Так что поле-массив уже идеально изолировано — ничто больше не делит память этого процесса.
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 = []; }
// ...
}Загвоздка — переиспользование процесса. FPM перерабатывает воркеры между запросами, а
демон крутится в одном процессе вечно. Массив выживает, а вместе с ним и устаревшие поля.
Именно поэтому существует clear() и почему доки настаивают на его вызове в конце каждой
единицы работы.
Небезопасно под Swoole
Swoole запускает много корутин в одном процессе. Все они делили бы этот единственный массив —
set() запроса A был бы виден запросу B. ProcessContext там небезопасен по построению;
используйте CoroutineContext.
CoroutineContext — мешок на корутину
Swoole даёт каждой корутине свой объект контекста через Swoole\Coroutine::getContext(),
который возвращает ArrayObject, уничтожаемый Swoole при завершении корутины. CoroutineContext
хранит свои поля внутри этого объекта под приватным ключом, так что два конкурентных запроса
читают и пишут в разные мешки.
Интересная часть — bag(): он возвращает массив по ссылке, чтобы мутации попадали в
собственный объект корутины, а не в копию:
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() и all() — все идут через bag(), поэтому автоматически локальны для
корутины, когда корутина активна. Когда нет — проваливаются в $fallback.
Фолбэк вне корутины
Не весь код выполняется внутри корутины. У бутстрапа сервера, onWorkerStart или обычного
режима CLI корутины нет — Swoole\Coroutine::getCid() там возвращает неположительное значение:
private function inCoroutine(): bool
{
return class_exists(\Swoole\Coroutine::class, false)
&& \Swoole\Coroutine::getCid() > 0;
}В этом состоянии bag() возвращает статический массив $fallback. Логгер продолжает работать
во время бутстрапа, не бросая исключений; эти поля просто не изолированы по корутинам — корутины
ещё нет, чтобы от неё изолировать. Проверка class_exists(..., false) (без автозагрузки)
означает, что этот же класс безопасно инстанцировать даже там, где Swoole не загружен, хотя вы
выбрали бы его только когда он есть.
Почему clear() отличается по рантайму
FPM — процесс умирает после каждого запроса → clear() опционален (хук на shutdown аккуратен)
CLI разово — процесс умирает после задачи → clear() не нужен
CLI-демон — один процесс, много итераций → clear() ОБЯЗАТЕЛЕН на итерацию
Swoole — мешок корутины уничтожается Swoole → clear() опционален (явный = чётче намерение)Правило большого пальца: если единица исполнения переживает одну задачу — очищайте между задачами. Два хранилища делают это безопасным по-разному — одно сбрасывает массив, которым вы владеете, другое опирается на снос корутины в Swoole.
Смотрите также
- Контекст запроса — рабочий процесс задать/очистить
- Корутины Swoole — точка входа Swoole
- Жизненный цикл записи лога — где читается снимок