TRENDSHIFT DEEP DIVE · russellromney/honker

honker — SQLite 한 파일에
"Postgres NOTIFY"를 심다

메시지 큐 쓰려면 Redis와 Celery를 또 깔아야 했다. honker는 "이미 있는 SQLite 파일 안에 큐를 같이 두면 되지 않냐"는 단순한 발상으로 출발한 SQLite 확장 + 다국어 바인딩 프로젝트다.

0먼저, 이게 다 무슨 말인가

SQLite, 큐, NOTIFY/LISTEN — 기본 단어부터.

honker를 이해하려면 세 가지 단어를 먼저 한 번씩 풀고 가야 합니다. SQLite, 메시지 큐, 그리고 NOTIFY/LISTEN. 셋 다 백엔드 개발에서 자주 등장하지만, 셋이 한 자리에 모이는 경우는 드물어서 처음 보면 어색합니다.

용어 1
SQLite (에스큐엘라이트)
서버 없이 파일 하나로 돌아가는 관계형 데이터베이스. MySQL이나 PostgreSQL은 별도 서버 프로세스를 띄워야 하지만, SQLite는 app.db 같은 파일 하나가 곧 데이터베이스 전체입니다. 휴대폰 앱, 브라우저, 운영체제 안에 이미 셀 수도 없이 깔려 있는, 세상에서 가장 많이 배포된 DB라고 해도 과언이 아닙니다.
용어 2
메시지 큐 / 태스크 큐 (message queue, task queue)
"이 일은 나중에 누가 처리해줘"라고 쪽지를 던져두는 줄. 예를 들어 회원가입 직후 환영 메일을 보내는 작업은 사용자가 기다릴 필요가 없는 일이라 별도 워커(worker, 작업자)에게 떠넘깁니다. 그 쪽지를 잠시 보관하고 순서대로 꺼내주는 자료구조가 큐.
용어 3
NOTIFY / LISTEN (노티파이 / 리슨)
Postgres가 가진 기능으로, "어떤 채널에 알림을 던지면" 그 채널을 듣고 있던 다른 프로세스가 실시간으로 깨어나는 푸시 알림 방식. 매초 "할 일 있나?" 묻는 폴링(polling)과 반대되는, 사건 발생 즉시 통보받는 방식입니다.

이 셋을 합치면 honker의 한 줄짜리 자기소개가 됩니다 — "SQLite 한 파일 안에 큐와 NOTIFY/LISTEN을 통째로 욱여넣은 확장". 어떤 언어든 SQLite만 쓸 수 있으면 같은 기능을 빌려 쓸 수 있게 설계됐습니다.

사무실 비유

일반적인 구성: 우리 회사 서류는 캐비닛(SQLite)에 보관, 업무 지시는 별도 게시판(Redis)에 붙임. 캐비닛 정리하는 사람과 게시판 보는 사람이 다르고, 게시판이 망가지면 업무가 멈춥니다.

honker가 제안하는 구성: 캐비닛 안 맨 뒷칸에 "오늘 할 일" 폴더를 같이 넣어둡니다. 서류 보관과 업무 지시가 같은 잠금장치를 공유하니, 거래 기록을 넣는 그 순간 메일 발송 지시도 같이 들어가고, 만약 거래가 취소되면 메일 지시도 함께 사라집니다.

1한 줄 요약

이 프로젝트가 던지는 핵심 메시지.

핵심 메시지

"SQLite가 메인 DB라면,
큐도 같은 파일 안에 둬라."

지금까지 SQLite 기반 앱이 큐가 필요하면 "Redis + Celery 깔자"가 정답이었습니다. 데이터스토어가 둘이 되고, 백업 정책도 둘이 되고, 비즈니스 테이블과 큐 사이에 이중 기록(dual-write) 문제가 생기고, 브로커 서버를 운영해야 합니다.

honker는 정반대 접근입니다 — 큐 자체를 SQLite 안의 한 테이블로 두면, INSERT INTO ordersqueue.enqueue(...)가 같은 트랜잭션에서 커밋됩니다. 롤백하면 둘 다 사라집니다. 외부 의존성 없이 "한 파일 + WAL 사이드카" 구조로 모든 게 끝납니다.

2왜 지금 주목받는가

기존 방식의 통증을 정확히 짚는다.

