ハクソク

世界を動かす技術を、日本語で。

Honker – SQLiteファイル内の耐久性のあるキュー、ストリーム、パブ/サブ、及びcronスケジューラー

概要

honkerは、SQLiteファイル内で耐障害性キュー、ストリーム、pub/sub、cronスケジューラを提供。
Postgres風のNOTIFY/LISTENを実現し、追加ミドルウェアやデーモン不要
複数言語対応で同一ファイル・同一フォーマットを共有。
トランザクション一貫性で、ビジネスデータとキュー操作を同時に管理可能。
運用コスト削減低レイテンシを両立。

honker:SQLiteで実現する耐障害性キュー・ストリーム・Pub/Sub・Cron

  • honkerは、SQLiteファイルに耐障害性キュー、ストリーム、pub/sub、cronスケジューラを統合
  • PostgresのNOTIFY/LISTENのような通知セマンティクスをSQLiteにもたらす
  • クライアント側ポーリング不要デーモンやブローカー不要の設計
  • クロスプロセスのウェイクレイテンシは**約0.7ms(MシリーズMac)**の高速応答
  • SQLite拡張として提供、どの言語でもSELECT load_extension('honker_ext')で利用可能
  • Python、Node、Rust、Go、Ruby、Bun、Elixirバインディングが共通オンディスクフォーマットを共有

SQLiteを主データストアとする利点

  • Bluesky’s PDS、Fly’s LiteFS、Tursoなど実運用例多数
  • SQLite利用アプリでは耐障害性キューが不可欠
  • 従来はRedis+Celeryなどを追加導入し、二重管理や運用負荷増加が発生
  • honkerは「キューも同一SQLiteファイル内」という設計思想
    • INSERT INTO ordersqueue.enqueue(...)同一トランザクションで実行
    • ロールバック時は両方とも巻き戻し、一貫性確保
    • キューはテーブルの行として管理、パーシャルインデックスで高速アクセス

マルチ言語・シンプルなAPI

  • 同一.dbファイル・同一フォーマット7言語サポート
    • Python、Node、Rust、Go、Ruby、Bun、Elixir、C++、SQL(拡張)
  • Python例
    • 同一トランザクションでビジネスデータとキュー投入
    • ワーカーはコミット時に自動ウェイクポーリング不要
    • Huey風デコレータでタスク定義・再試行・タイムアウトも簡単

内部動作とパフォーマンス

  • PRAGMA data_version1msごとにポーリング
    • SQLiteのコミットごとにインクリメントされる単調カウンタを利用
    • 3μsで読めるため高効率なウェイク信号
    • 1データベース1ポーラースレッド、リスナー数が増えても負荷一定
    • SELECTのみページキャッシュ負荷・ロック競合なし
    • リスナー数増加によるスケールも容易

ACIDトランザクションと拡張性

  • キューやストリーム、pub/sub拡張が管理するテーブルへのINSERT
  • queue.enqueue(payload, tx=tx)ビジネスロジックと同一トランザクション
  • ロールバック時はジョブも同時に消去、整合性維持
  • pg_notifyは高速だがリトライや可視性なし
  • Huey(SQLiteバックエンドPythonタスクキュー)から多くを参考
  • pg-boss、ObanはPostgres向け標準、Postgres利用者はそちら推奨

導入方法

  • Python例:pip install honker
  • **Node、Rust、Go、Ruby、Bun、Elixir、C++**等でも利用可能
  • SQLite拡張として既存ワークフローに容易に統合

honker導入による運用コスト削減と一貫性の強化

  • 追加ミドルウェア不要による運用・保守コスト削減
  • ビジネスデータとキューの同時管理データ一貫性を実現
  • 低レイテンシ・高スケーラビリティ本番運用にも最適
  • SQLiteを主データストアとするアプリにおける耐障害性キューの最適解

Hackerたちの意見

