7 мин

Шаблон cron + база данных: фоновые задачи без очереди

Узнайте про шаблон cron + база данных для запуска плановых фоновых задач с повторными попытками, блокировкой и идемпотентностью — без разворачивания полноценной системы очередей.

Шаблон cron + база данных: фоновые задачи без очереди

Проблема: плановые задачи без лишней инфраструктуры

Большинству приложений нужно, чтобы действия выполнялись позже или по расписанию: отправка напоминаний по электронной почте, ночная проверка биллинга, очистка старых записей, пересборка отчёта или обновление кеша.

На первых порах легко хочется подключить полноценную систему очередей — кажется, это «правильный» путь для фоновых задач. Но очереди добавляют новых движущихся частей: ещё один сервис для запуска, мониторинга, деплоя и отладки. Для небольшой команды (или одиноко работающего основателя) этот дополнительный вес может замедлить разработку.

Так встаёт реальный вопрос: как запускать плановые задачи надёжно, не поднимая лишнюю инфраструктуру?

Обычная первая попытка проста: добавить cron‑запись, которая вызывает endpoint, и пусть этот endpoint делает работу. Это работает — до первого сбоя. Как только у вас больше одного сервера, деплой в неудачный момент или задача выполняется дольше ожидаемого, начинаются запутанные ошибки.

Плановые задачи обычно ломаются предсказуемыми способами:

  • Двойные запуски: два сервера выполняют одну и ту же задачу, поэтому счета формируются дважды или письма отправляются дважды.
  • Пропущенные запуски: вызов cron прерывается во время деплоя, и никто не замечает до жалоб пользователей.
  • Молчащие ошибки: задача упала один раз и больше не запускается, потому что нет плана повторных попыток.
  • Частично выполненная работа: задача падает на полпути и оставляет данные в неконсистентном состоянии.
  • Отсутствие аудита: нельзя ответить на вопросы «когда это последний раз запускалось?» или «что произошло прошлой ночью?».

Шаблон cron + база данных — это компромисс. Cron по‑прежнему «будит» систему по расписанию, но намерения и состояние задач хранятся в базе данных, чтобы система могла координировать выполнение, делать повторы и фиксировать, что произошло.

Это подходит, когда у вас уже есть одна база данных (обычно PostgreSQL), небольшое количество типов задач и вы хотите предсказуемое поведение с минимальными операционными усилиями. Это также естественный выбор для приложений, быстро собранных на современных стеках (например, React + Go + PostgreSQL).

Это не подойдёт, когда нужна очень высокая пропускная способность, долгие задачи со стримингом прогресса, строгая последовательность между множеством типов задач или сильный фан‑аут (тысячи подзадач в минуту). В таких случаях полноценная очередь и выделенные воркеры обычно окупаются.

Главная идея простыми словами

Шаблон cron + база данных запускает фоновые задачи по расписанию без разворачивания полноценной системы очередей. Cron (или любой планировщик) по‑прежнему используется, но cron не решает, что запускать. Он просто часто «будит» воркер (обычно раз в минуту). База данных решает, какая работа пора, и гарантирует, что каждая задача будет взята только одним воркером.

Представьте общую доску с чеклистом. Cron — это человек, который заходит в комнату каждую минуту и спрашивает: «Кому что нужно сейчас сделать?» База данных — доска, на которой видно, что запланировано, что уже взято и что выполнено.

Компоненты просты:

  • Один триггер‑планировщик запускается часто.
  • Таблица задач хранит «что» и «когда» (время запуска), а также статус и счётчик попыток.
  • Один или несколько воркеров опрашивают таблицу, заявляют задачу и выполняют её.
  • Заявка на задачу использует блокировку в базе, чтобы два воркера не взяли одну строку.
  • База данных остаётся источником правды о том, что запускалось, что упало и что нужно повторить.

Пример: нужно отправлять напоминания по счетам каждое утро, обновлять кеш каждые 10 минут и чистить старые сессии ночью. Вместо трёх отдельных cron‑команд (каждая со своими режимами пересечений и отказов) вы храните записи задач в одном месте. Cron запускает тот же воркер. Воркер спрашивает PostgreSQL: «Что должно выполниться сейчас?» — и PostgreSQL позволяет воркеру безопасно захватить ровно одну задачу за раз.

Это масштабируется постепенно. Можно начать с одного воркера на одном сервере. Позже запустить пять воркеров на нескольких серверах. Контракт остаётся тем же: таблица — это контракт.

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

Проектируем таблицу задач (практическая схема)

Паттерн лучше всего работает, когда база данных становится источником правды о том, что должно выполниться, когда это должно выполниться и что произошло в прошлый раз. Схема несложная, но мелкие детали (поля блокировок и правильные индексы) важны по мере роста нагрузки.

Одна таблица или две?

Два распространённых подхода:

  • Одна объединённая таблица — когда вам важнее текущее состояние каждой задачи (просто, меньше JOIN‑ов).
  • Две таблицы — когда нужно чёткое разделение между «что это за задача» и «каждый её запуск» (лучше история, легче отлаживать).

Если вы ожидаете частую отладку ошибок, храните историю. Если хотите минимальную настройку — начните с одной таблицы и добавьте историю позже.

Практическая схема (версия с двумя таблицами)

Ниже макет для PostgreSQL. Если вы пишете на Go с PostgreSQL, эти колонки легко мапятся на структуры.

-- What should exist (the definition)
create table job_definitions (
  id            bigserial primary key,
  job_type      text not null,
  payload       jsonb not null default '{}'::jsonb,
  schedule      text,                      -- optional: cron-like text if you store it
  max_attempts  int not null default 5,
  created_at    timestamptz not null default now(),
  updated_at    timestamptz not null default now()
);

-- What should run (each run / attempt group)
create table job_runs (
  id            bigserial primary key,
  definition_id bigint references job_definitions(id),
  job_type      text not null,
  payload       jsonb not null default '{}'::jsonb,
  run_at        timestamptz not null,
  status        text not null,             -- queued | running | succeeded | failed | dead
  attempts      int not null default 0,
  max_attempts  int not null default 5,

  locked_by     text,
  locked_until  timestamptz,

  last_error    text,
  created_at    timestamptz not null default now(),
  updated_at    timestamptz not null default now()
);

Пара деталей, которые сэкономят вам силы позже:

  • Держите job_type короткой строкой для маршрутизации (например, send_invoice_emails).
  • Храните payload как jsonb, чтобы его можно было развивать без миграций.
  • run_at — это ваш «время следующего запуска». Cron (или скрипт планировщика) его устанавливает, воркеры потребляют.
  • locked_by и locked_until позволяют воркерам заявлять задачи, не мешая друг другу.
  • last_error должен быть коротким и читаемым человеком. Стек‑трейсы храните отдельно при необходимости.

Индексы, которые вам понадобятся

Без индексов воркеры будут сканировать слишком много строк. Начните с:

  • Индекса для быстрого поиска задач по времени: (status, run_at)
  • Индекса для обнаружения истёкших блокировок: (locked_until)
  • Опционально: частичный индекс только для активных задач (например, для статусов queued и failed)

Это сохраняет быстрый поиск «следующей задачи» даже при росте таблицы.

Блокировки и безопасная заявка задач

Цель проста: много воркеров могут работать, но только один должен взять конкретную задачу. Если два воркера обработают одну и ту же строку, вы получите дубли — письма, списания и т.д.

Безопасный подход — рассматривать заявку как «аренду» (lease). Воркер помечает задачу как заблокированную на короткий срок. Если воркер падает, аренда истекает и другую задачу может взять другой воркер. Для этого и нужен locked_until.

Используйте аренду, чтобы сбои не блокировали работу навсегда

Без аренды воркер может заблокировать задачу и никогда её не разблокировать (процесс убит, сервер перезагружен, деплой прерван). С locked_until задача станет снова доступной после истечения времени.

Типичное правило: задачу можно заявить, когда locked_until равен NULL или locked_until <= now().

Захватывайте задачи одним атомарным обновлением

Ключевая деталь — заявить задачу в одном выражении (или в одной транзакции). Нужно, чтобы база была арбитром.

Вот распространённый паттерн в PostgreSQL: взять одну просроченную задачу, заблокировать её и вернуть воркеру. (Этот пример использует таблицу jobs; тот же принцип применим к job_runs.)

