1 poin oleh GN⁺ 11 jam lalu | 1 komentar | Bagikan ke WhatsApp
  • Melalui ekstensi SQLite dan berbagai binding bahasa, ini memungkinkan pub/sub durable, antrean kerja, dan stream event ditangani bersama dalam file .db yang sama tanpa polling klien atau daemon·broker terpisah
  • notify(), stream(), dan queue() semuanya dicatat di dalam transaksi milik pemanggil, lalu di-commit bersama penulisan bisnis atau di-rollback bersama, sehingga mengurangi masalah dual-write
  • Mekanisme membangunkan antarproses berjalan dengan memeriksa PRAGMA **data_version** setiap 1ms, ditujukan untuk mencapai latensi tingkat milidetik satu digit dan biaya query yang sangat kecil
  • Antrean kerja mencakup retry, prioritas, eksekusi tertunda, dead-letter, scheduler, named lock, dan rate limiting, sementara stream mendukung pengiriman at-least-once dengan menyimpan offset per konsumen
  • Untuk lingkungan yang menggunakan SQLite sebagai penyimpanan utama, ini adalah konfigurasi yang menyatukan aplikasi dan pemrosesan asinkron dalam satu file database untuk menurunkan kompleksitas operasional, dan API-nya masih berstatus Experimental

Ikhtisar

  • Dengan ekstensi SQLite dan berbagai binding bahasa, ini menambahkan perilaku NOTIFY/LISTEN ala Postgres ke SQLite, serta memungkinkan pub/sub durable, antrean kerja, dan stream event ditangani dalam file .db yang sama tanpa polling klien atau daemon·broker terpisah
  • Berdasarkan layout on-disk yang didefinisikan sekali di Rust, binding Python, Node, Bun, Ruby, Go, Elixir, dan C++ disusun sebagai pembungkus tipis di atas loadable extension yang sama
  • Pendekatan membaca database setiap 1ms menggantikan polling di level aplikasi, dan biaya kueri PRAGMA data_version berada di tingkat mikrodetik satu digit sementara pengiriman notifikasi antarproses berada di tingkat milidetik satu digit
  • Jika SQLite digunakan sebagai penyimpanan utama, penulisan bisnis dan pemuatan ke antrean dapat di-commit atau di-rollback dalam transaksi yang sama, sehingga mengurangi kebutuhan mengoperasikan datastore terpisah dan masalah dual-write
  • API masih berstatus Experimental dan dapat berubah
  • Dijelaskan secara tegas bahwa bila Anda sudah menjalankan Postgres, menggunakan pg_notify, pg-boss, atau Oban akan lebih sesuai

Fitur utama

  • Menyediakan notify/listen antarproses, antrean kerja dengan retry, prioritas, eksekusi tertunda, dan tabel dead-letter, serta durable stream dengan offset per konsumen dalam satu file .db
  • Semua operasi send dapat digabungkan secara atomik dengan penulisan bisnis sehingga di-commit bersama atau di-rollback bersama
  • Waktu respons lintas proses berada di tingkat milidetik satu digit, dan juga mencakup handler timeout, retry berbasis exponential backoff, delayed jobs, task expiration, named lock, dan rate limiting
  • Juga mendukung scheduler berbasis leader election, periodic task bergaya crontab, serta penyimpanan hasil task secara opt-in
    • enqueue mengembalikan id, worker menyimpan nilai balik, dan pemanggil dapat menunggu hasil dengan queue.wait_result(id)
  • Disediakan dalam bentuk SQLite loadable extension sehingga klien SQLite apa pun dapat membaca tabel yang sama
  • Juga berjalan di dalam koneksi SQLite yang dimiliki ORM, dan panduan ORM membahas integrasi dengan SQLAlchemy, SQLModel, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, dan Ecto
  • Sebaliknya, cakupan yang sengaja tidak dimasukkan juga dijelaskan dengan jelas
    • task pipeline, chain, group, dan chord tidak didukung
    • replikasi multi-writer tidak didukung
    • orkestrasi workflow berbasis DAG tidak didukung

