1P by GN⁺ 1일전 | ★ favorite | 댓글 1개
  • SQLite 파일 하나에 내구성 큐, 스트림, pub/sub, 스케줄러를 통합해 Redis·Celery 같은 별도 브로커 없이 비동기 작업 처리 가능
  • PRAGMA data_version1ms 간격으로 폴링해 프로세스 간 단일 자릿수 밀리초 반응 속도 달성, 애플리케이션 레벨 폴링이나 데몬 불필요
  • 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 라이선스
Hacker News 의견들
  • 이걸 직접 만들었음. Honker는 SQLite에 cross-process NOTIFY/LISTEN를 붙여서, 데몬이나 브로커 없이 기존 SQLite 파일만으로 한 자릿수 ms 지연의 push 스타일 이벤트 전달을 해줌
    SQLite는 Postgres처럼 서버가 없어서, 일정 주기로 쿼리하는 대신 WAL 파일에 대한 가벼운 stat(2)로 폴링 소스를 옮긴 게 핵심임. SQLite는 작은 쿼리를 많이 날려도 효율적이라(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
      C API라면 더 직접적인 SQLITE_FCNTL_DATA_VERSION도 있음
      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
    이제 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

    • 좋은 홍보였고 주제와도 아주 잘 맞음
  • 폴링 없이 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을 스스로 만들려는 것처럼 보임