"SQLite는 임베디드 DB일 뿐"이라는 오래된 인식이 깨지고 있습니다. litestream(SQLite를 S3로 실시간 복제하는 도구), turso(엣지 SQLite 호스팅), cloudflare D1 같은 흐름이 동시에 등장하면서, "SQLite로도 진짜 서비스 만들 수 있다"는 사례가 늘었습니다. 그러나 진짜 서비스에는 항상 두 가지가 더 필요합니다 — 발행/구독(pub/sub)태스크 큐.

기존 방식의 함정 1
이중 기록 문제 (dual-write problem)

주문을 DB에 저장 → 그다음 Redis 큐에 "메일 보내기" 작업을 넣는다. 그런데 DB 저장은 성공하고 Redis 호출이 실패하면? 주문은 들어왔는데 환영 메일은 영영 안 갑니다. 반대로 DB 저장 후 앱이 죽으면 큐에는 작업이 안 들어갑니다.

기존 방식의 함정 2
백업·복구 정책이 둘로 쪼개진다

DB 백업은 매일 새벽 3시, Redis는 메모리에 있고 별도 스냅샷 정책. 장애 복구 시 두 시점이 일치하지 않으면 "주문은 있는데 큐는 비어 있는" 어색한 상태가 됩니다.

기존 방식의 함정 3
브로커 서버를 또 운영해야 한다

Redis든 RabbitMQ든, 데몬(daemon, 백그라운드 상주 프로세스)이 하나 더 늘어납니다. 모니터링·로그·메모리 한도·페일오버 — 운영 부담이 두 배. 1인 개발자나 작은 팀에겐 부담스러운 비용입니다.

honker의 답
"같은 트랜잭션 안에 넣어라"

비즈니스 INSERT와 큐 INSERT를 같은 SQLite 트랜잭션 안에 묶으면 위 셋이 한꺼번에 풀립니다. 둘 다 커밋되거나 둘 다 롤백. 백업도 파일 하나만 챙기면 됩니다. 별도 데몬도 필요 없습니다 — 워커는 SQLite의 WAL 파일이 변하는 걸 직접 감시합니다.

이 패턴은 사실 새로운 게 아닙니다. Postgres 진영에는 이미 pg-boss(Node), Oban(Elixir) 같은 "DB 안에 큐를 넣는" 라이브러리가 황금 표준으로 자리잡았습니다. honker는 그 패턴을 SQLite 세계로 가져오는 것이 목표입니다.

선행 사례 비유

Postgres 세계의 pg_notify는 SQL로 "이 채널에 신호 보내라"를 호출하면 듣고 있던 클라이언트가 깨어나는 기능입니다. 이걸 큐와 결합한 게 pg-boss, Oban이고요. honker는 이 구조 전체를 SQLite의 WAL(쓰기 선행 로그)과 stat() 시스템 콜로 재현한 작품입니다.

3기술 스택 전체 지도

Rust 코어 하나에 11개 언어 바인딩이 매달려 있다.

honker는 단일 라이브러리가 아니라 "Rust로 짠 코어를 모든 언어가 빌려 쓰는" 구조입니다. 같은 디스크 레이아웃을 한 번 Rust로 정의해두고, 각 언어 바인딩은 같은 테이블을 읽고 씁니다. 그래서 Python 워커가 Node 앱이 넣은 작업을 처리할 수 있습니다.

코어 계층 (Rust)

코어 1

honker-core

전체 시스템의 심장. Rust rlib(다른 Rust 크레이트가 의존하는 정적 라이브러리)로, SQLite 테이블 스키마 정의·WAL 감시 스레드·큐 클레임 로직 등이 모두 여기 있습니다. PyO3, napi-rs 같은 다양한 바인딩이 이 한 크레이트를 공유합니다.

의존성: rusqlite 0.39 (functions + hooks 기능), parking_lot(빠른 뮤텍스), chrono(시간), thiserror(에러 정의).
코어 2

honker-extension

SQLite loadable extension(외부에서 불러올 수 있는 동적 라이브러리, 확장자는 OS별로 .so/.dylib/.dll)으로 컴파일되는 cdylib. SQLite CLI에서 .load ./libhonker_ext 한 줄로 불러올 수 있고, 그러면 honker_claim_batch(), honker_stream_publish() 같은 SQL 함수가 등록됩니다. 언어 바인딩 없이 순수 SQL만으로 큐를 다룰 수 있게 해주는 계층.