Mulai cepat

  • Queue Python

    • Buka database dengan honker.open("app.db") dan dapatkan queue seperti db.queue("emails") untuk mengantrekan dan mengonsumsi pekerjaan
    • Di dalam blok with db.transaction() as tx:, jika INSERT pesanan dan emails.enqueue(..., tx=tx) dijalankan bersama, penulisan pesanan dan pengantrean tugas email akan terikat dalam transaksi yang sama
    • Worker mengambil pekerjaan satu per satu dalam bentuk async for job in emails.claim("worker-1"): lalu memprosesnya dengan job.ack() saat berhasil, atau job.retry(delay_s=60, error=str(e)) saat gagal
    • claim() adalah iterator asinkron yang secara internal memanggil claim_batch(worker_id, 1) pada setiap iterasi
    • Ia akan bangun pada commit apa pun di database, dan hanya kembali ke paranoia poll 5 detik jika commit watcher tidak bisa berfungsi
    • Untuk pekerjaan batch, penggunaan dipisahkan agar langsung memakai claim_batch(worker_id, n) dan queue.ack_batch(ids, worker_id), dengan visibility default 300 detik
  • Task Python

    • Dengan dekorator @emails.task(retries=3, timeout_s=30), pemanggilan fungsi akan langsung diubah menjadi enqueue ke queue dan mengembalikan TaskResult
    • Dari sisi pemanggil, ini bisa digunakan seperti send_email("alice@example.com", "Hi"), lalu menunggu hasil eksekusi worker dengan r.get(timeout=10)
    • Worker bisa dijalankan sebagai proses terpisah atau in-process, misalnya python -m honker worker myapp.tasks:db --queue=emails --concurrency=4
    • Nama otomatis adalah {module}.{qualname}, dan di lingkungan produksi disarankan memakai nama eksplisit seperti @emails.task(name="...") agar job pending tidak menjadi yatim karena perubahan nama
    • Periodic task menggunakan bentuk @emails.periodic_task(crontab("0 3 * * *"))
    • Contoh lebih lengkap ada di packages/honker/examples/tasks.py
  • Stream Python

    • db.stream("user-events") menyediakan pub/sub yang durable, dan UPDATE bisnis serta stream.publish(..., tx=tx) bisa dijalankan dalam transaksi yang sama
    • Jika berlangganan dengan async for event in stream.subscribe(consumer="dashboard"): maka baris setelah offset yang tersimpan akan diputar ulang terlebih dahulu, lalu setelah itu beralih ke pengiriman real-time berbasis commit
    • Offset untuk tiap named consumer disimpan di tabel _honker_stream_consumers
    • Penyimpanan offset otomatis secara default hanya dilakukan setiap 1000 event atau sekali per 1 detik, agar slot single-writer tidak terlalu sering dihantam bahkan pada throughput tinggi
    • Ini bisa diatur dengan save_every_n= dan save_every_s=; jika keduanya diatur ke 0, penyimpanan otomatis dimatikan dan stream.save_offset(consumer, offset, tx=tx) bisa dipanggil secara langsung
    • Jika terjadi crash, event in-flight setelah offset terakhir yang sudah di-flush akan dikirim ulang, mengikuti model at-least-once
  • Notify Python

    • Berlangganan pub/sub ephemeral dengan async for n in db.listen("orders"): dan kirim notifikasi di dalam transaksi dengan tx.notify("orders", {"id": 42})
    • Listener saat ini mulai dari titik MAX(id), sehingga riwayat lama tidak diputar ulang
    • Jika membutuhkan replay yang durable, gunakan db.stream()
    • Tabel notifications tidak dibersihkan secara otomatis, sehingga pada pekerjaan terjadwal perlu memanggil db.prune_notifications(older_than_s=…, max_keep=…)
    • Payload task harus valid sebagai JSON, dan writer Python serta reader Node dapat berbagi channel yang sama
  • Node.js

    • Di binding Node juga tersedia pola fungsi yang sama, seperti open('app.db'), db.transaction(), tx.notify(...), dan db.listen('orders')
    • Penulisan bisnis dan notify terikat pada commit yang sama, dan listen akan bangun pada commit apa pun di database lalu memfilter berdasarkan channel
  • Ekstensi SQLite

    • Setelah .load ./libhonker_ext, inisialisasi dengan SELECT honker_bootstrap();, lalu fitur queue, lock, rate limit, scheduler, stream, dan penyimpanan hasil bisa digunakan hanya dengan fungsi SQL
    • Tersedia fungsi seperti honker_claim_batch, honker_ack_batch, honker_sweep_expired, honker_lock_acquire, honker_rate_limit_try, honker_scheduler_tick, honker_stream_publish, honker_stream_read_since, dan honker_result_save
    • Binding Python dan extension berbagi _honker_live, _honker_dead, dan _honker_notifications, sehingga pekerjaan yang dimasukkan bahasa lain lewat extension bisa diambil oleh worker Python
    • Kompatibilitas skema dikunci di tests/test_extension_interop.py

