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

Show HN: Honker – SQLiteのためのPostgres NOTIFY/LISTENセマンティクス

概要

honker は、SQLiteに PostgresのNOTIFY/LISTEN 機能を追加する拡張機能。 耐久性のあるpub/sub・タスクキュー・イベントストリーム を提供し、 ポーリングやブローカー不要。 Rust製SQLite拡張として実装され、 Python/Node.js/Ruby/Go/Elixir/C++ など多言語対応。 WALファイルのイベント通知 で高速なプッシュセマンティクスを実現。 RedisやCelery不要 で、業務処理とキュー操作を同一トランザクションで管理可能。

honker概要と特徴

  • SQLite用拡張機能 で、 PostgresのNOTIFY/LISTEN に近いpub/sub・キュー機能を実装
  • Rust crate (honker, honker-core, honker-extension)として提供
  • Python(honker)/Node.js(@russellthehippo/honker-node)/Bun/Ruby/Go/Elixir/C++ 向けラッパー
  • on-diskレイアウトはRustで一元管理、各言語バインディングは薄いラッパー
  • WALファイルの変更検知 でイベント発火、 シングルミリ秒台の通知遅延 を実現
  • 耐久性のあるpub/sub、タスクキュー、イベントストリーム を1つのSQLiteファイルで管理
  • ポーリング不要・デーモンや外部ブローカー不要、シンプルな運用
  • トランザクション内で業務処理とキュー投入が同時コミット、ロールバックも一括
  • 各種リトライ、優先度、遅延実行、デッドレター、レートリミット等もサポート
  • APIは実験的段階、将来的に変更の可能性

honker導入の背景とメリット

  • SQLiteがメインDB の場合、 pub/subやタスクキュー も同じファイルで完結
  • RedisやCelery等の外部システム不要、バックアップや運用負荷を削減
  • ordersテーブルへのINSERTとqueue.enqueueを同一トランザクションで管理
  • 過去事例:pg_notify(Postgres)、Huey(Python)、pg-boss/Oban(Postgres)等を参考
  • Postgres利用中ならpg_notifyやOban推奨、SQLiteユーザー向け

主要機能一覧

  • 1つの.dbファイル内でプロセス間通知(notify/listen)
  • リトライ・優先度・遅延・デッドレター付きワークキュー
  • 業務処理とキュー投入のアトミックコミット
  • ミリ秒単位の高速リアクション、ポーリング不要
  • ハンドラータイムアウト、エクスポネンシャルバックオフによるリトライ
  • 遅延ジョブ、タスク有効期限、ネームドロック、レートリミット
  • crontab式定期タスク(リーダー選出スケジューラ付き)
  • タスク結果の保存・取得(IDで管理、ワーカーが結果永続化)
  • 耐久性のあるストリーム(各コンシューマごとにオフセット管理)
  • SQLite拡張としてどのクライアントからも同一テーブル参照可能
  • Python, Node.js, Rust, Go, Ruby, Bun, Elixir向けバインディング

クイックスタート例(Python)

  • pip install honker でインストール

  • db = honker.open("app.db") でDBオープン、 emails = db.queue("emails") でキュー生成

  • トランザクション内で業務処理とキュー投入をアトミックに実行

    with db.transaction() as tx:
      tx.execute("INSERT INTO orders (user_id) VALUES (?)", [42])
      emails.enqueue({"to": "alice@example.com"}, tx=tx)
    
  • ワーカー側で非同期イテレータでジョブを取得・処理

    async for job in emails.claim("worker-1"):
      try:
        send(job.payload)
        job.ack()
      except Exception as e:
        job.retry(delay_s=60, error=str(e))
    
  • claim()はWALコミットで即時起床、WAL監視不可時のみ5秒ポーリング

  • デフォルトvisibilityは300秒、バッチ処理もclaim_batchで対応

タスクデコレータ(Python)

  • 関数を@emails.taskで装飾し、直接enqueueせずジョブ化可能

    @emails.task(retries=3, timeout_s=30)
    def send_email(to: str, subject: str) -> dict:
      ...
      return {"sent_at": time.time()}
    
  • 呼び出し側:r = send_email("alice@example.com", "Hi")、r.get(timeout=10)で結果取得

  • ワーカーはpython -m honker worker myapp.tasks:db --queue=emails --concurrency=4で起動

  • *定期タスクは@emails.periodic_task(crontab("0 3 * * "))で実装

ストリーム・通知(Python)

  • 耐久性pub/sub:db.stream("user-events")でストリーム取得、publish/subscribeで利用
  • 各コンシューマは自身のオフセットを_honker_stream_consumersで管理
  • subscribeは履歴リプレイ→WAL通知でライブ配信に移行、オフセット自動保存可
  • ephemeral pub/sub:db.listen("orders")でリスナ起動、tx.notify("orders", {...})で通知
  • 耐久性リプレイ不要ならlisten/notify、必要ならstream/publish推奨
  • 通知テーブルは自動クリーンアップされないため、db.prune_notificationsで管理