바인딩 계층 (각 언어 패키지)

모든 바인딩은 packages/ 디렉토리 아래 in-tree 디렉토리로 들어 있으며, 같은 레포 안에서 관리됩니다. site/ 디렉토리(honker.dev 문서 사이트)만 별도 저장소(russellromney/honker-site)를 가리키는 git 서브모듈로 연결되어 있습니다. 서브모듈은 "다른 저장소를 내 저장소 안에 폴더처럼 연결해두는" git 기능입니다.

바인딩 1

Python (PyPI: honker)

가장 풍부한 바인딩. PyO3(Rust로 Python C 확장을 만드는 프레임워크)로 작성되어 있어 속도는 거의 네이티브 수준입니다. Huey나 Celery 같은 @task 데코레이터 스타일까지 제공해서, 함수 위에 줄 하나만 붙이면 그 함수 호출이 자동으로 큐로 들어갑니다.

바인딩 2

Node.js (npm: @russellthehippo/honker-node)

napi-rs(Rust로 Node 네이티브 모듈을 만드는 도구)로 빌드. 사전 빌드된 바이너리가 npm에 올라와 있어서 설치 후 별도 컴파일이 필요 없습니다.

바인딩 3

Rust / Go / Ruby / Bun / Elixir / C++ / .NET·C# / JVM / Kotlin

같은 honker-core 또는 loadable extension 위에 각 언어 관용 API를 입혀둔 얇은 래퍼. 총 11개 바인딩(Python·Node 포함)이 같은 디스크 포맷을 공유하므로, 언어가 늘어나도 호환성 깨질 위험이 낮습니다.

인프라/도구

저장소 루트는 Rust 워크스페이스(Cargo.toml)와 Python 워크스페이스(pyproject.toml)가 공존합니다. uv(Rust로 만든 빠른 Python 패키지 매니저)로 락 파일(uv.lock)을 관리하고, 빌드는 make build 한 방에 PyO3 모듈과 loadable extension을 모두 컴파일합니다. CI는 GitHub Actions, 테스트는 make test가 Rust + Python + Node를 약 10초에 통과시킵니다.

용어
cdylib vs rlib
Rust 라이브러리 두 가지 출력 형식. rlib은 "Rust 안에서만 의존성으로 쓰이는 정적 라이브러리"이고, cdylib은 "C ABI를 가진 동적 라이브러리(.so/.dll/.dylib)로, 다른 언어에서도 불러올 수 있는 형태"입니다. honker-core는 rlib(바인딩들이 Rust로 가져다 씀), honker-extension은 cdylib(SQLite가 직접 로드)입니다.

4아키텍처 심화 — 어떻게 깨우는가

honker의 진짜 마술은 "푸시 신호 없는 SQLite에 푸시를 만든 방법"이다.

Postgres에는 와이어 프로토콜(wire protocol, 서버가 클라이언트에 메시지를 보내는 네트워크 규약)이 있어 서버가 클라이언트를 깨울 수 있습니다. 하지만 SQLite는 서버가 없습니다. 그냥 파일입니다. 그러면 어떻게 "큐에 새 작업이 들어왔다"를 다른 프로세스에 알릴까요?

단순한 답의 함정
매초 SELECT 폴링

가장 쉬운 답은 "워커가 1초마다 SELECT * FROM queue WHERE state='pending' 던지고 결과 있으면 처리". 단점이 셋입니다 — (1) 대기시간이 평균 500ms 늘어남, (2) 1만 개 워커 × 1초 = 초당 1만 쿼리, (3) 아무 일도 없을 때조차 CPU/디스크가 계속 일합니다.

honker의 답 (stable backend)
PRAGMA data_version 폴링

honker의 기본(stable) 백엔드는 SQLite의 PRAGMA data_version 값을 1ms마다 읽어 카운터가 증가했는지 확인합니다. SQLite는 커밋이 일어날 때마다 이 카운터를 올리므로, honker는 카운터 변화를 감지하는 그 순간 모든 구독자를 깨웁니다. 파일시스템 메타데이터(mtime)를 건드리지 않고 DB 레벨에서 직접 확인하는 방식입니다.