Desain

  • Repositori ini mencakup loadable extension SQLite honker bersama binding Python, Node, Rust, Go, Ruby, Bun, dan Elixir
  • Ditujukan untuk aplikasi yang menggunakan SQLite sebagai penyimpanan utama, dengan fokus memindahkan package logic ke SQLite extension agar bisa dipakai dengan cara yang serupa di berbagai bahasa dan framework
  • Ada tiga primitive inti
    • notify() sebagai pub/sub ephemeral
    • stream() sebagai pub/sub durable dengan offset per konsumen
    • queue() sebagai work queue at-least-once
  • Ketiga primitive ini semuanya dicatat sebagai INSERT di dalam transaksi pemanggil, sehingga pengiriman pekerjaan dan penulisan bisnis akan di-commit bersama atau di-rollback bersama
  • Tujuannya adalah mewujudkan perilaku mirip NOTIFY/LISTEN tanpa polling di level aplikasi agar waktu respons tetap cepat
  • Jika file SQLite yang ada dipakai apa adanya, semua commit di database akan membangunkan worker, dan sebagian besar trigger mungkin hanya akan membaca pesan atau queue lalu berakhir dengan hasil kosong tanpa pemrosesan nyata
  • Overtriggering ini adalah tradeoff yang disengaja, dipilih demi perilaku yang lebih mendekati push dan waktu respons yang cepat

Default WAL yang direkomendasikan

  • Language binding secara default menggunakan journal_mode = WAL, yang menyediakan struktur reader konkuren dan single writer, batching fsync yang efisien, serta pengaturan wal_autocheckpoint = 10000
  • Mode lain seperti DELETE, TRUNCATE, dan MEMORY juga berfungsi, dan deteksi commit dilakukan berdasarkan PRAGMA data_version yang meningkat di semua journal mode
  • Yang hilang di mode non-WAL hanyalah karakteristik write saat read bersamaan; correctness dan wake antarproses sendiri tidak bergantung pada WAL
  • Seluruh sistem terdiri dari satu file .db, dan saat WAL diaktifkan, sidecar .db-wal dan .db-shm dapat ditambahkan
  • Claim ditangani dengan satu kali UPDATE … RETURNING melalui partial index, dan ack dengan satu kali DELETE
  • Pada journal mode apa pun, hanya ada satu writer pada satu waktu, dan keuntungan concurrent reader disediakan oleh WAL
  • PRAGMA data_version meningkat pada setiap commit dan checkpoint, sehingga situasi seperti WAL truncation, pembuatan dan penghapusan file journal, serta reuse dengan ukuran yang sama juga ditangani dengan benar
  • SQLite tidak memiliki wire protocol, jadi server push tidak dimungkinkan; konsumen harus memulai pembacaan sendiri
    • Sinyal wake adalah kenaikan counter
    • Setelah itu pengambilan aktual dilakukan dengan SELECT
  • Karena transaksi itu murah, jobs, events, dan notifications dicatat seperti pola outbox di dalam blok with db.transaction() yang sudah dibuka oleh pemanggil
  • Alih-alih memakai cara melihat ukuran file WAL·mtime lewat stat(2) atau kernel watcher seperti FSEvents·inotify·kqueue, digunakan PRAGMA data_version
    • data_version adalah counter monotonic yang dinaikkan SQLite untuk commit dari koneksi mana pun
    • Menangani dengan benar WAL truncation, clock skew, dan transaksi yang di-rollback
    • Kernel watcher di macOS bisa melewatkan write dari proses yang sama, dan stat(2) berbasis (size, mtime) bisa melewatkan commit ketika WAL di-truncate lalu membesar lagi ke ukuran yang sama
    • Bekerja sama di Linux, macOS, dan Windows, dan biaya CPU pada resolusi tingkat 1ms sangat kecil
    • Disebutkan bahwa biaya per kueri sekitar 3.5µs, atau total sekitar 3.5ms/detik pada 1kHz
  • Model lock SQLite mengasumsikan single machine, single writer, dan jika dua server menulis ke .db yang sama di atas NFS, akan terjadi korupsi
    • Untuk kasus seperti ini diperlukan sharding per file atau beralih ke Postgres

