# Honker - SQLite에 Postgres NOTIFY/LISTEN을 구현하는 확장

> Clean Markdown view of GeekNews topic #28855. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=28855](https://news.hada.io/topic?id=28855)
- GeekNews Markdown: [https://news.hada.io/topic/28855.md](https://news.hada.io/topic/28855.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-04-25T04:44:53+09:00
- Updated: 2026-04-25T04:44:53+09:00
- Original source: [github.com/russellromney](https://github.com/russellromney/honker)
- Points: 1
- Comments: 1

## Topic Body

- SQLite 파일 하나에 **내구성 큐, 스트림, pub/sub, 스케줄러**를 통합해 Redis·Celery 같은 별도 브로커 없이 비동기 작업 처리 가능  
- `PRAGMA data_version`을 **1ms 간격으로 폴링**해 프로세스 간 단일 자릿수 밀리초 반응 속도 달성, 애플리케이션 레벨 폴링이나 데몬 불필요  
- `notify()`, `stream()`, `queue()`는 모두 호출자의 **트랜잭션 안에서 기록**되며, 비즈니스 쓰기와 함께 커밋되거나 함께 롤백돼 dual-write 문제를 줄여줌  
- 작업 큐는 재시도, 우선순위, 지연 실행, dead-letter, scheduler, named lock, rate limiting을 포함하고, 스트림은 소비자별 오프셋을 저장하는 **at-least-once 전달**을 지원  
- SQLite를 주 저장소로 쓰는 환경에서 애플리케이션과 비동기 처리를 **한 데이터베이스 파일**로 묶어 운영 복잡도를 낮출수 있음  
- 세 가지 핵심 프리미티브 제공  
  - **queue()**: at-least-once 작업 큐 — 재시도, 우선순위, 지연 작업, dead-letter, visibility timeout  
  - **stream()**: 내구성 pub/sub — 컨슈머별 오프셋 추적, at-least-once 리플레이  
  - **notify()**: 일시적 pub/sub — fire-and-forget, 히스토리 리플레이 없음  
- Huey 스타일 **`@queue.task()` 데코레이터**로 함수를 큐 작업으로 변환, `crontab()` 기반 주기적 작업 + 리더 선출 스케줄러 지원  
- 큐 스키마는 `_honker_live` 테이블에 **partial index** 적용, claim은 `UPDATE … RETURNING` 하나, ack은 `DELETE` 하나로 처리해 dead 행 수와 무관한 일정 성능  
- SQLite 로드 가능 확장(`libhonker_ext`)으로 **모든 SQLite 3.9+ 클라이언트**에서 동일 테이블 접근 — Python 워커가 다른 언어에서 푸시한 작업을 클레임 가능  
- SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, **Ecto 등 주요 ORM과 연동** 가이드 제공  
- SIGKILL 중 트랜잭션도 SQLite ACID로 안전, 워커 크래시 시 visibility timeout 만료 후 자동 재클레임  
- Python, Node.js, Rust, Go, Ruby, Bun, Elixir, C++ **8개 언어 바인딩** 제공, 각각 PyPI·npm·crates.io·Hex·RubyGems로 독립 게시  
- Rust로 구현(honker-core + honker-extension)  
- Apache 2.0 라이선스

## Comments



### Comment 56256

- Author: neo
- Created: 2026-04-25T04:44:54+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=47874647) 
- 이걸 직접 만들었음. **Honker**는 SQLite에 **cross-process NOTIFY/LISTEN**를 붙여서, 데몬이나 브로커 없이 기존 SQLite 파일만으로 한 자릿수 ms 지연의 push 스타일 이벤트 전달을 해줌  
  SQLite는 Postgres처럼 서버가 없어서, 일정 주기로 쿼리하는 대신 WAL 파일에 대한 가벼운 `stat(2)`로 폴링 소스를 옮긴 게 핵심임. SQLite는 작은 쿼리를 많이 날려도 효율적이라([https://www.sqlite.org/np1queryprob.html](https://www.sqlite.org/np1queryprob.html)) 엄청난 업그레이드라고 하긴 어렵지만, WAL을 감시하고 SQLite 함수를 호출하기만 하면 되니 언어에 구애받지 않는다는 점이 흥미로움  
  여기에 ephemeral pub/sub, 재시도와 dead-letter가 있는 durable work queue, 소비자별 오프셋을 가진 event stream도 올려둠. 셋 다 기존 앱의 `.db` 파일 안의 row라서 비즈니스 쓰기와 **원자적으로 커밋**할 수 있고, 롤백되면 둘 다 함께 사라짐  
  원래는 litenotify/joblite였는데 `honker.dev`를 장난처럼 사두고 보니 Oban, pg-boss, Huey, RabbitMQ, Celery, Sidekiq처럼 다들 이름이 우스꽝스러워서 그냥 이 이름으로 감. 유용하거나 적어도 웃기면 좋겠고, 알파 소프트웨어라는 경고는 그대로 적용됨
  - 이건 주로 **프로세스 기반 동시성**만 다루기 쉬운 언어를 위한 용도로 보임  
    Java/Go/Clojure/C# 같은 쪽에서는 SQLite가 어차피 single writer라서, 애플리케이션이 그 writer를 관리하면서 언어 차원의 concurrent queue로 어떤 쓰기가 일어났는지 알고 관련 스레드들만 깨우는 편이 더 단순하고 깔끔해 보임  
    그래도 WAL을 이런 식으로 창의적으로 활용한 건 재미있고, Python/JS/TS/Ruby처럼 프로세스 기반 동시성이 흔한 언어에서는 notify 메커니즘으로 꽤 잘 맞아 보임
  - **1ms마다 stat()** 해도 생각보다 매우 저렴하다는 걸 이번에 알게 됐음  
    내 하드웨어에서는 호출당 1μs도 안 걸려서, 이 정도 폴링이면 CPU 사용률이 0.1%도 안 됨
  - 내가 뭔가 놓친 걸 수도 있지만, `stat(2)`보다 **`PRAGMA data_version`** 이 더 낫지 않나 싶음  
    [https://sqlite.org/pragma.html#pragma_data_version](https://sqlite.org/pragma.html#pragma_data_version)  
    C API라면 더 직접적인 `SQLITE_FCNTL_DATA_VERSION`도 있음  
    [https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion](https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion)
  - 꽤 멋짐. 나도 비슷한 걸 반쯤 만든 적 있음  
    이걸 **가벼운 Kafka**처럼 영속 메시지 스트림으로도 쓸 수 있는지 궁금함. 특정 topic에 대해 어떤 timestamp부터 과거+실시간 메시지를 전부 replay하는 식의 의미론도 가능한지 궁금함  
    pub/sub처럼 폴링으로도 흉내는 낼 수 있겠지만, 말한 대로 최적은 아닐 듯함
  - **subscriber 상태**도 함께 저장하면 더 좋아질 수 있을 듯함  
    읽기 위치, queue 이름, 필터 같은 걸 저장해두면 `stat(2)` 변화 때마다 모든 subscription thread를 깨워서 각자 N=1 SELECT를 하게 하는 대신, polling thread가 `Events INNER JOIN Subscribers`를 해서 실제로 매칭되는 subscriber만 깨울 수 있음

- 피드백 고마움. 제안들을 반영한 PR을 올렸음  
  [https://github.com/russellromney/honker/pulls/1](https://github.com/russellromney/honker/pulls/1)  
  이제 **3계층 폴링 구조**로 바뀜: 1ms마다 `PRAGMA data_version`, 100ms마다 `stat`, 그리고 오류 시 재연결 처리임  
  1) 1ms마다 `PRAGMA data_version`을 써서 기존 `stat` 기반 size/mtime 변경 감지를 대체했음. SQLite 자체의 commit counter라 monotonic하고, clock skew 영향도 없고, WAL truncation이나 rollback도 제대로 처리함. 약 3µs짜리 nonblocking query고, 성능 때문이 아니라 **정확성** 때문에 바꿨음. 오히려 약간 더 느림. truncation 위험도 생각보다 현실적이었음  
  테스트해보니 C API의 `SQLITE_FCNTL_DATA_VERSION`은 connection 간에는 동작하지 않았음. 그래서 지금은 여전히 VFS layer를 거치는 비용을 내고 있고, 그 tradeoff를 명시적으로 받아들이는 상태임  
  2) `data_version` 쿼리가 실패하면 디스크 일시 오류, NFS hiccup, connection corruption 같은 경우를 가정해 재연결을 시도하고, 예방 차원에서 subscriber도 깨움  
  3) 100ms마다 `stat`으로 `(dev, ino)`를 startup 시점 값과 비교해서 **파일 교체**를 잡아냄. atomic rename, litestream restore, volume remount 같은 경우인데, `data_version`은 열린 fd를 따라가므로 파일이 바뀌어도 원래 inode를 계속 보기 때문에 이걸 못 잡음  
  덕분에 Honker가 더 나아졌고 나도 배운 게 많았음
- 슬쩍 홍보하자면, 다가오는 **PostgreSQL 19**에서는 **LISTEN/NOTIFY**가 selective signaling에서 훨씬 잘 스케일하도록 최적화됐음  
  많은 backend가 서로 다른 channel을 listen하는 경우를 겨냥한 패치임  
  [https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9](https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9)
  - 좋은 홍보였고 주제와도 아주 잘 맞음

- 폴링 없이 **inotify**나 크로스플랫폼 wrapper로 WAL 변경을 감시하면 안 되나 싶음
  - **크로스플랫폼**이 깨짐. 특히 Mac에서는 조용히 삼켜버리는 경우가 있어서 신뢰하기 어려움  
    `stat`은 그냥 어디서나 동작함

- 별도 IPC보다 매력적인 건 **비즈니스 데이터와 원자 커밋**된다는 점임  
  외부 메시지 전달은 늘 "알림은 갔는데 트랜잭션은 롤백됨" 문제가 있고, 이게 금방 지저분해짐  
  하나 궁금한 건 WAL checkpoint임. SQLite가 WAL을 다시 0으로 truncate할 때 `stat()` 폴링이 그걸 제대로 처리하는지 모르겠음. 이벤트를 놓치는 구간이 있을 것 같다는 느낌이 듦
  - **원자성**이 사실상 전부라고 봄  
    예전에 Postgres+SQS 조합에서 enqueue를 다른 connection에 commit이 보이기 전에 trigger로 날려버리는 바람에 고생했음. retry logic을 붙이고, worker 쪽 폴링도 넣고, 결국 enqueue를 transaction 안으로 옮겼는데, 그러고 나면 결국 Honker가 하는 걸 더 많은 moving part로 다시 만드는 셈이었음  
    "notification은 갔는데 row는 아직 commit 안 됨"류 버그는 대개 조용하고 타이밍 의존적이라 추적이 정말 괴로움
  - WAL 파일은 남아 있고 truncate만 되니 그 자체로 update로 잡히긴 함  
    다만 이 부분 테스트는 아직 없어서 확인은 더 해야겠음. 좋은 포인트라 챙겨보겠음

- 고마움  
  SQLite 기반의 작은 앱이 많이 늘어났고, 대부분 queue와 scheduler가 필요함  
  직접 몇 가지를 굴려보긴 했지만 늘 **Postgres 계열 솔루션**의 우아함이 아쉬웠음  
  이건 곧바로 한 번 써볼 생각임
  - **작은 증식**이라는 표현이 내 사이드프로젝트 습관이 만든 군집을 설명하기에 딱 맞음  
    문제 부딪히면 repo에 PR이나 issue 남겨주면 좋겠음

- 여기서 **kqueue/FSEvents**를 쓰고 싶어지긴 하는데, Darwin은 같은 프로세스의 알림을 떨어뜨린다고 알고 있음  
  publisher와 listener가 같은 프로세스면 listener가 아예 안 깨어나는 경우가 있어서 추적하기 꽤 지저분함. `stat` 폴링은 못생겨 보여도 결국 어디서나 실제로 동작하는 건 이쪽 같음  
  WAL checkpoint 때 파일이 다시 줄어들면 wakeup이 발생하는지, 아니면 poller가 size 감소를 필터링하는지도 궁금함
  - 이 댓글은 **완전히 틀렸음**  
    kqueue의 VNODE 이벤트는 그 프로세스가 파일에 접근 권한만 있으면 전달되고, 같은 프로세스라고 걸러지는 필터는 없음
  - 이건 실제로 테스트가 필요함  
    확인해보고 다시 알려주겠음

- 아주 멋짐. 부하가 걸렸을 때 병목이 주로 **SQLite write throughput**인지, 아니면 **WAL notification layer**인지 궁금함
  - 병목은 쓰기와 claim/ack 흐름 쪽임  
    journal mode와 synchronous mode에 따라 많이 달라지기도 함  
    notification은 예전 `stat(2)` 방식이든 새 `PRAGMA` 기반이든 매우 저렴함. 다른 댓글에서도 `stat(2)`가 대략 1µs 수준이라고 했음

- 좋은 프로젝트임. 나도 SQLite를 보통 쓰임새보다 훨씬 더 밀어붙이는 걸 만들고 있음  
  SQLite가 실제로 어디까지 할 수 있는지 더 많은 사람이 탐색하는 걸 보니 고무적임

- **SQLAlchemy**를 쓰는 경우에도 통합 가능한지 궁금함  
  지금 모습만 보면 DB connection을 스스로 만들려는 것처럼 보임
