2 ポイント 投稿者 GN⁺ 2026-04-25 | 1件のコメント | WhatsAppで共有
  • 1つのSQLiteファイルに耐久性キュー、ストリーム、pub/sub、スケジューラを統合し、Redis・Celeryのような別個のブローカーなしで非同期ジョブ処理が可能
  • PRAGMA data_version1ms間隔でポーリングし、プロセス間で1桁ミリ秒の反応速度を実現。アプリケーションレベルのポーリングやデーモンは不要
  • notify(), stream(), queue()はすべて呼び出し元のトランザクション内で記録され、ビジネス書き込みと一緒にコミットまたはロールバックされるため、dual-write問題を軽減
  • ジョブキューは再試行、優先度、遅延実行、dead-letter、scheduler、named lock、rate limitingを含み、ストリームはコンシューマごとのオフセットを保存するat-least-once配信をサポート
  • SQLiteを主ストレージとして使う環境では、アプリケーションと非同期処理を1つのデータベースファイルにまとめて運用の複雑さを下げられる
  • 3つの中核プリミティブを提供
    • 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 … RETURNING1回、ackはDELETE1回で処理するため、dead行数に関係なく一定の性能
  • SQLiteのロード可能拡張(libhonker_ext)として提供され、すべてのSQLite 3.9+クライアントで同一テーブルにアクセス可能 — Pythonワーカーが他言語からpushされたジョブをclaim可能
  • SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, Ectoなど主要ORMと連携するガイドを提供
  • SIGKILL中のトランザクションもSQLite ACIDで安全であり、ワーカークラッシュ時はvisibility timeout満了後に自動で再claim
  • Python, Node.js, Rust, Go, Ruby, Bun, Elixir, C++の8言語バインディングを提供し、それぞれPyPI・npm・crates.io・Hex・RubyGemsで個別公開
  • Rustで実装(honker-core + honker-extension)
  • Apache 2.0ライセンス

1件のコメント

 
GN⁺ 2026-04-25
Hacker Newsのコメント
  • これを自分で作った。Honker は SQLite に プロセス間の NOTIFY/LISTEN を追加し、デーモンやブローカーなしで既存の SQLite ファイルだけを使って、1桁 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 も載せている。3つとも既存アプリの .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 回あたり 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 の破損などを想定して再接続を試み、予防的に 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 parts で作り直しているだけだった
      「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 を自前で作ろうとしているように見える