honker добавляет в SQLite очередь задач и pub/sub без Redis
Очередь задач и pub/sub для SQLite-приложения — теперь без Redis и Celery. honker кладёт всё прямо в .db-файл, где уже лежат ваши данные, и коммитит задачи в одной транзакции с бизнес-записью.
Новости TprogerОчередь фоновых задач и pub/sub — классические аргументы, чтобы рядом с SQLite-приложением поставить Redis и Celery. Расширение honker кладёт очередь, pub/sub и durable-стримы в тот же .db-файл, в котором уже лежат ваши данные, и коммитит их в одной транзакции с бизнес-записью. По сути автор переносит на SQLite привычную Postgres-связку NOTIFY/LISTEN — механизм, когда слушатели подписываются на именованный канал и получают события без опроса.
Ключевые факты
Что такое honker за 30 секунд
- SQLite-расширение на Rust плюс биндинги для Python, Node.js, Rust, Go, Ruby, Bun, Elixir и C++. Ставится через
pip install honkerили из любого SQLite-клиента черезSELECT load_extension('honker')— новый язык подключается одной SQL-командой, без ожидания биндинга. - Три примитива в одном файле БД:
notify()для мгновенных событий без истории, durable-стрим с оффсетами (не теряется при рестарте) и очередь задач с at-least-once-доставкой (минимум один раз — значит обработчик должен быть идемпотентным), retries и dead-letter-таблицей. - Бизнес-запись и постановка задачи коммитятся в одной транзакции SQLite — при rollback пропадают вместе. Это транзакционный outbox по умолчанию: события и данные либо оба попадают в БД, либо оба откатываются, без сторонних библиотек и диспетчеров.
- Cross-process уведомления идут за единицы миллисекунд: воркеры не опрашивают очередь, а ловят любой коммит в БД через
PRAGMA data_version. - Лицензия Apache 2.0, статус «экспериментальный» — API ещё может меняться, в продакшен тащить стоит с оглядкой.
Зачем это нужно, если есть Redis
Обычный ответ на «в SQLite-приложении нужна очередь задач» — добавить Redis и Celery. Это работает, но в обмен вы получаете второй сервис: его приходится отдельно бэкапить, следить, чтобы запись в бизнес-таблицу и постановка задачи в очередь не разъехались при падении, и держать брокер в живом состоянии. Автор honker Рассел Ромни предлагает другой подход: если SQLite — основная база, очередь должна жить в том же файле.
В коде это выглядит так: INSERT INTO orders и queue.enqueue(...) коммитятся в одной транзакции. Rollback откатывает обе операции. Очередь — это просто строки в таблице с частичным индексом по состоянию, без дополнительного диспетчера или отдельной таблицы очередей. Это классический паттерн transactional outbox — когда события и бизнес-запись кладутся в одну транзакцию, — но по умолчанию и без сторонних библиотек.
Как уведомления приходят за миллисекунду
У SQLite нет сетевого протокола, поэтому серверного push быть не может — клиент должен сам читать. honker обходит это через PRAGMA data_version — монотонный счётчик, который SQLite увеличивает на каждом коммите из любого соединения. Один поток в процессе опрашивает его каждую миллисекунду; сам запрос стоит единицы микросекунд, так что 1000 проверок в секунду — это миллисекунды CPU, а не проценты. Счётчик меняется в любом режиме журнала и виден между процессами, поэтому работает одинаково на Linux, macOS и Windows.
Когда счётчик сдвинулся, подписчики просыпаются и делают один короткий SELECT ... WHERE id > last_seen по частичному индексу. Часть из них ничего полезного не найдёт — их канал не менялся. Автор сознательно оставляет эти лишние пробуждения: отфильтровать в SELECT дешевле, чем пропустить нужное событие. Итог — end-to-end задержка доставки между процессами по медиане одна–две миллисекунды на современном ноутбуке.
Три примитива в одном файле БД
Как выглядит минимальный пример на Python — постановка задачи в очередь атомарно с бизнес-записью:
notify — мгновенный pub/sub без истории. Слушатели подключаются к db.listen("orders") и получают новые события с момента подписки. История до старта слушателя не воспроизводится — offline-подписчик пропустит удалённые записи. Полезно, когда важно низколатентное «что-то произошло» без гарантии доставки.
stream — durable-pub/sub с гарантиями: события переживают рестарт воркера. Каждый именованный потребитель ведёт собственный offset в таблице _honker_stream_consumers, поэтому после рестарта продолжает с места остановки. Доставка at-least-once; оффсет можно сохранять автоматически каждые N событий или M секунд, а можно вручную через save_offset в той же транзакции, что и ваша запись, — тогда offset и бизнес-изменение либо оба закоммитятся, либо оба откатятся.
queue — собственно очередь задач. Захват строки воркером — это UPDATE ... RETURNING по частичному индексу, подтверждение — один DELETE. Если воркер упал в середине обработки, взятая им задача считается протухшей по visibility-таймауту (по умолчанию пять минут на обработку) и возвращается в очередь — другой воркер её переподхватит. После трёх неудачных попыток задача уезжает в таблицу _honker_dead, которую претенденты не сканируют. Благодаря этому горячий путь не зависит от истории очереди.
Что уже встроено
Что honker умеет помимо трёх базовых примитивов:
- Cross-process pub/sub на одном файле БД.
- Очередь с приоритетами, отложенными задачами, декларативными retries и экспоненциальным backoff.
- Dead-letter-таблица для исчерпавших попытки задач.
- Таймауты обработчиков, время жизни задач, именованные локи и rate-limiting.
- Crontab-подобные периодические задачи с выбором лидера-планировщика.
- Опциональное хранение результатов:
enqueueвозвращает id, воркер пишет ответ, вызывающий ждёт его черезqueue.wait_result(id). - Durable-стримы с per-consumer оффсетами и настраиваемым интервалом флаша.
- Работа внутри соединения, которым владеет ORM: SQLAlchemy, SQLModel, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, Ecto.
В README явно сказано, чего в honker специально не будет: пайплайнов/цепочек/групп задач (как у Celery), multi-writer-репликации и оркестрации workflow через DAG. Автор берёт за образец Postgres-мир — pg-boss, Oban, pg_notify — и SQLite-проект Huey.
Где придётся остановиться
Первое ограничение — архитектурное: SQLite рассчитан на один файл, одного писателя и один хост. Два сервера, пишущие в один .db через NFS, рискуют получить повреждённую базу. Если нужна репликация между машинами, вариантов два: либо шардировать по файлам (отдельная .db на клиента или инстанс сервиса), либо переходить на Postgres.
Рекомендуемый режим журнала — WAL (Write-Ahead Log, отдельный файл журнала рядом с базой): он даёт конкурентных читателей при одном писателе и эффективный батчинг fsync. Другие режимы работают, но теряется именно конкурентное чтение при записи. Корректность wake-сигнала от PRAGMA data_version от WAL не зависит.
FAQ
Подходит ли honker для продакшена?
Автор сам помечает проект как экспериментальный — API ещё может меняться. Тесты на crash recovery уже есть, но рекомендуется использовать с оглядкой и следить за релизами. Для критичных нагрузок в Postgres-мире устоявшиеся pg-boss и Oban остаются более безопасным выбором.
Как это соотносится с Celery или RQ?
Celery и RQ — очереди поверх Redis/RabbitMQ. honker — очередь поверх SQLite без отдельного брокера. Выигрыш — один датастор, транзакционный outbox из коробки и задержка в миллисекунды. Цена — single-machine single-writer и отсутствие цепочек/DAG-оркестрации.
Что с multi-machine?
Multi-writer-репликация осознанно не входит в планы. Для нескольких серверов автор советует шардировать по файлам или переходить на Postgres.
Не дорого ли опрашивать базу каждую миллисекунду?
Один PRAGMA data_version — около 3,5 мкс. Даже на частоте 1000 запросов в секунду это 3,5 мс CPU в секунду на базу, а не на подписчика. Сто слушателей делят один поток опроса. Альтернативы — stat(2) на WAL-файле или ядерные watcher-ы (inotify / FSEvents / kqueue) — имеют свои баги: stat сбоит, когда WAL обнуляется и снова дорастает до прежнего размера, а FSEvents на macOS пропускает записи от того же процесса. Счётчик SQLite этих проблем лишён.
Можно ли использовать из моей ORM?
Да, расширение грузится на соединение, которым управляет ORM. В README есть примеры для SQLAlchemy, SQLModel, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord и Ecto. Ключевая идея — вызвать SQL-функцию honker_enqueue внутри той же транзакции, в которой идёт бизнес-запись.
Если у вас монорепа с SQLite, stand-alone-приложение или небольшой SaaS на одном-двух серверах — honker закрывает тот самый класс задач, ради которого обычно тянут Redis: очередь, pub/sub, периодические задачи, распределённые локи и rate-limiting. Если у вас уже кластер из десятка нод — это не для вас: SQLite упрётся в одного писателя и один хост. Код и документация — в репозитории на GitHub, обсуждение собралось на Hacker News, архитектуру разобрал Саймон Уиллисон у себя в блоге.