선택적(source 빌드) 백엔드도 두 가지 존재합니다 — kernel-watcher(inotify/kqueue/FSEvents를 feature flag로 활성화)와 shm-fast-path(WAL 공유 메모리를 직접 읽는 방식). 이 둘은 Cargo.toml의 optional feature로 분리되어 있으며 기본 배포에는 포함되지 않습니다.

우체통 비유

폴링 방식은 1분마다 우체통을 직접 열어보는 것. 편지가 매번 없어도 매번 가야 하니 지칩니다.

honker의 data_version 방식은 우체통 옆에 작은 카운터가 달려서, 우체통 문이 한 번이라도 열리면 숫자가 1 올라갑니다. 1ms마다 카운터만 흘끔 보면 됩니다 — 카운터가 그대로면 편지도 없는 것이고, 변했으면 그제서야 문을 엽니다. "확인"은 마이크로초, "실제 읽기"는 변할 때만 일어납니다.

구체 아키텍처: SharedUpdateWatcher

honker-core 안에는 SharedUpdateWatcher라는 구조체가 있습니다. 내부적으로 poll_data_version() 함수가 1ms 간격으로 data_version 카운터를 확인하며, 데이터베이스 하나당 폴링 스레드 단 한 개가 돌면서, 구독자(워커, 리스너)가 100명이든 1000명이든 신호 하나만 받으면 바운디드 채널(bounded channel, 정해진 크기의 메시지 큐)로 전부에게 한 번에 부채살처럼 펼쳐서(fan out) 알려줍니다.

┌──────────────────────────────────────────────────────┐
│  하나의 poll_data_version() 스레드 (1ms마다 data_version 확인) │
└────────────────────┬─────────────────────────────────┘
                     │ data_version 카운터 증가 감지
                     ▼
       ┌─────────────┼─────────────┐
       ▼             ▼             ▼
   [구독자 1]    [구독자 2]    [구독자 N]
   bounded ch    bounded ch    bounded ch
       │             │             │
       ▼             ▼             ▼
   SELECT … WHERE id > last_seen
   (부분 인덱스 활용)
       │
       ▼
   yield 작업, ack 후 다시 대기

큐 스키마와 부분 인덱스

큐는 두 개의 테이블입니다 — _honker_live(처리 중 또는 대기 중)와 _honker_dead(재시도 다 소진된 실패 작업). 핵심은 부분 인덱스(partial index, 조건을 만족하는 행만 포함하는 인덱스):

(queue, priority DESC, run_at, id) WHERE state IN ('pending','processing')

완료/사망 행은 인덱스에 들어가지 않으므로, 10만 개의 데드 레터가 쌓여 있어도 클레임 속도는 큐가 비어 있을 때와 똑같습니다. 클레임은 UPDATE … RETURNING 한 번, ack는 DELETE 한 번 — 모든 핫패스가 O(1)에 가깝습니다.

트랜잭셔널 아웃박스가 공짜로

현대 마이크로서비스 패턴 중 가장 유명한 게 트랜잭셔널 아웃박스(transactional outbox)입니다. "비즈니스 테이블 옆에 outbox 테이블을 두고, 이벤트도 같은 트랜잭션으로 INSERT, 별도 디스패처가 outbox 읽어서 메시지 큐로 보낸다"는 패턴이죠. honker에서는:

with db.transaction() as tx:
    tx.execute("INSERT INTO orders (user_id) VALUES (?)", [42])
    emails.enqueue({"to": "alice@example.com"}, tx=tx)
    stream.publish({"event": "order_created"}, tx=tx)

주문 저장, 이메일 큐 등록, 이벤트 스트림 발행이 한 트랜잭션 안에서 원자적으로 처리됩니다. 별도 outbox 테이블도, 별도 디스패처 프로세스도 필요 없습니다. "부수효과 행 자체가 곧 커밋된 행"이고, WAL을 보고 있던 누구나 1ms 안에 알아챕니다.

왜 WAL이 필수인가

용어
WAL (Write-Ahead Logging, 쓰기 선행 로깅)
DB가 데이터 파일을 직접 수정하기 전에 별도 로그 파일에 변경사항을 먼저 쓰는 방식. SQLite에서는 PRAGMA journal_mode=WAL로 켜며, 그러면 메인 .db 옆에 .db-wal.db-shm 파일이 생깁니다. 읽기와 쓰기가 서로 막지 않는다는 게 가장 큰 장점.

