Honker - SQLite में Postgres NOTIFY/LISTEN को लागू करने वाला एक्सटेंशन
(github.com/russellromney)- एक ही SQLite फ़ाइल में durable queue, stream, pub/sub, scheduler को एकीकृत करके Redis·Celery जैसे अलग broker के बिना asynchronous कार्यों को प्रोसेस किया जा सकता है
PRAGMA data_versionको 1ms अंतराल पर poll करके processes के बीच single-digit millisecond प्रतिक्रिया गति हासिल की जाती है, application-level polling या daemon की ज़रूरत नहींnotify(),stream(),queue()सभी caller के transaction के भीतर लिखे जाते हैं, इसलिए business writes के साथ commit होते हैं या साथ में rollback होते हैं, जिससे dual-write समस्या कम होती है- job queue में retry, priority, delayed execution, dead-letter, scheduler, named lock, rate limiting शामिल हैं, और stream at-least-once delivery को support करता है जिसमें हर consumer का offset स्टोर होता है
- SQLite को primary storage के रूप में इस्तेमाल करने वाले environments में application और asynchronous processing को एक database file में समेटकर operational complexity कम की जा सकती है
- तीन मुख्य primitives उपलब्ध हैं
- queue(): at-least-once job queue — retry, priority, delayed jobs, dead-letter, visibility timeout
- stream(): durable pub/sub — per-consumer offset tracking, at-least-once replay
- notify(): ephemeral pub/sub — fire-and-forget, history replay नहीं
- Huey-शैली के
@queue.task()decorator से functions को queue jobs में बदला जा सकता है,crontab()आधारित periodic jobs + leader election scheduler का support - queue schema में
_honker_livetable पर partial index लागू है, claim एकUPDATE … RETURNINGसे और ack एकDELETEसे होता है, इसलिए dead rows की संख्या से स्वतंत्र स्थिर performance मिलती है - SQLite loadable extension (
libhonker_ext) के रूप में सभी SQLite 3.9+ clients से एक ही table को access किया जा सकता है — Python worker दूसरे languages से push किए गए jobs को claim कर सकता है - SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, Ecto सहित प्रमुख ORM के साथ integration गाइड उपलब्ध हैं
- SIGKILL के दौरान के transactions भी SQLite ACID के कारण सुरक्षित रहते हैं, और worker crash होने पर visibility timeout समाप्त होने के बाद अपने-आप reclaim हो जाते हैं
- Python, Node.js, Rust, Go, Ruby, Bun, Elixir, C++ 8 language bindings उपलब्ध हैं, और प्रत्येक को PyPI·npm·crates.io·Hex·RubyGems पर अलग से प्रकाशित किया गया है
- Rust में implemented (honker-core + honker-extension)
- Apache 2.0 license
1 टिप्पणियां
Hacker News टिप्पणियाँ
मैंने यह खुद बनाया है। Honker SQLite में cross-process NOTIFY/LISTEN जोड़ता है, ताकि daemon या broker के बिना सिर्फ मौजूदा SQLite फ़ाइल के सहारे single-digit ms latency के साथ push-style event delivery मिल सके।
SQLite में Postgres जैसा server नहीं होता, इसलिए तय अंतराल पर query करने के बजाय polling source को WAL फ़ाइल पर हल्के
stat(2)पर शिफ्ट करना इसका मुख्य विचार है। SQLite बहुत सारी छोटी queries चलाने में भी efficient है (https://www.sqlite.org/np1queryprob.html), इसलिए इसे बहुत बड़ा upgrade कहना मुश्किल है, लेकिन WAL को मॉनिटर करके और सिर्फ SQLite function कॉल करके यह language-agnostic बन जाता है, यही दिलचस्प बात है।इसके ऊपर ephemeral pub/sub, retry और dead-letter के साथ durable work queue, और per-consumer offset वाले event stream भी जोड़े हैं। ये तीनों मौजूदा app की
.dbफ़ाइल के अंदर rows के रूप में हैं, इसलिए इन्हें business writes के साथ atomically commit किया जा सकता है, और rollback होने पर दोनों साथ में गायब हो जाते हैं।पहले इसका नाम litenotify/joblite था, लेकिन मैंने मज़ाक में
honker.devखरीद लिया था, फिर देखा कि Oban, pg-boss, Huey, RabbitMQ, Celery, Sidekiq जैसे नाम भी वैसे ही मज़ेदार हैं, तो यही नाम रख दिया। उम्मीद है कि यह उपयोगी होगा, या कम से कम मज़ेदार तो होगा, और alpha software वाली चेतावनी अभी भी लागू है।Java/Go/Clojure/C# जैसी भाषाओं में SQLite वैसे भी single writer है, इसलिए application खुद उस writer को मैनेज करे और language-level concurrent queue के जरिए यह जाने कि कौन-सी write हुई है और सिर्फ संबंधित threads को जगाए, यह ज़्यादा simple और clean लगता है।
फिर भी WAL का इस तरह creative इस्तेमाल काफ़ी मज़ेदार है, और Python/JS/TS/Ruby जैसी भाषाओं में जहाँ process-based concurrency आम है, वहाँ notify mechanism के रूप में यह काफ़ी अच्छा fit लगता है।
मेरे hardware पर इसमें प्रति call 1μs से भी कम लगता है, इसलिए इस स्तर की polling से CPU usage 0.1% भी नहीं होता।
stat(2)सेPRAGMA data_versionबेहतर नहीं होगा?https://sqlite.org/pragma.html#pragma_data_version
अगर C API इस्तेमाल कर रहे हैं तो और भी direct
SQLITE_FCNTL_DATA_VERSIONभी है।https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
जानना चाहता हूँ कि क्या इसे हल्के Kafka की तरह persistent message stream के रूप में भी इस्तेमाल किया जा सकता है। क्या किसी खास topic के लिए किसी timestamp से past+live messages पूरे replay करने जैसी semantics भी संभव हैं?
pub/sub की तरह polling से इसका approximation किया जा सकता है, लेकिन जैसा आपने कहा, शायद वह optimal नहीं होगा।
read position, queue name, filter जैसी चीज़ें स्टोर कर दें, तो
stat(2)बदलने पर हर subscription thread को जगाकर उनसे N=1 SELECT करवाने के बजाय polling threadEvents INNER JOIN Subscribersचला सकता है और सिर्फ वास्तव में match होने वाले subscribers को जगा सकता है।फ़ीडबैक के लिए धन्यवाद। सुझावों को शामिल करते हुए मैंने PR भेज दिया है।
https://github.com/russellromney/honker/pulls/1
अब यह 3-layer polling structure में बदल गया है: हर 1ms पर
PRAGMA data_version, हर 100ms परstat, और error होने पर reconnect handling।PRAGMA data_versionइस्तेमाल करके पुरानेstat-आधारित size/mtime change detection को replace किया है। यह खुद SQLite का commit counter है, इसलिए monotonic है, clock skew से प्रभावित नहीं होता, और WAL truncation या rollback को भी सही तरह संभालता है। यह लगभग 3µs का nonblocking query है, और मैंने इसे performance के लिए नहीं बल्कि correctness के लिए बदला है। उल्टा, यह थोड़ा धीमा है। truncation का risk भी उम्मीद से ज़्यादा real निकला।टेस्ट करने पर C API का
SQLITE_FCNTL_DATA_VERSIONconnections के बीच काम नहीं कर रहा था। इसलिए अभी भी VFS layer से होकर जाने की cost देनी पड़ रही है, और इस tradeoff को मैंने स्पष्ट रूप से स्वीकार किया है।data_versionquery fail होती है, तो disk temporary error, NFS hiccup, connection corruption जैसी स्थितियाँ मानकर reconnect की कोशिश की जाती है, और precaution के तौर पर subscriber को भी जगा दिया जाता है।statसे(dev, ino)की तुलना startup के समय के मान से करके file replacement पकड़ा जाता है। जैसे atomic rename, litestream restore, volume remount जैसी स्थितियाँ।data_versionखुली हुई fd को follow करता है, इसलिए फ़ाइल बदल जाने पर भी वह original inode देखता रहता है और इसे पकड़ नहीं पाता।इससे Honker बेहतर हुआ है और मैंने भी बहुत कुछ सीखा है।
हल्का-सा प्रचार करूँ तो, आने वाले PostgreSQL 19 में LISTEN/NOTIFY को selective signaling में कहीं बेहतर scale करने के लिए optimize किया गया है।
यह patch उन स्थितियों के लिए है जहाँ बहुत सारे backend अलग-अलग channels को listen कर रहे होते हैं।
https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9
polling के बिना inotify या cross-platform wrapper से WAL changes को monitor क्यों न किया जाए?
statबस हर जगह काम करता है।अलग IPC की तुलना में इसकी सबसे आकर्षक बात यह है कि यह business data के साथ atomic commit होता है।
बाहरी message delivery में हमेशा "notification तो चला गया लेकिन transaction rollback हो गया" जैसी समस्या रहती है, और यह जल्दी messy हो जाता है।
एक सवाल WAL checkpoint को लेकर है। जब SQLite WAL को वापस 0 पर truncate करता है, तो क्या
stat()polling इसे सही तरीके से संभालती है? लगता है जैसे events miss होने की कोई window हो सकती है।पहले Postgres+SQS के साथ मुझे बहुत परेशानी हुई थी, क्योंकि trigger enqueue को दूसरी connection में commit visible होने से पहले भेज देता था। फिर retry logic जोड़ा, worker side polling भी जोड़ी, और अंत में enqueue को transaction के भीतर लाना पड़ा। उसके बाद असल में वही चीज़ ज़्यादा moving parts के साथ दोबारा बन रही थी जो Honker कर रहा है।
"notification चला गया लेकिन row अभी commit नहीं हुई" जैसे bugs आमतौर पर चुपचाप और timing-dependent होते हैं, इसलिए इन्हें trace करना बेहद दर्दनाक होता है।
हालाँकि इस हिस्से के लिए अभी test नहीं है, इसलिए और verify करना होगा। अच्छा point है, मैं इसे देखूँगा।
धन्यवाद।
SQLite-आधारित छोटे apps अब बहुत बढ़ गए हैं, और उनमें से ज़्यादातर को queue और scheduler की ज़रूरत होती है।
मैंने खुद कुछ solutions चलाए हैं, लेकिन हमेशा Postgres-आधारित solutions की elegance की कमी महसूस हुई।
इसे मैं तुरंत आज़माने वाला हूँ।
अगर कोई समस्या मिले तो repo में PR या issue छोड़ना अच्छा रहेगा।
यहाँ kqueue/FSEvents इस्तेमाल करने का मन तो करता है, लेकिन मेरी समझ से Darwin उसी process की notifications drop कर देता है।
अगर publisher और listener एक ही process में हों, तो listener कभी-कभी जागता ही नहीं, और इसे trace करना काफ़ी messy हो जाता है।
statpolling भले ही बदसूरत लगे, लेकिन आख़िर में वही तरीका है जो हर जगह सचमुच काम करता है।WAL checkpoint के समय जब फ़ाइल फिर छोटी हो जाती है, तब wakeup होता है या poller size decrease को filter कर देता है, यह भी जानना चाहूँगा।
kqueue के VNODE events तब deliver होते हैं जब process के पास फ़ाइल तक पहुँच की अनुमति हो, same-process होने के आधार पर कोई filtering नहीं होती।
मैं जाँचकर फिर बताऊँगा।
बहुत शानदार। load आने पर bottleneck मुख्य रूप से SQLite write throughput होता है या WAL notification layer, यह जानना चाहूँगा।
यह journal mode और synchronous mode पर भी काफी निर्भर करता है।
notification चाहे पुराना
stat(2)तरीका हो या नयाPRAGMA-आधारित, बहुत सस्ता है। दूसरी टिप्पणियों में भी कहा गया किstat(2)लगभग 1µs के स्तर का है।अच्छा project है। मैं भी कुछ ऐसा बना रहा हूँ जो SQLite को उसके सामान्य उपयोग से कहीं आगे तक push करता है।
यह देखना उत्साहजनक है कि ज़्यादा लोग खोज रहे हैं कि SQLite वास्तव में कहाँ तक जा सकता है।
जानना चाहूँगा कि क्या SQLAlchemy इस्तेमाल करने पर भी इसका integration संभव है।
अभी के रूप से तो लगता है कि यह अपना DB connection खुद बनाना चाहता है।