2 puan yazan GN⁺ 5 일 전 | 1 yorum | WhatsApp'ta paylaş
  • Dayanıklı kuyruk, stream, pub/sub ve scheduler işlevlerini tek bir SQLite dosyasında birleştirerek Redis·Celery gibi ayrı bir broker olmadan asenkron işleme imkân tanır
  • PRAGMA data_version değerini 1 ms aralıklarla poll ederek süreçler arasında tek haneli milisaniye tepki süresi sağlar; uygulama seviyesinde polling ya da daemon gerekmez
  • notify(), stream(), queue() çağrılarının tümü çağıranın transaction'ı içinde yazılır; iş mantığı yazımlarıyla birlikte commit edilir veya birlikte rollback edilir, böylece dual-write sorununu azaltır
  • İş kuyruğu retry, öncelik, gecikmeli çalıştırma, dead-letter, scheduler, named lock ve rate limiting içerir; stream ise tüketici başına offset saklayan at-least-once teslimatı destekler
  • SQLite'ı ana veri deposu olarak kullanan ortamlarda uygulama ile asenkron işlemeyi tek bir veritabanı dosyasında birleştirerek operasyonel karmaşıklığı azaltabilir
  • Üç temel primitive sunar
    • queue(): at-least-once iş kuyruğu — retry, öncelik, gecikmeli işler, dead-letter, visibility timeout
    • stream(): dayanıklı pub/sub — tüketici başına offset takibi, at-least-once replay
    • notify(): geçici pub/sub — fire-and-forget, geçmiş replay yok
  • Huey tarzı @queue.task() decorator ile fonksiyonları kuyruk işine dönüştürür; crontab() tabanlı periyodik işler + leader election scheduler desteği sunar
  • Kuyruk şemasında _honker_live tablosuna partial index uygulanır; claim işlemi tek bir UPDATE … RETURNING, ack işlemi ise tek bir DELETE ile yapılır ve dead satır sayısından bağımsız tutarlı performans sağlar
  • SQLite yüklenebilir eklentisi (libhonker_ext) sayesinde tüm SQLite 3.9+ istemcileri aynı tablolara erişebilir — Python worker, başka dillerden gönderilen işleri claim edebilir
  • SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, Ecto dahil başlıca ORM'lerle entegrasyon kılavuzları sunar
  • SIGKILL sırasında transaction'lar da SQLite ACID ile güvendedir; worker çökmesi halinde visibility timeout dolduktan sonra otomatik yeniden claim edilir
  • Python, Node.js, Rust, Go, Ruby, Bun, Elixir, C++ olmak üzere 8 dil bağlayıcısı sunar; her biri PyPI·npm·crates.io·Hex·RubyGems üzerinden bağımsız olarak yayımlanır
  • Rust ile geliştirildi (honker-core + honker-extension)
  • Apache 2.0 lisansı