honker는 WAL 모드를 강제합니다. WAL 없이는 DELETE 저널 모드(기본값)에서 모든 쓰기가 EXCLUSIVE 락을 잡는데, 그러면 워커 한 명이 클레임하는 동안 모든 enqueue가 줄을 서야 합니다. 또 .db-wal 파일은 커밋마다 증가하고 체크포인트에서만 줄어들기 때문에 단조 증가하는 신호로 쓰기 좋습니다.

왜 기본값이 PRAGMA data_version인가

리눅스의 inotify, 맥의 FSEvents, BSD의 kqueue — 모두 커널이 파일 변화를 직접 푸시해주는 더 우아한 방법입니다. 그런데 honker의 stable backend는 이들 대신 PRAGMA data_version 폴링을 씁니다. 이유는 커널 파일 이벤트가 같은 프로세스 안의 쓰기를 누락할 수 있기 때문입니다. 한 Python 프로세스 안에 enqueuer와 listener가 같이 있으면 서로를 못 봅니다. data_version은 OS 무관하게 SQLite 레벨에서 ~1ms 정확도로 안정 작동하고, 비용은 약 0.5ms 추가 지연뿐입니다. kernel-watcher(inotify/kqueue)와 shm-fast-path는 optional source-build feature로 선택적으로 활성화할 수 있습니다.

5디렉토리 구조 해부

레포 안에 뭐가 어디에 있는지.

honker 레포의 폴더 구조는 "코어는 in-tree, 바인딩은 submodule"이라는 원칙으로 깔끔하게 갈라져 있습니다.

honker/
├── honker-core/          ← [핵심] Rust rlib. 모든 바인딩의 공통 토대
├── honker-extension/     ← [핵심] SQLite loadable extension (cdylib)
├── packages/
│   ├── honker/           ← Python (PyO3)
│   ├── honker-node/      ← Node.js (napi-rs)
│   ├── honker-rs/        ← Rust 친화 래퍼
│   ├── honker-go/        ← Go
│   ├── honker-ruby/      ← Ruby
│   ├── honker-bun/       ← Bun
│   ├── honker-ex/        ← Elixir
│   ├── honker-cpp/       ← C++
│   ├── honker-dotnet/    ← .NET/C#
│   ├── honker-jvm/       ← JVM
│   └── honker-kotlin/    ← Kotlin
├── tests/                ← 크로스 언어 통합 테스트
├── bench/                ← wake_latency, real_bench 등 성능 측정
├── site/                 ← honker.dev 문서 사이트 (Astro)  [git submodule]
├── Cargo.toml            ← Rust 워크스페이스 정의
├── pyproject.toml        ← Python 워크스페이스
├── Makefile              ← make test / make build 진입점
├── ROADMAP.md            ← Phase 단위로 정리된 향후 작업
└── CHANGELOG.md          ← 출시된 변경사항

주목할 디테일 셋:

디테일 1

코어 vs 바인딩 분리

honker-corehonker-extension은 메인 레포에 직접 들어 있습니다. 모든 바인딩이 의존하는 토대이기 때문에 일관성이 절대 필요합니다. 언어별 바인딩도 packages/ 아래 in-tree 디렉토리로 같은 레포에서 관리됩니다. 각자의 패키지 저장소(PyPI, npm, crates.io 등)로의 발행은 별도로 이루어지지만, 소스 코드는 단일 레포 안에 있습니다. site/만 예외적으로 별도 저장소를 가리키는 git 서브모듈입니다.

디테일 2

tests/ — 크로스 언어 통합 테스트

같은 .db 파일을 Python으로 쓰고 Node로 읽거나, Ruby로 쓰고 Python으로 읽는 시나리오를 검증합니다. test_extension_interop.py는 스키마 호환성을 핀으로 박아두는 일종의 계약 테스트입니다.

디테일 3

bench/ — 실제 측정값

"빠르다"고 주장하기 전에 측정합니다. wake_latency_bench.py는 커밋 → 워커 깨어남 지연을 재고, real_bench.py는 워커 N명·인큐어 M명의 처리량을 잽니다. M1/M2 맥북에서 중앙값 1~2ms 정도 나옵니다.

6이 레포에서 배울 수 있는 것