WITH next_job AS (
  SELECT id
  FROM jobs
  WHERE status = 'queued'
    AND run_at \u003c= now()
    AND (locked_until IS NULL OR locked_until \u003c= now())
  ORDER BY run_at ASC
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE jobs j
SET status = 'running',
    locked_until = now() + interval '2 minutes',
    locked_by = $1,
    attempts = attempts + 1,
    updated_at = now()
FROM next_job
WHERE j.id = next_job.id
RETURNING j.*;

Почему это работает:

  • FOR UPDATE SKIP LOCKED позволяет нескольким воркерам соревноваться, не блокируя друг друга.
  • Аренда устанавливается при заявке, поэтому другие воркеры её игнорируют до истечения.
  • RETURNING отдаёт строку воркеру, который выиграл гонку.

Насколько должна быть длинной аренда и как её продлевать?

Установите аренду длиннее обычного времени выполнения, но достаточно короткой, чтобы при падении процесс быстро восстанавливался. Если большинство задач завершаются за 10 секунд, аренды в 2 минуты обычно достаточно.

Для долгих задач продлевайте аренду по ходу (heartbeat). Простой подход: каждые 30 секунд продлевать locked_until, если вы всё ещё владеете задачей.

  • Длина аренды: 5–20× типичного времени выполнения
  • Интервал heartbeat: 1/4–1/2 аренды
  • Обновление продления должно содержать WHERE id = $job_id AND locked_by = $worker_id

Последнее условие важно: оно не позволит воркеру продлить аренду на задачу, которой он больше не владеет.

Повторы и бэкофф, которые ведут себя предсказуемо

Добавить админ-экран для задач
Сгенерировать небольшую внутреннюю страницу для безопасного просмотра упавших и 'dead' задач.

Повторы — то место, где этот паттерн либо работает спокойно, либо превращается в шумный кошмар. Цель проста: при сбое задачи пытаться снова позже так, чтобы можно было объяснить, измерить и остановить повторы.

Начните с явного и конечного состояния задач: queued, running, succeeded, failed, dead. На практике failed означает «упало, но будет повторяться», а dead — «упало и мы сдались». Такое различие предотвращает бесконечные циклы.

Счётчик попыток — второй ограничитель. Храните attempts (сколько раз пробовали) и max_attempts (сколько раз разрешено). Когда воркер ловит ошибку, он должен:

  • увеличить attempts
  • поставить статус failed, если attempts < max_attempts, иначе dead
  • вычислить run_at для следующей попытки (только для failed)

Бэкофф — это просто правило, которое решает следующую дату run_at. Выберите одно и документируйте:

  • Фиксированная задержка: всегда ждать 1 минуту
  • Экспоненциальная: 1м, 2м, 4м, 8м
  • Экспоненциальная с лимитом: экспоненциальная, но не больше, например, 30 минут
  • Добавьте джиттер: рандомизируйте немного, чтобы задачи не пытались одновременно

Джиттер важен, когда зависимость падёт и восстановится. Без него сотни задач могут повториться в один и тот же момент и снова упасть.

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

  • last_error (короткое сообщение, безопасное для админки)
  • error_code или error_type (помогает группировать ошибки)
  • failed_at и next_run_at
  • опционально last_stack (если контролируете размер)

Конкретное правило, которое часто работает: помечать задачи dead после 10 попыток и использовать экспоненциальный бэкофф с джиттером. Это даёт повторную обработку для временных сбоев и останавливает сломанные задачи.

Идемпотентность: как избежать дубликатов, если задача повторяется

Идемпотентность значит, что ваша задача может выполняться несколько раз, но итог будет один и тот же. В этом паттерне это важно, потому что одна и та же строка может быть взята снова после краха, таймаута или повтора. Если задача «отправить счёт по почте», повторный запуск может быть нежелателен.

Практический подход: разделите задачу на (1) выполнение работы и (2) применение эффекта. Эффект должен произойти один раз, даже если работа выполняется несколько раз.

Используйте ключ идемпотентности, привязанный к бизнес‑событию

Ключ идемпотентности должен исходить из того, что представляет задача, а не из попытки воркера. Хорошие ключи устойчивы и понятны: invoice_id, user_id + day, или report_name + report_date. Если две попытки касаются одного и того же реального события, у них должен быть один ключ.

Пример: «Сгенерировать ежедневный отчёт продаж за 2026-01-14» — ключ sales_report:2026-01-14. «Списать платёж по счёту 812» — invoice_charge:812.

Гарантируйте «только один раз» через ограничения базы

Проще всего позволить PostgreSQL отвергнуть дубликаты. Храните ключ идемпотентности в индексируемом поле и добавьте уникальное ограничение.

-- Example: ensure one logical job/effect per business key
ALTER TABLE jobs
ADD COLUMN idempotency_key text;

CREATE UNIQUE INDEX jobs_idempotency_key_uniq
ON jobs (idempotency_key)
WHERE idempotency_key IS NOT NULL;

Это предотвратит существование двух строк с одинаковым ключом одновременно. Если ваша схема хранит историю, накладывайте уникальность на таблицу эффектов, например sent_emails(idempotency_key) или payments(idempotency_key).

Распространённые побочные эффекты, которые стоит защищать:

  • Письма: создавайте строку sent_emails с уникальным ключом перед отправкой или записывайте идентификатор сообщения провайдера после отправки.
  • Webhook'и: храните delivered_webhooks(event_id) и пропускайте, если запись уже есть.
  • Платежи: всегда используйте идемпотентность провайдера платежей и собственный уникальный ключ в базе.
  • Запись файлов: пишите во временное имя, затем переименовывайте, или храните запись file_generated, ключом которой служит (type, date).

Если вы строите на стеке с Postgres (например, Go + PostgreSQL), эти проверки уникальности быстры и удобно держать рядом с данными. Главная идея проста: повторы — нормальны, дубликаты — нет.

Пошагово: минимальный воркер и планировщик

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

Выберите один скучный рантайм и придерживайтесь его. Цель паттерна — меньше движущихся частей, поэтому небольшой процесс на Go, Node или Python, который общается с PostgreSQL, обычно достаточно.

Соберите это в пяти шагах

  1. Создайте таблицы и индексы. Добавьте таблицу jobs (и дополнительные lookup‑таблицы по необходимости), индексируйте run_at и добавьте индекс, который поможет воркеру быстро находить доступные задачи (например, по (status, run_at)).

  2. Напишите небольшую функцию постановки в очередь. Приложение вставляет строку с run_at = now() или будущим временем. Держите payload маленьким и предсказуемым (ID и тип задачи, не огромные blob'ы).

INSERT INTO jobs (type, payload, status, run_at, attempts, max_attempts)
VALUES ($1, $2::jsonb, 'queued', $3, 0, 10);
  1. Реализуйте цикл захвата. Запускайте его в транзакции. Выбирайте несколько просроченных задач, блокируйте их так, чтобы другие воркеры пропустили, и помечайте running в той же транзакции.
WITH picked AS (
  SELECT id
  FROM jobs
  WHERE status = 'queued' AND run_at \u003c= now()
  ORDER BY run_at
  FOR UPDATE SKIP LOCKED
  LIMIT 10
)
UPDATE jobs
SET status = 'running', started_at = now()
WHERE id IN (SELECT id FROM picked)
RETURNING *;
  1. Обработка и финализация. Для каждой заявленной задачи выполните работу, затем обновите статус на done с finished_at. Если упало — запишите сообщение об ошибке и верните в queued с новым run_at (бэкофф). Держите финализационные обновления небольшими и выполняйте их всегда, даже при завершении процесса.

  2. Добавьте понятные правила повторов. Используйте простую формулу, например run_at = now() + (attempts^2) * interval '10 seconds', и останавливайте после max_attempts, выставляя status = 'dead'.

Добавьте базовую видимость

На первый день не нужен полноценный дашборд, но нужно достаточно, чтобы замечать проблемы:

  • Логируйте одну строку на задачу: заявлено, успешно, упало, повтор, dead.
  • Сделайте простой admin‑view для «dead jobs» и «старых running задач».\n- Настройте алерты по количеству (например, больше N dead задач за последний час).

Если вы уже на стеке Go + PostgreSQL, это хорошо мапится на один бинарный воркер плюс cron.

Реалистичный пример, который можно скопировать

Представьте небольшой SaaS с двумя плановыми задачами:

  • Ночная очистка, удаляющая просроченные сессии и старые временные файлы.
  • Еженедельный email «ваша активность», отправляемый каждому пользователю по понедельникам.

Держите всё просто: одна таблица в PostgreSQL для задач и один воркер, запускаемый каждую минуту (cron). Воркер захватывает просроченные задачи, выполняет их и фиксирует успех или ошибку.

Что ставится в очередь и когда

Вы можете ставить задачи в очередь из нескольких мест:

  • Ежедневно в 02:00: поставить одну задачу cleanup_nightly для «сегодня».
  • При регистрации: поставить send_weekly_report для следующего понедельника пользователя.
  • После события (например, «пользователь нажал Export report»): поставить send_weekly_report, который выполнится сразу для конкретного диапазона.

Payload — минимально необходимый набор данных. Держите его маленьким, чтобы было легко повторять.

{
  "type": "send_weekly_report",
  "payload": {
    "user_id": 12345,
    "date_range": {
      "from": "2026-01-01",
      "to": "2026-01-07"
    }
  }
}

Как идемпотентность предотвращает двойную отправку

Воркер может упасть в самый худший момент: сразу после отправки письма, но до того, как пометит задачу как «done». При рестарте он может снова взять ту же задачу.

Чтобы избежать дублей, дайте работе естественный ключ дедупликации и храните его там, где база может наложить ограничение. Для еженедельных отчётов хороший ключ — (user_id, week_start_date). Перед отправкой воркер записывает «я собираюсь отправить отчёт X». Если запись уже есть — пропускает отправку.

Это может быть простая таблица sent_reports с уникальным ограничением на (user_id, week_start_date) или уникальный idempotency_key в самой задаче.

Как выглядит падение и как оно возобновляется

Предположим, провайдер почты таймаутит. Задача падает, и воркер:

  • увеличивает attempts
  • сохраняет сообщение об ошибке для отладки
  • планирует следующую попытку с бэкоффом (например: +1 мин, +5 мин, +30 мин, +2 часа)

Если продолжает падать за пределами лимита (например, 10 попыток), помечаем как dead и прекращаем повторы. Задача либо однажды выполняется успешно, либо повторяется по понятному графику, а идемпотентность делает повторы безопасными.

Распространённые ошибки и ловушки

Прототипировать cron-исполнителя
Быстро превратите этот шаблон в рабочий cron-воркер и API.

Паттерн прост, но мелкие ошибки могут привести к дубликатам, застрявшей работе или неожиданной нагрузке. Большинство проблем проявляются после первого краха, деплоя или всплеска трафика.

Ошибки, приводящие к дублям или застреванию задач

Основные инциденты происходят из нескольких ловушек:

  • Запуск одной и той же задачи из нескольких cron‑записей без аренды. Если два сервера сработают в одну минуту, они оба могут заявить одну и ту же работу, если шаг захвата не атомарен и не ставит блокировку/аренду в той же транзакции.
  • Игнорирование locked_until. Если воркер падает после заявки, строка может остаться «в процессе» навсегда. Таймстамп аренды позволит другому воркеру подобрать задачу позже.
  • Мгновенные повторы при ошибке. Когда API недоступен, мгновенные повторы создают всплески, сжигают лимиты и продолжают падать в цикле. Всегда планируйте следующую попытку в будущем.
  • Расхождение «как минимум один раз» и «ровно один раз». Задача может выполниться дважды (таймауты, рестарты воркера, сетевые проблемы). Если два выполнения вредны — сделайте побочный эффект безопасным для повторов.
  • Хранение больших payload'ов в строке задачи. Огромные JSON‑блоб‑ы раздуют таблицу, замедлят индексы и сделают блокировки тяжелее. Храните ссылку (например, user_id, invoice_id или ключ файла) и подгружайте остальное при выполнении.

Пример: вы отправляете еженедельное письмо по счёту. Если воркер таймаутит после отправки, но до отметки задачи как выполненной, та же задача может быть повторена и отправить дубликат. Это нормально для паттерна, если вы не добавите защитный механизм (например, запись уникального события «письмо отправлено» по invoice_id).

Менее очевидные ловушки

Не смешивайте планирование и исполнение в одной долгой транзакции. Если держать транзакцию открытой во время сетевых вызовов, вы держите блокировки дольше и блокируете других воркеров.

Следите за расхождением часов между машинами. Используйте время базы (NOW() в PostgreSQL) как источник правды для run_at и locked_until, а не время сервера приложения.

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

Держите таблицу задач в порядке. Если завершённые задачи накапливаются бесконечно, запросы замедляются и растёт конкуренция за блокировки. Выберите простое правило хранения/архивации старых строк до того, как таблица станет огромной.

Быстрая чек‑листа и дальнейшие шаги

Чек‑лист

Перед вводом в прод убедитесь, что базовые вещи на месте. Малое упущение часто превращается в застрявшие задачи, неожиданные дубли или воркер, который бьёт по базе.

  • В таблице задач есть базовые поля: run_at, status, attempts, locked_until и max_attempts (плюс last_error для видимости).
  • Каждая задача может безопасно выполниться дважды. Если нет — добавьте ключ идемпотентности или уникальное ограничение вокруг побочного эффекта (например, один счёт на invoice_id).
  • Есть место для наблюдения за ошибками и принятия решений: просмотр упавших задач, повторный запуск или пометка как dead.
  • Таймаут аренды адекватен для работы: длиннее нормального времени, но короткий, чтобы упавшие воркеры не блокировали прогресс часами.
  • Бэкофф предсказуем: замедляет повторы и останавливается после max_attempts.

Если это выполнено — шаблон cron + база данных обычно стабилен для реальных рабочих нагрузок.

Дальнейшие шаги

Когда чек‑лист удовлетворён, уделите внимание повседневной эксплуатации:

  • Добавьте две простые админ‑действия: «повторить сейчас» (устанавливает run_at = now() и очищает блокировку) и «отменить» (переводит в терминальный статус). Это экономит время при инцидентах.
  • Пусть воркер логирует одну строку на задачу: тип, id, номер попытки и результат. Настройте алерт на рост количества ошибок.
  • Нагрузите систему реалистичным всплеском: много задач на одну минуту. Если захват задач становится медленным, добавьте нужный индекс (часто — по status, run_at).

Если хотите быстро собрать такую систему, Koder.ai (koder.ai) может помочь пройти от схемы до деплоя Go + PostgreSQL с меньшим ручным кодированием, пока вы сосредотачиваетесь на правилах блокировок, повторов и идемпотентности.

Если позже вы перерастёте эту схему, вы всё равно поймёте жизненный цикл задач, и те же идеи послужат основой для полноценной системы очередей.

FAQ

Когда стоит использовать cron и базу данных для фоновых задач?

Используйте этот подход, если у вас уже есть база данных, типов задач немного и вам нужна работа по расписанию без отдельной очереди. Он подходит для напоминаний по электронной почте, очистки, отчётов и обновления кэша.

Что делает cron в этой схеме?

Cron должен лишь регулярно запускать воркер, часто раз в минуту. База данных определяет, какие задачи пора выполнить, сохраняет их состояние и позволяет одному воркеру забрать каждую задачу.

Какие поля должны быть в таблице jobs?

Храните тип задачи, небольшой JSON payload, время выполнения, статус, число попыток, лимит повторов, владельца блокировки, срок её действия и короткое сообщение об ошибке. Эти поля дают воркерам достаточно информации, чтобы безопасно выполнять и восстанавливать задачи.

Как нескольким воркерам не запускать одну и ту же задачу?

Забирайте задачи в одной транзакции базы данных с блокировкой строк, например PostgreSQL FOR UPDATE SKIP LOCKED. До того как воркер начнёт выполнять задачу, обновите строку: укажите статус running и задайте срок аренды.

Зачем фоновым задачам нужен тайм-аут аренды?

Аренда - это временная блокировка, записанная в locked_until. Если воркер завершится с ошибкой, аренда истечёт, и другой воркер сможет забрать задачу, вместо того чтобы она навсегда зависла.

Как повторять неудачные задачи?

Повторяйте попытку позже по понятному правилу увеличения задержки: например, через 1 минуту, 2 минуты, 4 минуты, затем с ограниченной задержкой и небольшой случайностью. Когда допустимое число попыток исчерпано, помечайте задачу как dead, чтобы она больше не занимала время воркера.

Что такое идемпотентность и почему она важна?

Сделайте побочный эффект безопасным для повторения. Например, используйте стабильный ключ идемпотентности, такой как ID счёта или ID пользователя вместе с датой отчёта, а затем обеспечьте уникальность в PostgreSQL или у внешнего провайдера.

Гарантирует ли этот подход обработку ровно один раз?

Нет. Воркер может завершиться с ошибкой после отправки электронного письма или вызова платёжного провайдера, но до сохранения успешного результата. Проектируйте систему для обработки как минимум один раз, а идемпотентность предотвратит повторные эффекты.

Должен ли воркер держать транзакцию базы данных во время выполнения задачи?

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

Как отслеживать cron и задачи в базе данных?

Отслеживайте dead-задачи и давно выполняющиеся задачи, записывайте в журнал каждое получение задачи и результат, а также настраивайте оповещения при росте числа сбоев. Добавьте простые действия для администратора: повторить задачу сейчас, отменить её или посмотреть последнюю ошибку.

Похожие статьи