Node.js例

  • const { open } = require('@russellthehippo/honker-node')でDBオープン
  • トランザクションで業務処理+notifyをアトミックに実行
  • for await (const n of db.listen('orders'))で通知受信

SQLite拡張機能のSQL例

  • .load ./libhonker_extで拡張読込
  • SELECT honker_bootstrap()で初期化
  • INSERT INTO _honker_liveでキュー投入
  • SELECT honker_claim_batchでジョブ取得、SELECT honker_ack_batchでACK
  • 各種ロック、レートリミット、スケジューラ、ストリーム、結果保存APIも提供

設計思想とアーキテクチャ

  • Python/Node/Rust/Go/Ruby/Bun/Elixir向けバインディングを同梱
  • Huey(Python SQLiteキュー)を参考に設計
  • Postgresならpg_notify+pg-boss/Obanが相当、honkerはSQLite向け
  • 3つのプリミティブ:ephemeral pub/sub(notify)、durable pub/sub(stream)、at-least-once queue(queue)
  • 全てトランザクション内INSERTでアトミック
  • NOTIFY/LISTENセマンティクスをポーリング無しで実現、WALコミットごとに通知
  • WALモード必須(journal_mode = WAL)
  • WALファイルのstat(2)で変更検知、クロスプラットフォームで1ms精度
  • .db + .db-walのみで完結、運用・スナップショット容易
  • サーバープッシュ不可のため、ファイル変更→SELECTで通知
  • トランザクションは安価、ジョブ・イベント・通知は全てテーブルの行として管理

注意点・非対応事項

  • WALモード必須、DELETE/TRUNCATEモード非対応
  • 複数ライター複製、DAG型ワークフロー等は非対応
  • バックアップ先や共有ファイルシステム等、WALモード非推奨環境では利用不可
  • API・仕様は今後変更の可能性あり

このように、 honker はSQLite単体で 高機能なpub/sub・タスクキュー・イベントストリーム を実現するための 軽量・高性能拡張 です。 外部システム無しで、業務処理とキュー処理のアトミック性 を重視するプロジェクトに最適です。

Hackerたちの意見

みんな、これ作ったよ!HonkerはSQLiteにクロスプロセスのNOTIFY/LISTENを追加するんだ。既存のSQLiteファイルを使って、デーモンやブローカーなしで、ミリ秒単位の遅延でプッシュスタイルのイベント配信ができるよ。今、高トラフィックのアプリはFramework+SQLite+LitestreamをVPSで使ってるから、「SQLiteだけ使えばいい」っていうパーティーにちょっとした盛り上がりを持ってきたかったんだ。SQLiteはPostgresみたいにサーバーを動かさないから、トリックはSQLite接続でのインターバルクエリからWALファイルの軽量なstat(2)にポーリングソースを移すことなんだ。SQLiteでは小さなクエリが効率的だから(https://www.sqlite.org/np1queryprob.html)、これは大きなアップグレードではないけど、クロスランゲージの結果は結構面白いよ。WALファイルを聞いてSQLiteの関数を呼ぶだけだから、言語に依存しないんだ。ストア/通知のプリミティブに加えて、Honkerはエフェメラルなpub/sub(pg_notifyみたいな)、リトライやデッドレター付きの耐久性のあるワークキュー(pg-boss/Obanみたいな)、消費者ごとのオフセット付きのイベントストリームを提供するよ。これら3つはアプリの既存の.dbファイルの行として存在して、ビジネスの書き込みと原子的にコミットできるんだ。これがクールなのは、ロールバックすると両方とも消えるから。前はlitenotify/jobliteって呼ばれてたけど、彼女のためにhonker.devをジョークで買ったら、MQやタスク、ワーカーには面白い名前が多いことに気づいたんだ。Oban、pg-boss、Huey、RabbitMQ、Celery、Sidekiqとかね。だから、ちょっとしたおふざけでこの名前がついたんだ。Honkerはこれらの巨人と同じ道を歩んで、同じ虚無に向かってホーンを鳴らすよ。役に立つか、面白いと思ってもらえたら嬉しいな。標準のアルファソフトウェアの警告は適用されるよ。

名前が好き!

すごい、1msごとにstat()を呼ぶのがこんなに安いなんて知らなかった。どうやら、私のハードウェアでは1回の呼び出しに1μsもかからないから、ポーリングのCPU時間は0.1%未満だね。

これの主なユースケースは、プロセスベースの並行性しか使えない言語向けなの?JavaやGo、Clojure、C#では、SQLiteにはシングルライターがあるから、挿入や更新、変更を気にするスレッドに通知できる理由が見えにくいんだ。アプリケーションがシングルライターを管理してるから、書き込み中と何を書いたかがわかるし、その方法で通知セマンティクスを得る方がシンプルでクリーンに感じてた。とはいえ、WALをクリエイティブに使う人を見るのは楽しいね。プロセスベースの並行性しかないPythonやJS、TS、Ruby向けの通知メカニズムが見られるのはクールだよ。いい仕事だね!

面白いアイデアだね!サブスクライバーの状態も保存したら役立つかな?(読み取り位置、キュー名、フィルターなど)そうすれば、stat(2)が変わった時にすべてのサブスクリプションスレッドを起こす代わりに、ポーリングスレッドがEvents INNER JOIN Subscribersを使って、マッチするサブスクライバーだけを起こせるかも。

これめっちゃ面白い!今、PostgresqlでLISTEN/NOTIFYとPostgraphileを使って何か作ってるんだけど、理論的にはスワップ可能なバックエンドを持って、データベースサーバーにそんなに依存しないようにしたいんだよね。

何か見落としてるかも。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#sql...

すごいね!似たようなものの半端なバージョンを持ってるよ :) これを軽量のKafkaとして使える?特定のトピックのために、あるタイムスタンプからすべてのメッセージ(過去+リアルタイム)を再生するみたいなセマンティクスで?pub/subのように、ポーリングなどで再現できるけど、言う通り、それは最適じゃないよね。