Arsitektur

  • Jalur wake

    • Untuk setiap Database, ada satu PRAGMA poll thread yang memeriksa data_version setiap 1ms
    • Saat counter berubah, tick di-fan-out ke bounded channel milik tiap subscriber
    • Tiap subscriber menjalankan SELECT … WHERE id > last_seen yang memanfaatkan partial index dan mengembalikan baris baru, lalu menunggu lagi
    • Bahkan jika ada 100 subscriber, cukup satu poll thread
    • Listener idle sama sekali tidak menjalankan kueri SQL
    • Biaya idle hanyalah satu kueri PRAGMA data_version per database setiap 1ms, dan jumlah listener bisa bertambah nyaris gratis berkat struktur yang memakai pembacaan counter SQLite
    • SharedWalWatcher di honker-core memiliki poll thread dan melakukan fan-out ke channel bounded SyncSender<()> per subscriber id
    • Setiap pemanggilan db.wal_events() mendaftarkan subscriber, dan handle yang dikembalikan akan otomatis unsubscribe saat Drop
    • Saat listener di-drop, rx.recv() -> Err terjadi di bridge thread dan thread itu akan membersihkan diri lalu berhenti
  • Skema queue

    • _honker_live berisi baris dengan status pending dan processing
    • Partial index berbentuk (queue, priority DESC, run_at, id) WHERE state IN ('pending','processing')
    • Claim dilakukan dengan satu kali UPDATE … RETURNING melalui indeks ini
    • Ack adalah satu kali DELETE
    • Baris yang melampaui batas retry dipindahkan ke _honker_dead dan tidak dipindai lagi di jalur claim
    • Berkat partial index pada state, hot path claim dibatasi oleh ukuran working set, bukan ukuran seluruh history
    • Bahkan jika ada 100k dead row, kecepatan claim tetap sama seperti queue tanpa dead row
  • Iterator claim

    • async for job in q.claim(id) berulang kali memanggil claim_batch(id, 1) dan mengeluarkan pekerjaan satu per satu
    • Job.ack() adalah satu DELETE di dalam transaksinya sendiri, dan nilai kembali adalah True jika claim masih valid, atau False jika visibility window telah lewat dan worker lain telah mengambil ulang
    • Akan terbangun pada commit database dari proses mana pun, dan paranoia poll 5 detik adalah satu-satunya fallback
    • Untuk pekerjaan batch harus langsung memakai claim_batch(worker_id, n) dan queue.ack_batch(ids, worker_id)
    • Library tidak menyembunyikan batch di balik iterator, agar biaya transaksi dan perilaku visibility at-most-once bisa ditangani dengan lebih jelas
  • Penggabungan transaksi

    • notify() adalah fungsi skalar SQL yang didaftarkan pada writer connection
    • Fungsi ini melakukan INSERT ke _honker_notifications di bawah transaksi terbuka milik pemanggil
    • queue.enqueue(…, tx=tx) dan stream.publish(…, tx=tx) juga bekerja dengan cara yang sama
    • Jika terjadi rollback, job, event, dan notification juga ikut hilang
    • Ini adalah pola transactional outbox bawaan, yang menangani business write dan enqueue side effect bersama tanpa instalasi library terpisah
    • Tidak ada dispatch table atau dispatcher process terpisah; row side effect itu sendiri menjadi baris yang di-commit, dan proses mana pun yang memantau database dapat mengambilnya dalam sekitar 1ms
  • Over-triggering yang lebih cepat daripada polling

    • Perubahan data_version membangunkan semua subscriber dari Database tersebut, bukan hanya membangunkan secara selektif channel yang di-commit
    • Biaya jika terbangun secara keliru hanyalah satu kali SELECT terindeks pada tingkat mikrodetik
    • Sebaliknya, jika target yang seharusnya dibangunkan terlewat, itu akan menjadi bug correctness yang diam-diam
    • Filtering channel ditangani di jalur SELECT, bukan pada tahap trigger notification
    • SQLite juga dapat menangani secara efisien pola menjalankan banyak kueri kecil
  • Kebijakan retensi

    • Pekerjaan queue tetap ada sampai di-ack, dan jika melampaui batas retry akan dipindahkan ke _honker_dead
    • Event stream dipertahankan, dan tiap named consumer melacak offset-nya sendiri
    • Notify bersifat fire-and-forget dan tidak ada pembersihan otomatis
    • Kebijakan retensi dipilih pemanggil per primitive, dan db.prune_notifications(older_than_s=…, max_keep=…) harus dipanggil langsung
    • Pendekatannya adalah membuat kebijakan retensi terlihat di kode pemanggil, bukan menyembunyikannya di balik default library

