Повтор — нормальный режим распределённой системы

Worker может выполнить действие, но упасть до записи успеха. Клиент увидит timeout и повторит запрос. Оркестратор тоже повторит activity. Если действие — генерация черновика, вы получите лишние токены; если это письмо, публикация или платёж — реальный ущерб. Поэтому каждый side effect проектируют для повторного исполнения.

Выберите границу бизнес-операции

Ключ должен означать то, что пользователь считает одной операцией: invoice_id + charge, campaign_id + recipient_id + send, document_id + version + publish. Случайный UUID, генерируемый при каждом retry, бесполезен: он делает повторы разными операциями.

Таблица идемпотентности

Создайте запись с уникальным индексом по (tenant_id, operation, key). Храните hash нормализованного payload, статус, attempt, external_reference, response snapshot и expiry. Если тот же ключ пришёл с другим payload, возвращайте конфликт — молча применять старый результат опасно.

BEGIN;
INSERT INTO operations(key, payload_hash, status)
VALUES (:key, :hash, 'started')
ON CONFLICT DO NOTHING;
-- победитель выполняет следующий шаг
COMMIT;

Inbox и transactional outbox

Inbox дедуплицирует входящие события. Outbox решает разрыв между записью состояния и отправкой команды: бизнес-транзакция сохраняет изменение и событие outbox вместе, а отдельный publisher доставляет событие с повторами. Получатель всё равно обязан дедуплицировать его — outbox убирает потерю, но не обещает отсутствие повторов.

Состояние «исход неизвестен»

Самый сложный случай: внешний API принял команду, но ответ потерялся. Нельзя автоматически повторять необратимое действие. Сначала запросите состояние по idempotency key или external reference. Если провайдер не поддерживает поиск и ключи, добавьте ручное подтверждение либо reconciliation job.

Checkpoint для многошагового агента

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

Что нельзя считать защитой

  • локальный флаг в памяти исчезает при рестарте;
  • проверка «есть ли объект» перед созданием создаёт race condition;
  • mutex одного процесса не защищает от второго worker;
  • низкая вероятность дубля не делает платёж безопасным.

Нужна уникальность, гарантированная общим хранилищем или целевым сервисом.

Тесты, которые находят настоящие ошибки

Инъецируйте падение после внешнего успеха, но до локального commit; дважды доставляйте одно сообщение параллельно; повторяйте запрос после истечения TTL; меняйте payload при том же ключе. Инвариант теста формулируйте по бизнесу: списание одно, письмо одно, опубликованная версия одна.

Жизненный цикл записи операции

Статусы должны иметь строгие переходы: reserved → executing → succeeded, а также failed_retryable, failed_terminal и unknown. Владелец lease обновляет heartbeat; другой worker может забрать просроченную запись, но сначала обязан выполнить reconciliation. Ответ успешной операции кэшируется и возвращается всем повторам с тем же ключом.

TTL и повторное использование ключа

Удалять ключ сразу после ответа опасно: запоздавшее сообщение снова выполнит действие. TTL выбирают по максимальному времени доставки, сроку retry и бизнес-риску. Для платежа или публикации запись может храниться столько же, сколько сама сущность. Если storage дорог, оставьте компактный tombstone с hash payload и external reference.

Компенсация не равна откату

Распределённую цепочку нельзя откатить как одну транзакцию. Вместо этого определите компенсирующее действие: отменить бронь, создать возврат, снять публикацию. Компенсация сама является идемпотентной операцией с отдельным ключом и может завершиться ошибкой. Пользователю показывают фактическое состояние, а не фиктивное «всё отменено».

С чего начать внедрение?
Выберите один реальный workflow, опишите его внешние эффекты и отказ в середине выполнения. Затем добавьте минимальные policy, idempotency и наблюдаемость до роста автономности.
Можно ли решить задачу одним system prompt?
Нет. Prompt задаёт поведение модели, но timeout, права, уникальность операций, изоляция и аудит должны обеспечиваться исполняющей инфраструктурой.
Какие ошибки нельзя повторять автоматически?
Ошибки прав и валидации, исчерпанный бюджет, просроченный дедлайн и необратимые действия с неизвестным исходом требуют остановки, сверки состояния или человека.
Что измерять в production?
Успешность и длительность по шагам, число повторов, возраст работы, внешние эффекты, ошибки policy, стоимость, насыщение лимитов и долю ручных вмешательств.
Как безопасно увеличить автономность?
Расширяйте полномочия по одному классу действий: shadow, затем canary с малым лимитом, обязательный аудит и kill switch, после чего анализируйте реальные инциденты.
← Все статьи блога