kqueue/FSEventsは魅力的だけど、Darwinでは同プロセスの通知が落ちちゃうんだ。同じプロセスにパブリッシャーとリスナーがいると、リスナーは全く反応しないから、追いかけるのが厄介なんだよね。statポーリングは見た目は悪いけど、実際にどこでも動く唯一の方法なんだ。WALチェックポイントでは何が起こるの?ファイルが縮むと、それがウェイクアップを引き起こすのか、それともポーラーがサイズの減少をフィルタリングするのか?

実際にテストする必要があるね。結果は報告するよ。

inotify(または何かのクロスプラットフォームラッパー)を使って、ポーリングなしでWALの変更を監視できないの?

クロスプラットフォームが壊れる、特にMacだと静かに飲み込まれちゃう。statは普通に動くけど。

同じマシン上のプロセスは、ファイルに触れずに異なるIPCを使えるんじゃない?面白いけど、ほとんどのケースではIPCメソッドの一つにアドレスを渡す方が速い気がするし、SQLite自体は耐久性のある部分だけに必要になると思うんだ。

この拡張機能はSQLiteのネイティブトランザクションを利用してるんだ。例えば、データをキューに入れてるとき、制約違反でトランザクションがロールバックされると、そのデータもロールバックされる。外部IPCで実現することもできるけど、すごく慎重なプログラミングが必要だよ。

ビジネスデータとのアトミックコミットが、別のIPCよりも優れているポイントだね。外部メッセージパッシングは「通知は送信されたけどトランザクションがロールバックされた」って問題が常にあって、これが厄介なんだ。気になることが一つあるんだけど、WALチェックポイント。SQLiteがWALをゼロに切り詰めるとき、stat()のポーリングはそれを正しく処理できるのかな?イベントが失われる可能性がある気がする。

WALファイルは残るけど、トランケートされるからそれが更新としてカウントされる。ただ、これについてはテストしてないんだ。いい意見ありがとう、確認しておくね。

ありがとう!SQLiteをバックエンドにした小さなアプリがたくさんあるんだけど、ほとんどがキューとスケジューラーを必要としてるんだ。自分でいくつか作ったけど、Postgresのソリューションのエレガンスが恋しかった。すぐに試してみるよ!

最高だね。今、AWS SQSを使ってて、メール送信みたいな非同期タスクのためにラムダ関数を呼び出してるけど、Honkerは素晴らしいローカルの代替品みたい。Litestreamを使うときに何か問題とかある?

いや、違うよ!この拡張は生のSQLへのショートカットとして機能するだけ。Litestreamはwalファイルを編集するけど、普通のチェックポイントみたいなもんだから、そんなに悪くない。ただ、直接テストはしてないけど。多分、テストする必要があるね。

ちょっと宣伝させて!次のPostgreSQL 19のリリースでは、LISTEN/NOTIFYが最適化されて、選択的シグナルでスケールが良くなるんだ。つまり、たくさんのバックエンドが異なるチャンネルをリッスンしてるときにね。パッチ: https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit...

いいプラグインだね、すごく関連性がある。

SQLAlchemy使ってるんだけど、これって統合できるの?データベース接続を自分で作りたがってるみたい。

こういうスキルレベルのツールがもっと早くあったらよかったな。1週間前にSQLiteと静的デプロイで日次クロニクルサイトを運営してて、まさにその痛点にぶつかった。結局、単一の通知セマンティクスのためにPostgresをインストールする必要がある代替案が多くて、粗雑なポーリングループになっちゃった。質問なんだけど、1つのプロセスで10k以上の同時リスナーがいると、最初に何が壊れると思う?SQLiteがPostgresの安価な処理を維持できるか気になる。

10kリスナーは多いね。stat()でサンダリングハード問題が起きるかも。これだけの規模だとSQLiteはベストな選択じゃないかも。