honker는 그 자체로도 좋지만, 코드를 읽으면 더 좋은 학습 재료다.

honker 코드는 분량이 적당하고(Python ~27% + Rust ~24%, 나머지는 C#·Go·JS·Elixir·C++·Ruby 등 다수 언어, 약 290커밋 이상) 의도가 명확해서 다음 주제들을 실전 코드로 익히기에 적합합니다.

SQLite를 깊이 이해하기

WAL 모드의 진짜 의미, PRAGMA(SQLite 설정 명령) 활용, partial index의 위력, loadable extension 작성법 — honker는 이 모든 걸 사용합니다. 특히 wal_autocheckpoint를 10000 페이지로 설정해서 fsync(디스크 동기화) 호출을 줄이고, 그게 성능의 대부분을 차지한다는 분석은 따로 글을 써도 될 만한 통찰입니다.

Rust에서 다중 언어 바인딩 만들기

한 번 Rust로 짠 코드를 PyO3로 Python에, napi-rs로 Node에, 그대로 cdylib으로 빌드해 SQLite에 — 같은 코어를 여러 ABI로 노출하는 모범 사례를 통째로 볼 수 있습니다. ABI(Application Binary Interface, 바이너리 인터페이스)는 "컴파일된 코드끼리 어떻게 함수를 호출하는가"에 대한 약속입니다.

"폴링 없이 푸시" 만들기

이 패턴은 분산 시스템에서 끝없이 반복됩니다. 카프카, NATS, Postgres NOTIFY, Linux inotify — 모두 같은 문제를 다른 방법으로 풉니다. honker는 아무 동기화 원시(primitive)도 없는 SQLite에서 푸시 비슷한 걸 어떻게 만들 수 있는가에 대한 한 가지 답입니다. 이 사고방식은 다른 도구를 다룰 때도 응용됩니다.

워크스페이스 빌드 시스템

Cargo 워크스페이스 + uv + Makefile의 조합으로 다언어 모노레포(monorepo, 단일 저장소에 여러 프로젝트)를 깔끔하게 관리하는 법. 서브모듈을 적극 활용하면서도 핵심은 in-tree로 두는 결정이 흥미롭습니다.

로드맵 읽는 법

ROADMAP.md는 단순한 TODO가 아니라 Phase <Name> 형식으로 정리되어 있습니다. 번호 대신 이름을 쓰는 이유는 "중간에 새 작업을 끼워넣어도 번호를 다시 매기지 않아도 되기 때문"입니다. 큰 오픈소스 프로젝트의 계획 관리 방식을 엿보기 좋습니다.

7시스템 요구사항과 제약

honker가 안 맞는 상황을 솔직히 짚는다.

좋은 도구라도 모든 상황에 맞지는 않습니다. honker의 설계는 다음을 전제합니다.

제약 1
단일 머신, 단일 작성자

SQLite의 락은 한 호스트 안에서만 동작합니다. 두 서버가 NFS(네트워크 파일 시스템)로 같은 .db를 동시에 쓰면 파일이 깨집니다. honker는 한 머신 위에서만 의미가 있습니다. 여러 머신이 필요하면 파일 단위로 샤딩하거나 Postgres로 옮겨야 합니다.

제약 2
WAL 모드 강제

journal_mode = WAL이 켜져 있어야만 동작합니다. WAL 사이드카 파일이 생기면 안 되는 환경(공유 파일시스템 백업 대상 등)에선 쓸 수 없습니다.

제약 3
"과잉 트리거" 트레이드오프

WAL이 변하면 모든 구독자가 깨어납니다. 본인 채널의 일이 아니어도 일단 SELECT가 한 번 일어납니다. 이걸 over-triggering이라 부르고, honker는 일부러 이걸 받아들입니다 — "한 명이 놓치는 것보다 열 명이 헛깨는 게 낫다"가 명시적 철학입니다.

honker가 잘 맞는 상황
한 머신 위에서 SQLite로 끝내고 싶을 때

소규모 SaaS, 개인 프로젝트, 엣지 배포, 자체 호스팅 도구, 노트북에서 도는 백그라운드 작업러너 등. 별도 인프라 없이 "파일 하나 = 시스템 전체"를 유지하고 싶을 때 압도적으로 편합니다.

8직접 해볼 만한 실습 과제

난이도별로 손에 잡히는 시작점을 제시한다.

난이도 ★ — 입문

1) Hello queue: 메일 발송 시뮬레이션