数日前の議論: https://news.ycombinator.com/item?id=47874647
面白いアプローチだし、新しいプロジェクトに使うと結構楽しいかもね。 > どうやって動くかというと、honkerはSQLiteのPRAGMAデータバージョンを毎ミリ秒チェックするんだ。これは、SQLiteがどの接続、ジャーナルモード、プロセスからでもコミットするたびに増加する単調カウンターで、正確なウェイク信号のために約3µsの読み取りが必要なんだ。
最後にこう書いてあるね: "pg-bossとObanはPostgres側のゴールドスタンダード" でも、Obanは今SQLiteもサポートしてるよ。 https://github.com/oban-bg/oban
あと、Graphile Workerってのもあるよ。 https://github.com/graphile/worker
"アイドルコストは、データベースごとに毎ミリ秒1回の軽量SELECT" って言ってるけど、これを書いたLLMはちょっと行き過ぎたと思う。忙しくSELECTをポーリングするのが、"カーネルファイルウォッチャー"より良いとは思えないんだよね。
"毎ミリ秒1回の軽量SELECT" って、ちょっと妊娠したって言ったティーンエイジャーを思い出すな。
うん、同じ直感があったよ。これは「いいアイデア」って感じだけど、実行がイマイチだね。つまり、こんな風にSQLiteを叩くくらいなら、もうRedis使った方がいいんじゃない?
データベースに変更を加えてないなら、SELECTって意味あるの?変更してるなら、ファイルウォッチャーが起きた後はポーリングしなきゃいけないんじゃない?WALモードだと、SQLiteは共有メモリをちょっと見ればこのクエリに応えられると思うけど、確かに忙しい待機状態だね。
俺には、カーネルファイルウォッチャーを作らないように頼まれたみたいに聞こえるけど、実装には全く関係ないのに、どこにでもそのことを書いてるのが気になる。
> 1ミリ秒ごとに軽量なSELECT 一分あたりたったの1ドルで、スーパーカーをリースできるよ。
敬意を表して(ありがとう、笑) - そうかもしれないですね。最初の意図はinotify的なものを使うことでしたが、最初からプラットフォームごとの違いを避けました。これは間違いなく楽しみのためのプロジェクトで、意図せず大きくなってしまったので、強化や改善に取り組んでいます。Flyが大好きです。
RailsのLitestackを思い出すな。結局、Rails自体がSQLiteに全力を注ぎ始めたから、放棄されたんだよね。 https://github.com/oldmoe/litestack
すべてにおいて*
過去に似たようなことをinotifyを使って実装したことがあるけど、-walファイルのIN_MODIFYを監視する必要があるんだ。ちゃんと動かすためには、BEGIN IMMEDIATE TRANSACTION; ROLLBACK; を実行しないと、新しい変更がプロセスに見えないことがあったよ。もっとターゲットを絞ったアプローチがあると思うけど、-shmファイルの特定のバイトに対してflockを使うとかね。
> 本当にSQLiteをバックエンドにしたアプリで実作業が流れると、キューが必要になる。普通の答えは「Redis + Celeryを追加しろ」ってことだけど、冗談だよね?SQLiteは通常、シングルプロセス(複数スレッド)アプリケーションに使われる。スレッドやプロセス間で通信する正しい方法はリングバッファで、構造体を割り当てて(通常はポインタをインクリメントする)、通知にはfutex/eventfdを使う(タスクがすぐに来るときはカーネルに行かないようにスピンロックも使う)。それにRedisが必要な理由は何?永続的なタスクが必要なら、テーブルに保存して、通知にはfutexを使えばいいじゃん。このポーリングは非効率的だし、他の怠けた開発者がアプリに追加するライブラリにすべきじゃない。> honkerはSQLiteのPRAGMA data_versionを毎ミリ秒ポーリングしてる。それは、SQLiteがどの接続からでもコミットするたびにインクリメントする単調カウンターで、ジャーナルモードやプロセスも含まれる — 精密なウェイク信号のための約3µsの読み取り。これは1秒あたり3ms = 待機しているスレッドごとに0.3%のCPU時間が無駄になる。Electronみたいに、これを書いたのは本物のプログラマーじゃなくてウェブ開発者って感じがする。
それでも、「Redisクラスターをこのシンプルな拡張に置き換えたら、N倍速くなった」みたいな記事が出てくると思うよ。
> これは1秒あたり3ms = 待機しているスレッドごとに0.3%のCPU時間が無駄になる。実際には「プロセスごと、データベースごと(通常は1)」の話だと思うし、スレッドやテーブルの数には基づいてないんじゃないかな。`data_version`のセマンティクスからすると、ポーリングするのは1つの接続だけで十分だし、比較的軽量な「DBが変更された、キューをチェックしろ」っていうチェックとして使われてる(これがほぼ全目的だと思う)。それに、これは主にマルチプロセス用に意図されてると思うから、プロセス内のダーティトラッカー(例えば、挿入/更新/削除の後にチェックするだけ)じゃ不十分だと思う。だから、ちょっとクレイジーだとは思うけど、少なくともすごくシンプルだね。fsnotifyのような監視はかなり明白な改善点だと思うけど、なんでそれが含まれてないのかは分からない。もしかしたら遅いのかな?fs通知で実際にパフォーマンスが良くて信頼性のあることをやったことがないから、どんな罠が待ってるかは分からないけど。
作者です - 以前ここに投稿しました: https://news.ycombinator.com/item?id=47874647 SQLのポーリングとの大きな違いは、データページではなくメタデータに触れていることです。軽量ではあるものの、元のstat(2)の方向性が信頼できなかったので、ポーリングなしで動作させるための作業を進めています(inotify、kqueue、mmapされたshmファイルチェック)。フィードバックやリポジトリへの貢献を大歓迎です。まだ最終的な形を模索中です。
SQLiteの大ファンなんだけど、もしSQLiteが単一のライタープロセスに制約するなら、アプリケーション層でこれをやった方が良くない?