Изоляция контекста под Swoole
Под Swoole один процесс обслуживает тысячи конкурентных запросов. Обычный массив на процесс
позволил бы user_id одного запроса протечь в логи другого. CoroutineContext вместо этого
привязывает контекст к корутине, так что каждый запрос видит только свой.
Какую проблему это решает
ProcessContext хранит один массив на
процесс — безопасно под FPM (один запрос = один процесс), небезопасно под Swoole (много
корутин делят процесс). CoroutineContext использует Swoole\Coroutine::getContext(),
который возвращает мешок, привязанный к текущей корутине. Когда корутина завершается, Swoole
уничтожает мешок автоматически.
Требует ext-swoole
CoroutineContext требует ext-swoole (или ext-openswoole). Это единственная часть
библиотеки, зависящая от расширения — всё остальное работает где угодно.
Подключение в точке входа
Соберите менеджер с CoroutineContext или подмените хранилище на существующем менеджере до
начала приёма запросов:
use Flytachi\Winter\Logger\LoggerFactory;
use Flytachi\Winter\Logger\Context\CoroutineContext;
// Точка входа HTTP-сервера Swoole, до того как сервер начнёт принимать запросы:
LoggerFactory::setContextStorage(new CoroutineContext());
LoggerFactory::setDefaultChannel('http');Сборка с нуля вместо этого:
use Flytachi\Winter\Logger\LoggerManager;
use Flytachi\Winter\Logger\Context\CoroutineContext;
$manager = new LoggerManager(
contextStorage: new CoroutineContext(),
channels: [ /* ... */ ],
);stdout безопасен под Swoole
Для каналов Swoole используйте output: 'stdout' — Swoole управляет HTTP-ответами через
собственный асинхронный I/O, поэтому запись в php://stdout никогда не вызывает проблему
broken pipe, которая есть у FPM. Подробнее в
Вывод и broken pipe.
Задавайте контекст на запрос
Задавайте поля внутри обработчика запроса — они видны только этой корутине:
$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']);
// Обрабатываем запрос — каждая строка лога несёт request_id/method/path:
LoggerFactory::getLogger('App\\Handler')->info('handling');
$response->end('ok');
// Опционально — Swoole и так очищает контекст корутины при её завершении,
// но явный clear делает намерение очевидным:
$ctx->clear();
});Два конкурентных запроса получают каждый свой request_id — они никогда не видят полей друг друга.
Вне корутины
Часть кода выполняется до существования какой-либо корутины — например, onWorkerStart или
бутстрап сервера. Там Swoole\Coroutine::getCid() не положителен, поэтому CoroutineContext
откатывается к статическому массиву. Логгер продолжает работать во время бутстрапа, не выбрасывая
исключений; эти поля просто не изолированы по корутинам (корутины ещё нет, чтобы изолировать).
$server->on('workerStart', function () {
// Здесь нет корутины — CoroutineContext использует фолбэк-массив, всё равно работает:
LoggerFactory::getLogger('App\\Server')->info('worker started');
});WebSocket и другие события корутин
Та же изоляция применяется к любому колбэку Swoole, выполняющемуся в своей корутине — каждый
onMessage, onConnect или task срабатывает в свежей корутине со своим мешком контекста.
Связанное
- Контекст запроса — общий рабочий процесс задать/очистить
- Изоляция контекста — внутренности: мешки по ссылке и фолбэк
- Справочник API → CoroutineContext