TrueAsync ServerКонкурентність усередині запиту

Конкурентність усередині запиту ​

У попередньому розділі showProfile завантажував профіль одним рядком, і я чесно вдав, що за ним нічого немає. Час зізнаватися. За ним три джерела: дані користувача з бази, замовлення з бази, відгуки із зовнішнього API. Три незалежні походи по дані.

Знайома задача? Ще б пак. Це дослівно десятий розділ першої серії, і рішення переноситься без жодної зміни:

php
use Async\TaskGroup;

function showProfile(int $userId, HttpRequest $req, HttpResponse $res): void
{
    $group = new TaskGroup();

    $group->spawnWithKey('user',    fn() => fetchUser($userId));
    $group->spawnWithKey('orders',  fn() => fetchOrders($userId));
    $group->spawnWithKey('reviews', fn() => fetchReviews($userId));

    $res->json($group->all()->await(timeout(2000)));
}

Три запити йдуть конкурентно. Сторінка збирається за час найповільнішого джерела, а не за суму всіх трьох. Таймаут є, fail-together є, CompositeException є.

Тут важливо відчути одну річ: сервер не привніс жодних власних правил конкурентності. Взагалі жодних. Обробник це звичайна корутина, і всередині неї працює все, що ви вже знаєте. Сервер лише відчинив двері, крізь які HTTP-запити заходять у вже знайомий вам світ.

Пул під справжнім навантаженням ​

Пам'ятаєте дев'ятий розділ першої серії? Десять воркерів імпорту, один об'єкт PDO, переплутані транзакції, хаос. Тоді це був навчальний приклад на десять корутин. Так ось, забудьте про десять.

У сервері конкурентних користувачів бази стільки, скільки запитів зараз у польоті. Двадцять. П'ятсот. Скільки нажене трафік, стільки й буде, і це число ви не контролюєте. Єдине, що рятує, ви вже знаєте:

php
$pdo = new PDO($dsn, $user, $password, [
    PDO::ATTR_POOL_ENABLED => true,
    PDO::ATTR_POOL_MIN     => 4,
    PDO::ATTR_POOL_MAX     => 16,
]);

Механіка та сама, що й була: кожна корутина у свій момент володіє з'єднанням одноосібно, транзакції закріплені, а обірване з'єднання тихо замінюється свіжим. Але в POOL_MAX тепер з'явилася нова робота. Тепер це запобіжник між стихією й базою: тисяча конкурентних запитів вишикується в чергу за шістнадцятьма з'єднаннями. Погодьтеся, це краще, ніж тисяча з'єднань, що прийшли до PostgreSQL заразом.

Чий це запит? ​

П'ятнадцятий розділ першої серії закінчився питанням, де зберігати «поточне»: користувача, локаль, ідентифікатор запиту. Тоді ми збудували відповідь на контекстах. Сервер доводить цю історію до кінця, причому красиво.

Кожен обробник виконується у власному scope. А в scope запиту є контекст, спільний для всього дерева корутин цього запиту:

php
use function Async\request_context;
use function Async\spawn;

$server->addHttpHandler(function (HttpRequest $req, HttpResponse $res) {
    request_context()
        ->set('request_id', $req->getHeader('x-request-id') ?? bin2hex(random_bytes(8)))
        ->set('user_id', authenticate($req));

    // ... далі хоч десять рівнів викликів углиб ...
});

function logInfo(string $message): void
{
    $requestId = request_context()->find('request_id');
    error_log("[$requestId] $message");
}

logInfo може бути викликана з обробника. З корутини, яку він породив. З корутини всередині TaskGroup усередині сервісу всередині репозиторію. Неважливо: request_context() скрізь один і той самий, поки ми всередині цього запиту. А сусід-запит, який просто зараз обробляється впереміш із нашим? У нього свій. Триста конкурентних запитів, триста незалежних request_id, нуль глобальних змінних.

Різницю з current_context() тепер можна сформулювати в один рядок: той про одну корутину, цей про весь запит.

Scope прибирає за запитом ​

І останній наслідок, найнепомітніший і найцінніший. Оскільки обробник живе в scope, увесь восьмий розділ першої серії застосовується до запиту автоматично, без жодної дії з вашого боку.

Породили корутину й забули дочекатися? Запит завершиться, scope прибере. Клієнт обірвав з'єднання на півдорозі? Сервер скасує scope запиту, скасування кооперативно дійде до кожної дочірньої корутини, і кожен finally вивільнить своє. Пам'ятаєте, скільки дисципліни вимагала структурна конкурентність? Тут вона перестала бути дисципліною. Тепер це властивість платформи: життя будь-якої корутини запиту обмежене самим запитом. Крапка.

На цьому з невидимою частиною сервера все. Далі підуть байти, і чимало: клієнт завантажує нам файл на гігабайт, а ми віддаємо йому звіт на два. Куди все це складати, щоб не розбудити OOM killer? Наступний розділ саме про це.