Pemulihan crash

  • rollback akan menghapus jobs, events, dan notifications bersama dengan penulisan bisnis sesuai karakteristik ACID SQLite
  • tetap aman meski SIGKILL terjadi di tengah transaksi, dan saat open berikutnya atomic commit rollback SQLite tidak meninggalkan stale state
    • penggunaan WAL atau rollback journal mengikuti journal mode
    • verifikasi dilakukan di tests/test_crash_recovery.py, dengan menghentikan subprocess sebelum COMMIT lalu memeriksa PRAGMA integrity_check == 'ok' dan alur notify yang baru
  • jika worker mati saat memproses pekerjaan, worker lain akan melakukan claim ulang setelah visibility_timeout_s berlalu
    • nilai default adalah 300 detik
    • attempts akan bertambah
    • jika melebihi default max_attempts sebanyak 3 kali, baris akan dipindahkan ke _honker_dead
  • listener yang sedang offline saat prune berlangsung akan melewatkan event yang sudah dibersihkan; jika membutuhkan replay yang durable, gunakan db.stream() yang menyimpan offset per konsumen

Integrasi framework web

  • plugin framework tidak disediakan; pendekatannya adalah menghubungkan lewat beberapa baris glue code karena API-nya kecil
  • pada FastAPI, disediakan contoh untuk menjalankan worker loop saat startup, lalu melakukan business write dan queue enqueue bersama-sama di dalam transaksi saat menangani request
  • endpoint SSE dapat dibangun di atas db.listen(channel) atau db.stream(name).subscribe(...) dalam bentuk async def stream(...): yield f"data: ...\n\n" hanya dalam sekitar 30 baris
  • pada Django dan Flask, direkomendasikan menjalankan worker sebagai proses CLI terpisah dengan pola seperti Celery atau RQ

Penggunaan ORM

  • load libhonker_ext pada koneksi ORM, lalu panggil fungsi SQL di dalam transaksi milik ORM agar enqueue di-commit secara atomik bersama business write
  • pada contoh SQLAlchemy, extension dimuat di event connect dan SELECT honker_bootstrap() dijalankan, lalu di dalam transaksi s.begin() model INSERT dan SELECT honker_enqueue(...) dipanggil bersama
  • worker berjalan sebagai proses terpisah yang menggunakan honker.open("app.db"), dan commit watcher akan terbangun oleh commit dari koneksi mana pun ke file yang sama
  • panduan Using with an ORM mencakup integrasi Django, SQLModel, Drizzle, Kysely, sqlx, GORM, ActiveRecord, Ecto, pola wrapper TypedQueue[T] untuk SQLModel/Pydantic, serta caveat terkait Prisma

Performa

  • disebut mampu memproses ribuan pesan per detik di laptop modern
  • latensi wake antarproses dibatasi oleh poll cadence 1ms, dengan median sekitar 1~2ms pada M-series
  • pengukuran pada hardware nyata dapat dilakukan dengan bench/wake_latency_bench.py dan bench/real_bench.py

