· 2 мин чтения
Lease и fencing: как очередь не теряет задачу и не даёт ей выполниться дважды «зомби»
Воркер может упасть, зависнуть или проснуться, когда его задачу уже забрали. Разбираю, как lease и токен аренды в job-queue решают это в SQLite.
У фоновой очереди две неприятные ситуации. Воркер упал посреди задачи, и она «зависла» в статусе running. Или воркер просто надолго замер (пауза GC, сеть), задачу отдали другому, а потом он очнулся и пишет результат поверх чужого.
Lease
Захват задачи выдаёт аренду: срок lease_until (по умолчанию 30 секунд). Пока обработчик работает, воркер продлевает аренду heartbeat-ом каждую треть срока. Если процесс упал, heartbeat прекращается, аренда истекает, и «reaper» возвращает задачу в очередь с backoff или, если попытки кончились, в dead.
Захват одним оператором
Выбор задачи и выдача аренды делает один SQL-оператор:
UPDATE jobs SET status='running', attempts=attempts+1,
lease_until=?, worker=?, token=?, started_at=?
WHERE id=(SELECT id FROM jobs WHERE status='pending' AND run_at<=?
ORDER BY priority DESC, run_at ASC, id ASC LIMIT 1)
RETURNING ...
Поэтому два воркера, даже из разных процессов на одном файле базы, не получат одну задачу.
Fencing
Каждый захват записывает уникальный токен. Продление, завершение и ошибка выполняются с условием WHERE status='running' AND token=?. Если задачу уже перехватили, условие не совпадёт, и «зомби»-воркер получит ErrLeaseLost вместо того, чтобы испортить результат.
Что это не даёт
Гарантия остаётся at-least-once: после истечения аренды обработчик может быть вызван повторно, поэтому он должен быть идемпотентным. Exactly-once очередь не обещает. Подробности и ограничения в README проекта.