Python으로 pip install honker 후, "이메일 보내는 함수"를 큐로 처리해봅니다. print(payload)만 하는 가짜 발송 함수면 충분합니다. 한 터미널에서 enqueue를 N번 호출하고, 다른 터미널에서 워커를 띄워 작업이 차곡차곡 처리되는 걸 관찰합니다.

목표: enqueue → claim → ack의 한 사이클을 눈으로 확인. 워커를 중간에 SIGKILL하면 visibility_timeout(기본 300초) 후 다른 워커가 다시 가져오는지 검증.
난이도 ★★ — 응용

2) 트랜잭셔널 아웃박스 패턴 직접 구현

"주문 생성 → 환영 이메일 큐 등록 → 분석 이벤트 스트림 발행"을 같은 트랜잭션에 묶어봅니다. 그다음 raise Exception()으로 일부러 롤백을 발생시키고, 세 개가 모두 사라지는지 확인합니다.

목표: outbox 패턴이 honker에선 라이브러리 없이 "그냥" 되는 걸 체감. Celery + Redis로 같은 걸 만들면 얼마나 많은 코드가 필요한지 비교해보기.
난이도 ★★ — 응용

3) Python 발행 → Node 구독 크로스 언어 테스트

같은 app.db 파일을 Python에서 열어 db.queue("emails").enqueue(...)로 작업을 넣고, Node.js에서 @honker/node로 같은 큐를 claim 합니다. 디스크 레이아웃이 정말 공유되는지 직접 확인.

목표: 다언어 ABI 호환이 어떻게 보장되는지 감 잡기. JSON 페이로드를 한쪽이 잘못 넣으면 어떻게 깨지는지도 일부러 시도.
난이도 ★★★ — 심화

4) wake_latency_bench 직접 돌리고 결과 해석하기

python bench/wake_latency_bench.py --samples 500를 본인 머신에서 실행하고, 1ms·5ms·99percentile 지연이 어느 정도 나오는지 측정. PRAGMA wal_autocheckpoint 값을 1000 / 10000 / 100000으로 바꿔보며 처리량이 어떻게 달라지는지 비교.

목표: "체크포인트당 fsync"가 왜 성능의 핵심 변수인지 손으로 확인. 디스크 캐시·SSD 모델별 차이도 함께 관찰.
난이도 ★★★ — 심화

5) loadable extension을 빌드해 SQLite CLI에서만 큐 다루기

Python 바인딩 없이 cargo build -p honker-extension --releaselibhonker_ext만 빌드하고, sqlite3 CLI에서 .load로 불러서 순수 SQL로 enqueue/claim을 실행해봅니다. SELECT honker_claim_batch('emails', 'me', 10, 300); 같은 SQL이 어떤 JSON을 반환하는지 직접 관찰.

목표: 언어 바인딩의 마법을 한 겹 벗기고, "결국 SQL 함수들의 묶음"이라는 본질을 확인.

9관련 기술 심화 로드맵

honker를 이해하면서 함께 익혀두면 좋은 주제들.

1~2주차

SQLite 본격 입문

"SQLite The Definitive Guide", 공식 문서의 WAL 페이지, PRAGMA 목록을 한 바퀴. 핵심은 (1) WAL이 왜 동시성에 좋은가, (2) checkpoint 메커니즘, (3) busy_timeout과 락 대기. litestream 문서도 함께 보면 SQLite를 프로덕션에서 쓰는 감각이 생깁니다.

3~4주차

메시지 큐 패턴

at-least-once vs at-most-once vs exactly-once 의미를 정확히 구별하기. visibility timeout, dead letter queue, exponential backoff, idempotency 키 — 모두 honker가 다루는 개념입니다. AWS SQS 문서가 이 용어들의 표준 정의를 잘 풀어놓고 있습니다.

5~6주차

Postgres NOTIFY/LISTEN과 pg-boss / Oban

honker가 베껴오려는 원본을 직접 만져봅니다. LISTEN channel; -- 또 다른 세션에서 -- NOTIFY channel, 'hello';를 psql로 실행해보고, pg-boss 또는 Oban의 README를 정독. 같은 문제를 Postgres 진영은 어떻게 풀었는지 비교하면 honker의 디자인 결정이 또렷해집니다.

