Перейти к основному содержимому

Два значения слова «воркер»

Словом «воркер» называют три несвязанные вещи. У них общее только название — поэтому имеет смысл сначала их развести.

Web Worker Второй поток JavaScript в браузере. Считает, чтобы интерфейс оставался отзывчивым.
Обработчик очереди Потребитель, который забирает единицы работы из очереди. Живёт на сервере.
Service Worker Сетевой прокси для PWA. Не связан ни с одним из двух выше — смотрите витрину PWA.

Проверка: спросите, что он потребляет. Обработчик очереди потребляет очередь, Web Worker — сообщения, которые вы ему шлёте, Service Worker — события fetch. Задача не потребляет ничего: она просыпается по таймеру и выполняется.

Воркер A — поток браузера

Обе кнопки запускают одну и ту же функцию: декодировать изображение, уменьшить его и пройти по каждому пикселю, собирая гистограмму цветов. Отличается только поток выполнения. Следите за индикатором, а не за палитрой.

В этом браузере нет OffscreenCanvas или createImageBitmap, поэтому демонстрация здесь не работает.

Путь через воркер передаёт ArrayBuffer, а не копирует его: postMessage(data, [buffer]) передаёт владение и обнуляет копию отправителя. На большой фотографии это разница между копированием и передачей указателя — и самая часто упускаемая часть API.

Воркер B — обработчик очереди

Спит

Это развёртывание бессерверное (vercel): ни один процесс не переживает ответ, поэтому воркер не может быть резидентным. Вместо него очередь разбирает ежедневный cron. Этот сон — не дефект, а сам урок: именно поэтому вся система задач построена на push-триггерах.

Исходящая очередь уведомлений — настоящая таблица-очередь, а это её потребитель. Работа забирается одной атомарной командой: строки блокируются, помечаются и возвращаются вместе, поэтому два воркера не могут взять одну и ту же строку.

outbox.ts — claimDeliveries()
UPDATE notifications.notification_deliveries AS d
SET status = 'processing',
    attempts = d.attempts + 1,
    attempted_at = now()
FROM (
    SELECT id
    FROM notifications.notification_deliveries
    WHERE status = 'pending'
      AND next_attempt_at <= now()
    ORDER BY next_attempt_at, created_at
    LIMIT $1
    FOR UPDATE SKIP LOCKED
) AS c
WHERE d.id = c.id
RETURNING d.id, d.channel, d.attempts;
Размер пакета 25
Максимум попыток 5
Задержка повтора 30s → 2m → 8m → 32m
Срок захвата 5m

Текущая глубина очереди

0 Ожидают
0 В обработке
1 Отправлены
0 Ошибка
0 Исчерпаны

Счётчик попыток служит и токеном ограждения. Если воркер умирает во время отправки, сборщик возвращает строку в очередь после истечения срока захвата — а поскольку повторный захват увеличивает счётчик, запоздалая запись мёртвого воркера уже не совпадает и отбрасывается, вместо того чтобы испортить строку.

Не воркеры: задачи

Фоновая работа в этом проекте состоит из задач, а не из воркеров. Это различие не придирка — оно решает, нужна ли вам очередь вообще.

Задача Единица работы. Что-то будит её и говорит, что выполнить. В простое не стоит ничего, потому что в простое её не существует.
Воркер Процесс, который уже запущен и сам спрашивает очередь, что подошло. Стоит денег и в простое, а взамен даёт повторы и быстрый подхват.

Всё запланированное здесь именно проталкивается, а не вытягивается: cron, интервал или клик администратора прямо называют работу. Смотрите витрину задач.

Думаете, этот паттерн можно сделать лучше? Расскажите как.

Оставить отзыв