Absurd: пять месяцев durable execution на Postgres - опыт эксплуатации и разбор архитектуры
Durable execution - это попытка сделать так, чтобы длинные, сложные фоновые процессы вели себя как хорошо спроектированная программа: каждый шаг можно перезапустить, ничего не потеряв. Если воркер умер на шестом шаге из десяти, не нужно перепроходить первые пять, городить многослойную retry-логику поверх очередей, держать в голове сложные графы зависимостей. Вместо этого система сама помнит, где остановилась, и продолжает выполнение с последнего устойчивого состояния.
Absurd - минималистичная реализация такой модели прямо внутри Postgres. Без отдельных сервисов, без тяжёлого рантайма и без сотен тысяч строк кода. По сути, это один SQL-файл с хранимыми процедурами и лёгкие обёртки для популярных языков. Автор Flask, Армин Ронахер, использует Absurd уже пять месяцев под реальной нагрузкой и подробно разбирает, что в этой архитектуре выстояло, а что пришлось дорабатывать.
Что такое Absurd: модель в двух словах
Вся логика Absurd живёт в базе данных. Ядро - SQL-скрипт `absurd.sql`, который создаёт схему, набор таблиц и хранимых процедур:
- управление задачами (workflows);
- шаги и чекпоинты;
- события, по которым задачи могут "просыпаться";
- планирование на основе claims - воркер "резервирует" задачу, чтобы её не подхватил другой.
Поверх этого ядра - тонкие SDK для TypeScript, Python и экспериментальный для Go. Задача разработчика - в коде описать workflow как набор шагов, а Absurd возьмёт на себя:
- разбиение на шаги;
- запись прогресса в чекпоинты;
- рестарты с последнего успешного шага;
- ожидание внешних событий;
- долговременное хранение состояния в Postgres.
Задачи могут "спать" часами, днями и неделями, не удерживая никаких ресурсов приложений: состояние живёт в таблицах, а воркеры поднимаются и опускаются по мере надобности.
Декомпозированные шаги и чекпоинты
Базовая единица работы в Absurd - шаг. В первоначальной версии API основным примитивом был `ctx.step(fn)`: вы передаёте функцию, система её выполняет и кэширует результат. Если воркер прерывается, при следующем запуске Absurd смотрит: шаг уже выполнен, результат есть - значит, повторно вызывать функцию не нужно.
Это покрывает 80-90 % обычных задач: интеграции, API-запросы, вычисления, обновления записей. Важная особенность: шаг - это не просто вызов функции, а консистентный фрагмент бизнес-логики, который должен быть либо выполнен целиком, либо не выполнен вообще. За это отвечает транзакционная модель Postgres и продуманное хранение состояний шагов.
Однако по мере эксплуатации стало ясно: одной конструкции `step()` мало. Иногда нужно:
- сначала понять, выполнялся ли шаг до этого;
- принять условное решение - действительно ли нужно его выполнять;
- моделировать заведомые падения, чтобы проверить устойчивость;
- строить более гибкие before / after-хуки вокруг вызова.
Именно здесь в игру вступило обновлённое API шагов.
Новое API: beginStep / completeStep
Главное архитектурное изменение за пять месяцев - введение парных операций `beginStep()` / `completeStep()`. Вместо непрозрачного "вызови и забудь" теперь можно:
1. Явно начать шаг - `beginStep()` возвращает handle.
2. Проверить его состояние: выполнялся ли он ранее, есть ли сохранённый результат.
3. В зависимости от этого решить, выполнять ли "тело" шага или пропустить его.
4. По завершении записать результат через `completeStep()`.
Такое разделение:
- позволяет реализовать до- и пост-хуки вокруг вызова внешних систем;
- упрощает отладку сложных сценариев, где шаг может вести себя по-разному в зависимости от прошлого состояния;
- помогает тестировать падения в строго определённых местах workflow.
Особенно это критично, когда шаги делают "грязные" вещи: например, взаимодействуют с внешними API, отправляют деньги, триггерят запуск сторонних систем. Вам нужно быть уверенным, что при ретрае шаг не совершит действие повторно, если оно уже было успешно выполнено.
Результаты задач: от "fire-and-forget" к управляемым workflow
Изначально Absurd был спроектирован по принципу "fire-and-forget": задачу запускают, дальше она живёт своей жизнью в фоновом режиме. Это отлично подходит для чисто асинхронных процессов - например, массовой обработки данных или длительных миграций.
Но в реальном продукте быстро появляются кейсы, где нужно:
- запустить задачу из запроса пользователя;
- дождаться результата;
- показать статус или частичный прогресс;
- породить дочерние задачи и отслеживать их завершение.
За пять месяцев в Absurd появилась полноценная поддержка получения и ожидания результатов задач. Теперь можно:
- стартовать workflow;
- продолжать делать что-то ещё в приложении;
- позже запросить его статус и итог;
- при необходимости - синхронно дождаться завершения.
Это открывает дорогу сложным композициям: один workflow может порождать дочерние задачи, агрегировать их результаты, строить "фан-аут / фан-ин" паттерны без самостоятельного управления очередями.
Для интеграции с агентами (например, интеллектуальными планировщиками или системами, принимающими решения) это особенно важно: агент инициирует задачи, а затем может либо периодически опрашивать их статус, либо ждать событий от Absurd и реагировать на завершение шагов и подзадач.
absurdctl: Habitat для операционных задач
Отдельный пласт изменений связан не с кодом SDK, а с эксплуатацией. На бумаге durable execution выглядит элегантно, но в продакшене главное - что вы будете делать в ночь на воскресенье, когда одна из задач застряла, а бизнес ждёт результата.
Чтобы это было не больно, появился `absurdctl` - CLI, превращающий Absurd в удобный "habitat" для фоновых процессов:
- инициализация и миграция схемы;
- создание и управление очередями;
- запуск, остановка и рестарт задач;
- просмотр зависших шагов и статусов;
- ручной триггер ретраев и перераспределение задач между воркерами.
Фактически `absurdctl` стал визуальным (пусть и текстовым) интерфейсом к "внутренностям" системы: операторы видят, на каком шаге завис конкретный workflow, что именно пытается сделать воркер, и могут без паники вмешаться.
Наличие такого инструмента делает Absurd похожим на полноценную execution-платформу, а не библиотеку для разработчиков. DevOps-командам проще интегрировать её в существующие пайплайны, строить мониторинг и алертинг вокруг конкретных очередей и типов задач.
Habitat: окружение для задач как часть архитектуры
Под Habitat Армин описывает целую совокупность практик и инструментов вокруг Absurd: не только SQL-ядро и SDK, но и всё, что нужно, чтобы задачи жили предсказуемо:
- правила именования очередей и типов задач;
- единый способ логирования шагов и результатов;
- шаблоны развёртывания воркеров;
- мониторинг claim-ов, lease-ов и дедлоков;
- сценарии ручного вмешательства через `absurdctl`.
За пять месяцев выяснилось, что успех durable execution не только в самой идее чекпоинтов, но и в том, насколько дисциплинировано команда использует систему:
- как делит задачи на шаги;
- как оборачивает внешние вызовы;
- как пишет ретраи;
- как логирует инциденты.
Absurd дал удобную точку опоры: с одной стороны - минимальное ядро, с другой - возможность навесить сверху "habitat", подстроенный под конкретную компанию или продукт.
Интеграция с агентами и внешними системами
Durable execution становится особенно интересным, когда в картине появляются агенты - системы, принимающие решения, планирующие действия и реагирующие на мир вокруг:
- ML-агенты, которые строят планы обработки данных;
- бизнес-оркистраторы, раскладывающие сложную операцию на подзадачи;
- пользовательские ассистенты, запускающие фоновые процессы по команде.
Absurd оказался удобной платформой для таких сценариев по нескольким причинам:
1. Долгая жизнь задач. Агент может запустить цепочку действий, "забыть" о ней на часы или дни, а потом вернуться и проверить, что уже сделано, что в очереди, а что упало и требует вмешательства.
2. Событийная модель. Задачи могут ждать внешних событий - например, ответа из сторонней системы, подтверждения от пользователя или наступления определённого времени.
3. Чёткие чекпоинты. Агент может безопасно переигрывать только тот участок процесса, где произошёл сбой, не перепроизводя уже выполненные операции.
На практике это превращает Absurd в надёжный "двигатель" под капотом агентных систем: всё, что связано с состоянием и ретраями, перекладывается на Postgres, а в приложении остаётся только логика принятия решений.
Что устояло без переделок
Армин отмечает, что фундаментальная архитектура, описанная ещё в ноябре, практически не изменилась:
- модель задач и шагов;
- система чекпоинтов;
- работа с событиями;
- механизм suspend - приостановки задач до внешнего сигнала;
- claim-based планирование - резервирование задач воркерами.
Эти базовые абстракции оказались достаточно универсальными, чтобы пережить реальную нагрузку без радикальной переработки. Всё развитие шло вокруг них: добавлялись guard-rail'ы, улучшались edge-cases, наводился порядок в том, как воркеры конкурируют за задачи и как система восстанавливается после сбоев.
Что добавили под боевой нагрузкой
По мере того как Absurd стал критичным компонентом инфраструктуры, всплыли типичные для продакшен-систем проблемы. За пять месяцев были добавлены:
- Ужесточение работы с claim-ами. Воркеры теперь аккуратнее резервируют и возвращают задачи, снижая риск двойного выполнения.
- Watchdog-и. Наблюдатели, которые отслеживают зависшие или "сломанные" воркеры и возвращают их незавершённые задачи обратно в очередь.
- Защита от дедлоков на уровне Postgres. Более аккуратная работа с транзакциями и блокировками, чтобы тяжёлая нагрузка не приводила к взаимным блокировкам.
- Lease management. Управление "арендой" задач: воркер регулярно продлевает lease, показывая, что он ещё жив; если перестаёт - задача возвращается в пул.
- Обработка гонок на событиях. В реальной жизни события могут приходить почти одновременно, а воркеры - пытаться их обработать в пересекающихся транзакциях. Эти случаи пришлось целенаправленно закрывать.
Почти все эти изменения нельзя придумать на чистом дизайне: они появляются только тогда, когда система перестаёт быть игрушкой и становится чем-то, на что реально опираются продуктовые фичи.
Главная нерешённая архитектурная проблема: партиционирование
Единственное место, где архитектура пока остаётся "дырявой", - масштабирование таблиц за счёт партиционирования. Для систем долгоживущих задач и событий это критично: количество записей растёт быстро, и без продуманного шардинга и партиций Postgres рано или поздно начнёт тормозить.
Ключевой камень преткновения - операции вида `DETACH PARTITION CONCURRENTLY`. В теории они позволяют безболезненно отцепить старую партицию и, например, архивировать её. На практике:
- такие операции не запускаются из `pg_cron` из‑за ограничений на транзакции;
- сочетание длительных транзакций и фоновых задач создаёт нестабильные сценарии;
- часть админских операций пока приходится делать вручную или через костыли.
То есть архитектурно Absurd готов жить на партиционированных таблицах, но практический рецепт безболезненного обслуживания под тяжёлой нагрузкой ещё не доведён до идеала. Это то направление, где автор явно видит дальнейшее развитие.
Для чего используют Absurd на практике
За пять месяцев эксплуатации Absurd "прижился" в ролях, где классические очереди и cron-задачи начинают скрипеть:
- Сложные миграции и бэкфиллы. Когда нужно пройтись по миллионам записей, аккуратно меняя структуру данных, с возможностью продолжить с произвольного места.
- Интеграции с ненадёжными внешними сервисами. Повторные запросы, компенсационные транзакции, сложные правила ретраев - всё это легче выразить как набор шагов с чекпоинтами.
- Оркестрация долгих бизнес-процессов. Регистрации, сложные верификации, цепочки согласований, где этапы могут растягиваться на недели.
- Фоновые операции для пользовательских функций. Там, где в интерфейсе нужно сразу вернуть ответ, но тяжёлая обработка должна продолжаться надёжно и предсказуемо.
При этом главный плюс Absurd - отсутствие отдельного сервиса: достаточно Postgres, который у большинства команд и так является ключевым компонентом. Нет дополнительных систем для мониторинга, бэкапов и обновлений - всё сосредоточено вокруг уже знакомой базы.
Чего не хватает, чтобы назвать систему завершённой
Даже после десятка релизов Армин честно признаёт: у Absurd ещё много зон роста:
- Улучшенные инструменты визуализации. CLI даёт контроль, но графическое представление цепочек задач, их статуса и временных линий сделало бы систему ещё удобнее.
- Более богатое моделирование зависимостей. Уже сейчас можно порождать дочерние задачи и ждать их завершения, но хочется первой формы декларативных DAG-ов и сложных условных ветвлений.
- Стандартизованные паттерны. Многие команды повторяют одни и те же конструкции: саги, компенсации, дедлайны. Они могли бы стать готовыми building blocks в SDK.
- Документация под разные языки. TypeScript и Python-проекты сильно различаются культурой и подходами. Хочется руководств, заточенных под каждый стек.
Часть из этого уже на подходе, часть пока живёт на уровне идей, но опыт первых пяти месяцев показывает: ядро достаточно компактно и понятно, чтобы поверх него спокойно наращивать функциональность.
Размеры SDK и вопрос сложности: когда "меньше" действительно "лучше"
Один из самых ярких контрастов, на который указывает Армин: объём кода. TypeScript SDK Absurd занимает порядка 1400 строк, Python - около 1900. Для сравнения, популярные системы вроде Temporal для Python достигают порядка сотен тысяч строк.
Это не попытка "победить" конкурентов цифрами, а показатель архитектурного выбора:
- ядро вынесено в SQL и использует проверенные механизмы Postgres;
- SDK остаётся тонким слоем удобства, а не отдельным рантаймом;
- любая сложность, не требующая кода, старается выражаться схемой БД и транзакциями.
Такой подход уменьшает барьер входа: легче прочитать и понять тысячу строк кода, чем десятки тысяч. А главное - снижает риск того, что библиотека начнёт "жить собственной жизнью", не вписываясь в остальную инфраструктуру.
Open source сегодня: имеет ли всё это смысл
Наконец, Армин поднимает более общий вопрос: насколько вообще имеет смысл выпускать подобные вещи в открытый доступ. Код Absurd распространяется под лицензией Apache 2.0, то есть его можно свободно использовать, модифицировать и встраивать в коммерческие продукты.
В эпоху, когда крупные компании выстраивают закрытые платформы, подобные проекты показывают альтернативный путь:
- компактное, понятное ядро;
- фокус на интеграции с уже существующими инструментами (в данном случае - Postgres);
- готовность делиться реальными отчётами эксплуатации, включая проблемы и ограничения.
Пять месяцев в продакшене - не окончательный вердикт, но важная отметка: система перестала быть экспериментом и стала живым инструментом, которым можно пользоваться.
Выводы: когда стоит присмотреться к Absurd
Absurd - это попытка сделать durable execution максимально доступным:
- один SQL-файл с хранимыми процедурами;
- тонкий SDK на TypeScript и Python, плюс экспериментальный Go;
- никаких отдельных сервисов, рантаймов и плагинов компилятора;
- Postgres как единая точка истины для всех состояний задач.
Если вам близка идея пошагового выполнения с чекпоинтами, но тяжёлые платформы кажутся избыточными, Absurd даёт третий путь: встроить execution-движок прямо в вашу базу. Пять месяцев реальной нагрузки показали, что такая архитектура не только возможна, но и практична - при условии аккуратной эксплуатации, продуманного habitat вокруг воркеров и готовности закрывать технические углы вроде партиционирования.
Дальше судьбу системы во многом определит сообщество пользователей и готовность команд строить на этой идее свои процессы. Но уже сейчас видно: durable execution не обязан быть монструозным сервисом. Иногда достаточно одной базы данных, хорошей схемы и полутора тысяч строк кода.