Konfigurasi pengembangan

  • Tata letak repositori

    • honker-core/: rlib Rust yang dibagikan semua binding, disertakan in-tree, dan juga didistribusikan ke crates.io
    • honker-extension/: cdylib untuk SQLite loadable extension, disertakan in-tree, dan juga didistribusikan ke crates.io
    • packages/honker/: paket Python yang mencakup PyO3 cdylib serta Queue, Stream, Outbox, Scheduler
    • packages/honker-node/: binding Node.js dan merupakan git submodule
    • packages/honker-rs/: wrapper ergonomis untuk Rust dan merupakan git submodule
    • packages/honker-go/: binding Go dan merupakan git submodule
    • packages/honker-ruby/: binding Ruby dan merupakan git submodule
    • packages/honker-bun/: binding Bun dan merupakan git submodule
    • packages/honker-ex/: binding Elixir dan merupakan git submodule
    • packages/honker-cpp/: binding C++ dan merupakan git submodule
    • tests/: direktori integration test lintas paket
    • bench/: direktori benchmark
    • site/: situs honker.dev, berbasis Astro, dan merupakan git submodule
    • tiap repositori binding didistribusikan secara terpisah ke PyPI, npm, crates.io, Hex, RubyGems, dan lainnya, sementara fondasi bersama honker-core dan honker-extension disertakan langsung di repositori ini
    • saat clone, diperlukan git clone --recursive atau git submodule update --init --recursive

Pengujian dan cakupan

  • make test secara default menjalankan tes Rust, Python, dan Node, dengan jalur cepat memakan waktu sekitar 10 detik
  • make test-python-slow mencakup soak test dan tes cron real-time, memakan waktu sekitar 2 menit
  • make test-all menjalankan seluruh tes termasuk mark yang lambat
  • make build menjalankan PyO3 maturin develop dan build loadable extension
  • benchmark dapat dijalankan dengan python bench/wake_latency_bench.py --samples 500, python bench/real_bench.py --workers 4 --enqueuers 2 --seconds 15, python bench/ext_bench.py
  • untuk memasang alat coverage gunakan make install-coverage-deps, yang menginstal coverage.py dan cargo-llvm-cov
  • make coverage menghasilkan dua laporan HTML di coverage/, dan make coverage-python membuat laporan untuk jalur Python, sementara make coverage-rust membuat laporan berdasarkan Rust unit test honker-core
  • coverage Python disebut sekitar 92% untuk packages/honker/
  • coverage Rust hanya mencerminkan cargo test; berbagai jalur di honker_ops.rs hanya dieksekusi oleh test suite Python sehingga tidak tertangkap dalam laporan Rust
  • penggabungan cross-language coverage melalui penggabungan data profil LLVM lintas batas PyO3 sulit dilakukan dan masih ditunda

Lisensi

  • menggunakan lisensi Apache 2.0
  • detail lebih lanjut ada di LICENSE