7~8주차

Rust + PyO3 / napi-rs로 다언어 라이브러리 만들기

"오늘의 환율" 같은 작은 함수를 Rust로 짜서 PyO3와 napi-rs로 동시에 배포해봅니다. honker가 어떻게 한 코어를 여러 ABI로 노출하는지 손으로 따라할 수 있습니다.

10핵심 키워드 사전

이 글에 처음 나온 용어를 한 곳에 모아둔다.

WAL
Write-Ahead Logging
변경을 로그에 먼저 쓰고 나중에 본 파일에 반영하는 방식. SQLite WAL 모드의 사이드카 파일이 honker의 "신호"가 됩니다.
NOTIFY/LISTEN
Postgres의 pub/sub 메커니즘
한 세션이 채널에 알림을 발행하면 그 채널을 LISTEN 중인 세션이 깨어남. honker가 SQLite에서 흉내내고 싶어하는 동작.
transactional outbox
트랜잭셔널 아웃박스 패턴
비즈니스 INSERT와 이벤트 발행을 같은 트랜잭션에 묶어 "둘 다 커밋 또는 둘 다 롤백"을 보장하는 패턴. honker에선 라이브러리 없이 자동으로 적용.
visibility timeout
가시성 타임아웃
워커가 작업을 가져가놓고 ack 안 하면 N초 후 다른 워커가 다시 가져가는 메커니즘. honker 기본값은 300초.
partial index
부분 인덱스
WHERE 조건을 만족하는 행만 포함하는 인덱스. honker는 state IN ('pending','processing')인 행만 색인해서, 데드 레터가 쌓여도 클레임 속도가 느려지지 않게 합니다.
loadable extension
SQLite 로드 가능 확장
SQL .load 명령으로 동적으로 불러올 수 있는 동적 라이브러리. honker-extension이 이 형식으로 빌드되어, 언어 바인딩 없이 순수 SQL로도 큐를 다룰 수 있게 합니다.
PyO3 / napi-rs
Rust ↔ Python / Node 브리지
Rust 함수를 Python 또는 Node에서 호출 가능한 형태로 만들어주는 프레임워크. honker의 다언어 바인딩 인프라.
stat(2)
파일 메타정보 조회 시스템 콜
파일 크기·수정시각 등을 OS에서 가져오는 호출. 매우 가볍고 OS 무관하게 동작해서, honker는 이걸 1ms마다 호출해 WAL 변화를 감지합니다.
시도해볼 만한 것

오늘 한 시간 안에 해볼 수 있는 일

  1. 로컬에 설치하고 enqueue → claim 한 사이클을 돌려본다. pip install honker, README의 "At a glance" 예제 7줄을 그대로 실행. 두 터미널에 enqueuer와 worker를 띄우는 것만으로 메시지 큐의 기본 흐름이 손에 잡힌다.
  2. SQLite 파일을 GUI 툴로 열어 _honker_live 테이블을 직접 들여다본다. DB Browser for SQLite 같은 무료 도구로 .db 파일을 열어 큐가 진짜 "그냥 행 N개"라는 걸 눈으로 확인. payload, state, attempts 같은 컬럼이 어떻게 변하는지 관찰하면 추상이 사라진다.
  3. 이미 쓰던 작은 사이드 프로젝트의 "Redis + Celery"를 honker로 교체해본다. 운영 부담이 진짜 줄어드는지, 백업이 진짜 단순해지는지를 본인 코드로 검증. 단일 머신 한정이라는 제약이 본인 워크로드와 맞는지 같이 점검.
  4. ROADMAP.md를 처음부터 끝까지 읽는다. Phase Ranger, Phase Mantle, Phase Mays 같은 이름과 그 안의 결정들을 읽다 보면, 잘 관리되는 오픈소스 프로젝트가 어떻게 미래 일을 정리하는지 자연스럽게 익혀진다.
  5. honker-core의 Rust 코드 100줄만 골라 읽는다. SharedWalWatcher 또는 claim_batch 구현을 골라 한 함수만 깊이 읽어보면, "이론으로 들은 설계가 코드에서는 이렇게 생겼구나"가 명확해진다.
원문 · russellromney/honker · github.com/russellromney/honker · 공식 사이트 honker.dev · 라이선스 MIT OR Apache-2.0