1 yorum

 
GN⁺ 5 일 전
Hacker News görüşleri
  • Bunu ben yaptım. Honker, SQLite’a processler arası NOTIFY/LISTEN ekleyerek, daemon ya da broker olmadan yalnızca mevcut SQLite dosyasıyla tek haneli ms gecikmede push tarzı olay iletimi sağlıyor.
    SQLite’ın Postgres gibi bir sunucusu olmadığı için, belirli aralıklarla sorgu atmak yerine polling kaynağını WAL dosyasına yönelik hafif bir stat(2) çağrısına taşımak işin kilit noktasıydı. SQLite, küçük sorgulardan çok sayıda gönderildiğinde de verimli olduğu için (https://www.sqlite.org/np1queryprob.html) bunu devasa bir yükseltme diye anlatmak zor, ama yalnızca WAL’ı izleyip SQLite fonksiyonlarını çağırmak yeterli olduğu için dilden bağımsız olması ilginç.
    Buna ek olarak ephemeral pub/sub, retry ve dead-letter içeren durable work queue ve tüketici başına offset tutan bir event stream de ekledim. Üçü de mevcut uygulamanın .db dosyası içindeki row’lar olduğu için iş yazmalarıyla atomik olarak commit edilebiliyor; rollback olursa ikisi de birlikte yok oluyor.
    Aslında adı litenotify/joblite olacaktı ama honker.dev alan adını şaka olsun diye almıştım; sonra Oban, pg-boss, Huey, RabbitMQ, Celery, Sidekiq gibi isimlerin de epey komik olduğunu fark edip bununla devam ettim. Umarım faydalı ya da en azından komik olur; alfa yazılım uyarısı ise aynen geçerli.

    • Bu daha çok process tabanlı eşzamanlılığı kolay yöneten diller için gibi görünüyor.
      Java/Go/Clojure/C# tarafında SQLite zaten single writer olduğu için, uygulamanın o writer’ı yönetip dil seviyesindeki concurrent queue ile hangi yazmanın gerçekleştiğini bilmesi ve sadece ilgili thread’leri uyandırması daha basit ve temiz görünüyor.
      Yine de WAL’ı bu şekilde yaratıcı kullanmak eğlenceli ve Python/JS/TS/Ruby gibi process tabanlı eşzamanlılığın yaygın olduğu dillerde notify mekanizması olarak oldukça uygun görünüyor.
    • Her 1ms’de bir stat() etmenin düşündüğümden çok daha ucuz olduğunu burada öğrendim.
      Benim donanımımda çağrı başına 1μs bile sürmüyor; bu düzeyde polling CPU kullanımını %0.1’in bile altında tutuyor.
    • Bir şeyi kaçırıyor olabilirim ama stat(2) yerine PRAGMA data_version daha iyi olmaz mı diye düşünüyorum.
      https://sqlite.org/pragma.html#pragma_data_version
      C API kullanıyorsanız daha doğrudan SQLITE_FCNTL_DATA_VERSION da var.
      https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
    • Oldukça havalı. Ben de bunun benzerini yarım yamalak yapmıştım.
      Bunun hafif bir Kafka gibi kalıcı mesaj akışı olarak da kullanılıp kullanılamayacağını merak ediyorum. Belirli bir topic için belli bir timestamp’ten itibaren geçmiş+gerçek zamanlı tüm mesajları replay etme semantiği de mümkün mü diye merak ediyorum.
      Pub/sub gibi polling ile taklit edilebilir ama dediğin gibi muhtemelen en iyi çözüm olmaz.
    • Subscriber durumu da birlikte tutulursa daha iyi olabilir gibi.
      Okuma konumu, queue adı, filtre gibi şeyleri saklarsanız, stat(2) değiştiğinde tüm subscription thread’lerini uyandırıp her birine N=1 SELECT yaptırmak yerine polling thread Events INNER JOIN Subscribers çalıştırıp gerçekten eşleşen subscriber’ları uyandırabilir.
  • Geri bildirim için teşekkürler. Önerileri yansıtan bir PR açtım.
    https://github.com/russellromney/honker/pulls/1
    Artık 3 katmanlı bir polling yapısı var: her 1ms’de PRAGMA data_version, her 100ms’de stat ve hata durumunda yeniden bağlanma.

    1. Her 1ms’de PRAGMA data_version kullanarak mevcut stat tabanlı size/mtime değişim tespitinin yerini aldım. Bu, SQLite’ın kendi commit counter’ı olduğu için monotonic, clock skew’dan etkilenmiyor ve WAL truncation ya da rollback’i düzgün ele alıyor. Yaklaşık 3µs süren nonblocking bir sorgu; değişiklik performans için değil doğruluk için yapıldı. Hatta biraz daha yavaş. Truncation riski de düşündüğümden daha gerçekçi çıktı.
      Test ettiğimde C API’deki SQLITE_FCNTL_DATA_VERSION connection’lar arasında çalışmadı. Bu yüzden şimdilik hâlâ VFS layer üzerinden gitmenin maliyetini ödüyorum ve bu tradeoff’u bilinçli olarak kabul etmiş durumdayım.
    2. data_version sorgusu başarısız olursa, bunun geçici disk hatası, NFS hiccup ya da connection bozulması gibi bir durum olduğunu varsayıp yeniden bağlanmayı deniyorum; önlem amaçlı olarak subscriber’ları da uyandırıyorum.
    3. Her 100ms’de stat ile (dev, ino) değerlerini başlangıçtaki değerlerle karşılaştırarak dosya değişimini yakalıyorum. Bu; atomic rename, litestream restore ya da volume remount gibi durumlar için gerekli, çünkü data_version açık fd’yi takip ettiğinden dosya değişse bile eski inode’u görmeye devam ediyor ve bunu kaçırıyor.
      Bunun sayesinde Honker daha iyi hale geldi, ben de çok şey öğrendim.
  • Küçük bir tanıtım geçeyim: yaklaşan PostgreSQL 19 sürümünde LISTEN/NOTIFY, selective signaling senaryolarında çok daha iyi ölçeklenecek şekilde optimize edildi.
    Bu yama, çok sayıda backend’in farklı channel’ları listen ettiği durumları hedefliyor.
    https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9

    • Güzel tanıtımdı, ayrıca konuyla da çok uyumlu.
  • Polling olmadan inotify ya da çapraz platform bir wrapper ile WAL değişikliklerini izlemek mümkün değil mi diye düşündüm.

    • Çapraz platform tarafı bozuluyor. Özellikle Mac’te sessizce yutulan durumlar olduğu için güvenmek zor.
      stat ise her yerde çalışıyor.
  • Ayrı bir IPC’ye göre daha çekici olmasının nedeni, iş verisiyle atomik commit edilmesi.
    Harici mesaj iletiminde her zaman "bildirim gitti ama transaction rollback oldu" sorunu var ve bu iş hızla çamura dönüyor.
    Merak ettiğim tek şey WAL checkpoint. SQLite WAL’ı tekrar 0’a truncate ettiğinde stat() polling bunu düzgün ele alıyor mu bilmiyorum. O sırada event kaçırılabilecek bir aralık varmış gibi geliyor.

    • Bence atomiklik neredeyse her şey.
      Daha önce Postgres+SQS kombinasyonunda, enqueue işlemini ayrı bir connection’da commit görünmeden önce trigger ile göndermemiz yüzünden çok uğraşmıştım. Retry logic ekledik, worker tarafında polling de koyduk, sonunda enqueue’yu transaction içine taşıdık; ama bunu yapınca da aslında Honker’ın yaptığını daha fazla moving part ile yeniden kurmuş oluyorsunuz.
      "Bildirim gitti ama row henüz commit edilmedi" türü hatalar genelde sessiz ve zamanlamaya bağımlı oluyor; iz sürmesi gerçekten çok zor.
    • WAL dosyası yerinde kalıyor ve sadece truncate ediliyor, dolayısıyla bu kendi başına bir update olarak yakalanıyor.
      Yine de bu kısım için henüz test yok, o yüzden daha fazla doğrulamam lazım. Güzel nokta, bakacağım.
  • Teşekkürler.
    SQLite tabanlı küçük uygulamaların sayısı çok arttı ve çoğunun queue ile scheduler’a ihtiyacı var.
    Ben de birkaç çözümü çalıştırdım ama hep Postgres kökenli çözümlerin zarafetini aradım.
    Bunu doğrudan deneyeceğim.

    • Küçük çoğalma ifadesi, yan proje alışkanlıklarımın oluşturduğu kümeyi anlatmak için çok uygun.
      Bir sorunla karşılaşırsanız repo’ya PR ya da issue bırakmanız harika olur.
  • Burada kqueue/FSEvents kullanmak isteği doğuyor ama bildiğim kadarıyla Darwin aynı process’ten gelen bildirimleri düşürüyor.
    Publisher ve listener aynı process içindeyse listener’ın hiç uyanmadığı durumlar olabiliyor; takip etmesi oldukça kirli bir sorun. stat polling çirkin görünse de sonunda gerçekten her yerde çalışan şey bu gibi duruyor.
    WAL checkpoint sırasında dosya yeniden küçülürken wakeup oluşuyor mu, yoksa poller boyut küçülmesini filtreliyor mu onu da merak ediyorum.

    • Bu yorum tamamen yanlış.
      kqueue’nun VNODE event’leri, process’in dosyaya erişim yetkisi olduğu sürece iletilir; aynı process olduğu için uygulanan bir filtre yok.
    • Bunun gerçekten test edilmesi lazım.
      Kontrol edip tekrar haber vereceğim.
  • Çok havalı. Yük altında darboğazın daha çok SQLite yazma throughput tarafında mı yoksa WAL notification layer tarafında mı olduğunu merak ediyorum.

    • Darboğaz yazma ile claim/ack akışında.
      Journal mode ve synchronous mode’a göre de çok değişiyor.
      Notification tarafı, eski stat(2) yaklaşımı da yeni PRAGMA tabanlı yaklaşım da olsa, çok ucuz. Başka yorumlarda da stat(2) için yaklaşık 1µs denmişti.
  • Güzel proje. Ben de SQLite’ı alışılmış kullanım alanının çok ötesine iten bir şey geliştiriyorum.
    SQLite’ın gerçekte nereye kadar gidebildiğini daha fazla kişinin keşfetmesini görmek cesaret verici.

  • SQLAlchemy kullanan durumlarda da entegre edilebilir mi diye merak ediyorum.
    Şu anki haliyle sanki DB connection’ı kendisi açmak istiyormuş gibi görünüyor.