1 komentar

 
GN⁺ 11 jam lalu
Pendapat Hacker News
  • Saya yang membuat ini. Honker menambahkan NOTIFY/LISTEN lintas-proses ke SQLite, sehingga pengiriman event bergaya push dengan latensi satu digit ms bisa dilakukan hanya dengan file SQLite yang sudah ada, tanpa daemon atau broker
    Karena SQLite tidak punya server seperti Postgres, inti pendekatannya adalah memindahkan sumber polling ke stat(2) ringan terhadap file WAL alih-alih melakukan query secara berkala. SQLite juga efisien meski banyak query kecil dikirim (https://www.sqlite.org/np1queryprob.html), jadi ini mungkin bukan peningkatan yang luar biasa besar, tetapi menarik karena cukup memantau WAL dan memanggil fungsi SQLite saja, sehingga tidak terikat bahasa
    Di atas itu, saya juga menambahkan pub/sub ephemeral, durable work queue dengan retry dan dead-letter, serta event stream dengan offset per konsumen. Ketiganya berupa row di dalam file .db aplikasi yang sudah ada, sehingga bisa di-commit secara atomik bersama write bisnis, dan jika rollback terjadi maka keduanya hilang bersama
    Awalnya namanya litenotify/joblite, tetapi setelah iseng membeli honker.dev, saya melihat nama-nama seperti Oban, pg-boss, Huey, RabbitMQ, Celery, Sidekiq juga sama konyolnya, jadi saya pakai nama ini saja. Semoga berguna atau setidaknya lucu, dan peringatan bahwa ini software alpha tetap berlaku

    • Ini tampaknya terutama berguna untuk bahasa yang lebih mudah menangani hanya konkurensi berbasis proses
      Di Java/Go/Clojure/C# dan sejenisnya, karena SQLite tetap single writer, tampaknya lebih sederhana dan rapi jika aplikasi mengelola writer itu sendiri lalu memakai concurrent queue tingkat bahasa untuk mengetahui write mana yang terjadi dan hanya membangunkan thread terkait
      Meski begitu, pemanfaatan WAL dengan cara kreatif seperti ini tetap menarik, dan untuk bahasa seperti Python/JS/TS/Ruby yang umum memakai konkurensi berbasis proses, ini terlihat cukup cocok sebagai mekanisme notify
    • Saya baru sadar bahwa bahkan stat() setiap 1ms ternyata sangat murah
      Di perangkat keras saya, satu panggilan bahkan tidak sampai 1μs, jadi polling setingkat ini memakai CPU bahkan tidak sampai 0,1%
    • Mungkin saya melewatkan sesuatu, tetapi bukankah PRAGMA data_version lebih baik daripada stat(2)?
      https://sqlite.org/pragma.html#pragma_data_version
      Jika memakai C API, ada juga SQLITE_FCNTL_DATA_VERSION yang lebih langsung
      https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
    • Cukup keren. Saya juga pernah membuat setengah jadi sesuatu yang mirip
      Saya penasaran apakah ini juga bisa dipakai sebagai stream pesan persisten seperti Kafka ringan. Apakah semantik seperti memutar ulang semua pesan lama+real-time untuk topic tertentu sejak timestamp tertentu juga memungkinkan?
      Mungkin bisa ditiru dengan polling seperti pub/sub, tetapi seperti yang Anda bilang, sepertinya bukan cara yang optimal
    • Mungkin akan lebih baik jika status subscriber juga disimpan
      Jika posisi baca, nama queue, filter, dan sebagainya disimpan, maka saat ada perubahan stat(2), alih-alih membangunkan semua thread subscription agar masing-masing melakukan SELECT N=1, thread polling bisa melakukan Events INNER JOIN Subscribers dan hanya membangunkan subscriber yang benar-benar cocok
  • Terima kasih atas masukannya. Saya sudah mengirim PR yang menerapkan usulan-usulan itu
    https://github.com/russellromney/honker/pulls/1
    Sekarang ini berubah menjadi struktur polling 3 lapis: PRAGMA data_version tiap 1ms, stat tiap 100ms, dan penanganan reconnect saat error

    1. PRAGMA data_version tiap 1ms kini dipakai untuk menggantikan deteksi perubahan size/mtime berbasis stat sebelumnya. Karena ini commit counter milik SQLite sendiri, nilainya monotonic, tidak terpengaruh clock skew, dan menangani WAL truncation maupun rollback dengan benar. Ini query nonblocking sekitar 3µs, dan saya menggantinya bukan karena performa melainkan karena akurasi. Bahkan sedikit lebih lambat. Ternyata risiko truncation juga lebih realistis dari yang saya kira
      Dari pengujian saya, SQLITE_FCNTL_DATA_VERSION di C API tidak bekerja antar-koneksi. Jadi saat ini saya masih menanggung biaya melewati layer VFS, dan secara eksplisit menerima tradeoff itu
    2. Jika query data_version gagal, saya mencoba reconnect dengan asumsi kasus seperti error disk sementara, hiccup NFS, atau koneksi korup, dan sebagai langkah pencegahan saya juga membangunkan subscriber
    3. Tiap 100ms, stat membandingkan (dev, ino) dengan nilai saat startup untuk mendeteksi penggantian file. Ini untuk kasus seperti atomic rename, restore litestream, atau remount volume; data_version mengikuti fd yang terbuka, jadi saat file berubah, ia tetap melihat inode lama dan tidak bisa mendeteksinya
      Berkat itu Honker jadi lebih baik, dan saya juga belajar banyak
  • Sedikit promosi, di PostgreSQL 19 mendatang, LISTEN/NOTIFY telah dioptimalkan agar jauh lebih scalable untuk selective signaling
    Patch ini ditujukan untuk kasus banyak backend yang masing-masing mendengarkan channel berbeda
    https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9

    • Promosinya bagus, dan juga sangat relevan dengan topik ini
  • Saya penasaran apakah perubahan WAL tidak bisa dipantau dengan inotify atau wrapper lintas-platform tanpa polling

    • Lintas-platform jadi rusak. Khususnya di Mac, kadang notifikasinya diam-diam tertelan sehingga sulit dipercaya
      stat ya bekerja di mana-mana
  • Yang membuat ini lebih menarik daripada IPC terpisah adalah bahwa ini di-commit atomik dengan data bisnis
    Pengiriman pesan eksternal selalu punya masalah seperti "notifikasi sudah terkirim tetapi transaksi di-rollback", dan ini cepat sekali jadi berantakan
    Satu hal yang saya penasaran adalah checkpoint WAL. Saat SQLite memangkas WAL kembali ke 0, saya tidak tahu apakah polling stat() menanganinya dengan benar. Rasanya seperti ada celah tempat event bisa terlewat

    • Atomisitas praktis adalah segalanya
      Saya pernah susah payah dengan kombinasi Postgres+SQS karena trigger mengirim enqueue di koneksi lain sebelum commit terlihat. Saya menambahkan retry logic, polling di sisi worker, dan akhirnya memindahkan enqueue ke dalam transaksi; setelah itu, sebenarnya saya hanya sedang membuat ulang apa yang dilakukan Honker dengan lebih banyak bagian bergerak
      Bug jenis "notifikasi sudah terkirim tetapi row belum di-commit" biasanya sunyi dan bergantung timing, jadi benar-benar menyiksa untuk dilacak
    • File WAL tetap ada dan hanya di-truncate, jadi itu sendiri terdeteksi sebagai update
      Namun saya memang belum punya pengujian untuk bagian ini, jadi masih perlu dipastikan. Poin yang bagus, akan saya cek
  • Terima kasih
    Banyak aplikasi kecil berbasis SQLite bermunculan, dan kebanyakan butuh queue dan scheduler
    Saya sendiri sudah mencoba menjalankan beberapa hal, tetapi selalu merasa elegansi solusi keluarga Postgres itu kurang
    Saya akan langsung mencoba ini

    • Ungkapan proliferasi kecil sangat pas untuk menggambarkan kumpulan yang terbentuk dari kebiasaan side project saya
      Jika menemui masalah, akan bagus kalau Anda meninggalkan PR atau issue di repo
  • Di sini saya jadi ingin memakai kqueue/FSEvents, tetapi setahu saya Darwin membuang notifikasi dari proses yang sama
    Jika publisher dan listener berada di proses yang sama, kadang listener sama sekali tidak terbangun, jadi pelacakannya bisa cukup berantakan. Polling stat memang terlihat jelek, tetapi pada akhirnya inilah yang benar-benar bekerja di mana-mana
    Saya juga penasaran apakah saat checkpoint WAL membuat file menyusut lagi, wakeup tetap terjadi, atau poller justru memfilter penurunan size

    • Komentar ini sepenuhnya salah
      Event VNODE kqueue dikirim selama proses tersebut punya izin akses ke file, dan tidak ada filter yang mengecualikan proses yang sama
    • Ini memang perlu diuji langsung
      Saya akan cek dan kabari lagi
  • Sangat keren. Saya penasaran saat diberi beban, apakah bottleneck utamanya ada di throughput write SQLite, atau di layer notifikasi WAL

    • Bottleneck ada di alur write dan claim/ack
      Ini juga sangat bergantung pada journal mode dan synchronous mode
      Notifikasi sangat murah, baik dengan cara stat(2) lama maupun pendekatan PRAGMA baru. Di komentar lain juga disebut stat(2) kira-kira berada di level 1µs
  • Proyek yang bagus. Saya juga sedang membuat sesuatu yang mendorong SQLite jauh melampaui penggunaan biasanya
    Melihat lebih banyak orang mengeksplorasi sampai sejauh mana SQLite sebenarnya bisa dipakai itu menggembirakan

  • Saya penasaran apakah ini juga bisa diintegrasikan saat memakai SQLAlchemy
    Dari tampilannya sekarang, sepertinya ini ingin membuat koneksi DB sendiri