Circuit breaker, retry и dead-letter queue в Python: как не терять данные при сбоях. Соберите надёжный пайплайн с примером кода.
Вы когда-нибудь задумывались, что происходит с заказом, если платёжный шлюз падает в самый неподходящий момент? Обычный ответ — «мы залогируем ошибку», но что дальше? В этой статье я покажу, как соединить circuit breaker, retry и dead-letter queue в одном декораторе, чтобы ни одна транзакция не пропала. Вы узнаете, как реализовать это на Python и какие подводные камни ждут на пути.
Представьте: вы пишете интернет-магазин на Django. Обычный CRUD: товары, корзина, checkout. И вот на этапе оплаты вы задаёте себе вопрос: что если платёжный шлюз ответит не медленно, а просто упадёт? Запрос ушёл, деньги, возможно, списались, а ответ так и не пришёл. Куда денется заказ?
В большинстве кодовых баз ответ честный: заказ уйдёт в логи, и на этом всё. Когда сбой закончится, работа, которая не выполнилась, просто потеряется. Восстановить её можно только вручную, перелопачивая логи.
Я столкнулся с этой проблемой в своём пет-проекте. Начал писать circuit breaker вручную, а потом узнал, что уже есть библиотеки вроде pybreaker и tenacity. Но они отвечают на разные вопросы: retry — «попробовать ещё раз?», circuit breaker — «стоит ли вообще вызывать?». Ни один не отвечает на главный: когда breaker открыт и вызов не происходит, куда девается заказ клиента?
Я собрал библиотеку, которая объединяет circuit breaker, retry, fallback и dead-letter queue в один декоратор. Вот как это выглядит:
import baldur
@baldur.protected("charge-customer", retry=True, dlq=True)
def charge(order_id: str) -> dict:
return payment_gateway.charge(order_id)
Каждый паттерн по отдельности — учебник: circuit breaker, retry, fallback и dead-letter queue. Но вместе они дают синергию: breaker знает, что сделал retry, fallback покрывает и исчерпанные ретраи, и открытый breaker, а работа, которая пережила всё это, попадает в durable-хранилище вместо того, чтобы испариться. Потом её можно воспроизвести, когда зависимость вернётся.
Изначально функция захвата была в платном тарифе. В июле я перенёс её в open-source ядро. Логика простая: не терять работу — это вся суть проекта. Если прятать это за лицензионным ключом, бесплатная версия становится демонстрацией проблемы, а не решением. Операции с очередью в масштабе — платные, но захват и возврат работы — нет.
Service mesh умеет ретраить HTTP-запросы между сервисами, и делает это хорошо. Но он не может переставить в очередь Celery-задачу, вернуть кэшированную цену вместо ошибки или понять, что этот сбой относится к заказу 12345. Для этого нужны аргументы вызова, а значит, логика должна жить внутри процесса. Если у вас уже есть mesh — это не замена ему, а дополнение для слоя, который mesh не видит.
Бэкенд по умолчанию — in-memory. Никакого Redis, Docker или переменных окружения. Подключайте Redis, когда понадобится разделять состояние между воркерами (в проде — обязательно), но первый запуск должен работать на ноутбуке без лишних зависимостей. Инструмент для надёжности, который требует инфраструктуры до того, как его можно попробовать, — это инструмент, который вы оцените в пятницу вечером и больше никогда не откроете.
На странице resource budget раньше был показатель overhead на колене насыщения. Теперь его там нет, и страница объясняет почему. Изменение в circuit breaker убрало запись состояния, которая выполнялась на каждом успешном запросе — и эта запись оказалась основной частью того, что измерялось. Затем повторное измерение проводилось на хосте, который был недостаточно тихим, так что сравнение утонуло в собственном шуме. Поэтому страница теперь говорит, что цифра отозвана до повторного измерения. Лучше не публиковать ничего, чем цифру, против которой есть доказательства. Единственная цифра, которую я привожу — ~39 мкс на защищённый вызов на стандартной цепочке, в памяти, без сети.
Это не проверено боевыми историями. У меня нет производственного инцидента, о котором можно рассказать. Что я могу показать — тестовый набор, архитектурные проверки на каждый коммит и измеренный бюджет с методологией. Это ранний доступ: API стабилен, но минорная версия может содержать ломающие изменения с записью в changelog. Работает в одном регионе (Redis Sentinel для высокой доступности — PRO-функция). Это не хостинг-сервис: код выполняется в вашем процессе, не отправляет телеметрию и не делает внешних вызовов.
Если вы сталкивались с этим — сбой закончился, дашборды позеленели, а работа, которая не выполнилась, просто пропала — мне интересно, что вы сделали. Грепали логи и вручную восстанавливали записи? Писали одноразовый скрипт воспроизведения? Решили, что не стоит восстанавливать?
Я построил библиотеку вокруг своего ответа на этот вопрос. Я почти не знаю, насколько он распространён, и хочу узнать это от вас, а не гадать. Если посмотрите на код, два решения, которые я хотел бы оспорить: декоратор вместо sidecar и in-memory по умолчанию. Оба выглядят правильными, пока кто-то не запустит их в неожиданной конфигурации.
Если вы используете tenacity и хотите честное сравнение, где он останавливается — вот сравнение (там есть случаи, где tenacity — лучший выбор).
Попробуйте добавить baldur в свой проект и оберните один проблемный вызов (например, к платёжному API) декоратором @baldur.protected. Посмотрите, как поведёт себя система при имитации сбоя. Убедитесь, что заказы не теряются. Это займёт полчаса, но даст понимание, как работают эти паттерны в деле.
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →