2 điểm bởi GN⁺ 2026-04-25 | 1 bình luận | Chia sẻ qua WhatsApp
  • Tích hợp hàng đợi bền vững, stream, pub/sub, bộ lập lịch vào trong một tệp SQLite duy nhất, cho phép xử lý tác vụ bất đồng bộ mà không cần broker riêng như Redis hay Celery
  • Dùng PRAGMA data_version để polling mỗi 1ms nhằm đạt độ phản hồi giữa các tiến trình ở mức vài mili giây, không cần polling ở cấp ứng dụng hay daemon
  • notify(), stream(), queue() đều được ghi trong transaction của bên gọi, nên sẽ commit cùng hoặc rollback cùng với phần ghi nghiệp vụ, giúp giảm vấn đề dual-write
  • Hàng đợi tác vụ bao gồm retry, ưu tiên, chạy trễ, dead-letter, scheduler, named lock, rate limiting; stream hỗ trợ phân phối at-least-once bằng cách lưu offset theo từng consumer
  • Trong môi trường dùng SQLite làm kho lưu trữ chính, có thể gộp ứng dụng và xử lý bất đồng bộ vào một tệp cơ sở dữ liệu để giảm độ phức tạp vận hành
  • Cung cấp ba primitive cốt lõi
    • queue(): hàng đợi tác vụ at-least-once — retry, ưu tiên, tác vụ trì hoãn, dead-letter, visibility timeout
    • stream(): pub/sub bền vững — theo dõi offset theo từng consumer, replay at-least-once
    • notify(): pub/sub tạm thời — fire-and-forget, không có replay lịch sử
  • Hỗ trợ biến hàm thành tác vụ hàng đợi bằng decorator @queue.task() kiểu Huey, cùng tác vụ định kỳ dựa trên crontab() + scheduler có leader election
  • Schema của hàng đợi áp dụng partial index trên bảng _honker_live; thao tác claim chỉ cần một UPDATE … RETURNING, ack chỉ cần một DELETE, nên hiệu năng ổn định независимо от số lượng dòng chết
  • Là tiện ích mở rộng SQLite có thể nạp động (libhonker_ext), cho phép mọi client SQLite 3.9+ truy cập cùng một bảng — worker Python có thể claim tác vụ được push từ ngôn ngữ khác
  • Cung cấp hướng dẫn tích hợp với các ORM phổ biến như SQLAlchemy, Django, Drizzle, Kysely, sqlx, GORM, ActiveRecord, Ecto
  • Kể cả transaction bị dừng bởi SIGKILL vẫn an toàn nhờ ACID của SQLite; khi worker crash, tác vụ sẽ tự động được reclaim sau khi visibility timeout hết hạn
  • Cung cấp binding cho 8 ngôn ngữ: Python, Node.js, Rust, Go, Ruby, Bun, Elixir, C++, mỗi gói được phát hành độc lập trên PyPI, npm, crates.io, Hex, RubyGems
  • Được triển khai bằng Rust (honker-core + honker-extension)
  • Giấy phép Apache 2.0

1 bình luận

 
GN⁺ 2026-04-25
Ý kiến trên Hacker News
  • Tôi đã tự làm cái này. Honker thêm NOTIFY/LISTEN liên tiến trình vào SQLite, để truyền sự kiện kiểu push với độ trễ chỉ vài ms bằng chính file SQLite sẵn có, không cần daemon hay broker
    SQLite không có server như Postgres, nên điểm mấu chốt là chuyển nguồn polling sang stat(2) nhẹ trên file WAL thay vì query theo chu kỳ. SQLite vẫn hiệu quả ngay cả khi bắn nhiều query nhỏ (https://www.sqlite.org/np1queryprob.html), nên khó gọi đây là một nâng cấp khổng lồ, nhưng việc chỉ cần theo dõi WAL và gọi hàm SQLite khiến nó không phụ thuộc ngôn ngữ là điều khá thú vị
    Tôi cũng thêm ephemeral pub/sub, durable work queue có retry và dead-letter, cùng event stream có offset theo từng consumer. Cả ba đều chỉ là các row trong file .db của ứng dụng hiện có, nên có thể commit nguyên tử cùng với ghi nghiệp vụ, và nếu rollback thì cả hai cùng biến mất
    Ban đầu nó tên là litenotify/joblite, nhưng sau khi mua honker.dev cho vui thì thấy Oban, pg-boss, Huey, RabbitMQ, Celery, Sidekiq đều có tên khá ngộ nên tôi cứ chốt tên này. Hy vọng nó hữu ích hoặc ít nhất là buồn cười, và cảnh báo đây là phần mềm alpha vẫn hoàn toàn đúng

    • Có vẻ cái này chủ yếu phù hợp với các ngôn ngữ mà chỉ dễ xử lý đồng thời dựa trên tiến trình
      Với Java/Go/Clojure/C# thì SQLite vốn dĩ đã là single writer, nên có vẻ đơn giản và gọn hơn nếu ứng dụng tự quản lý writer đó rồi dùng concurrent queue ở cấp ngôn ngữ để biết có ghi nào xảy ra và chỉ đánh thức các thread liên quan
      Dù vậy, cách tận dụng WAL sáng tạo như thế này vẫn rất thú vị, và với các ngôn ngữ như Python/JS/TS/Ruby, nơi đồng thời dựa trên tiến trình là phổ biến, nó có vẻ khá hợp làm cơ chế notify
    • Lần này tôi mới biết stat() mỗi 1ms hóa ra rẻ hơn nhiều so với tưởng tượng
      Trên phần cứng của tôi, mỗi lần gọi còn chưa đến 1μs, nên kiểu polling này dùng chưa đến 0,1% CPU
    • Có thể tôi đã bỏ sót gì đó, nhưng liệu PRAGMA data_version có tốt hơn stat(2) không nhỉ
      https://sqlite.org/pragma.html#pragma_data_version
      Nếu là C API thì còn có SQLITE_FCNTL_DATA_VERSION trực tiếp hơn nữa
      https://sqlite.org/c3ref/c_fcntl_begin_atomic_write.html#sqlitefcntldataversion
    • Khá hay. Tôi cũng từng làm dở một thứ tương tự
      Tôi tò mò liệu cái này có thể dùng như Kafka hạng nhẹ cho luồng thông điệp bền vững hay không. Cũng muốn biết liệu có hỗ trợ ngữ nghĩa kiểu replay toàn bộ thông điệp quá khứ + thời gian thực từ một timestamp nào đó cho một topic cụ thể không
      Có lẽ vẫn có thể giả lập bằng polling như pub/sub, nhưng như bạn nói thì chắc không phải tối ưu
    • Có lẽ sẽ còn tốt hơn nếu lưu cả trạng thái subscriber
      Nếu lưu vị trí đọc, tên queue, bộ lọc v.v. thì thay vì mỗi khi stat(2) đổi là đánh thức mọi subscription thread để mỗi thread tự chạy một SELECT với N=1, polling thread có thể Events INNER JOIN Subscribers và chỉ đánh thức những subscriber thật sự khớp
  • Cảm ơn phản hồi. Tôi đã mở PR phản ánh các đề xuất đó
    https://github.com/russellromney/honker/pulls/1
    Giờ nó đã chuyển sang cấu trúc polling 3 tầng: PRAGMA data_version mỗi 1ms, stat mỗi 100ms, và xử lý reconnect khi có lỗi

    1. Mỗi 1ms dùng PRAGMA data_version để thay thế việc phát hiện thay đổi size/mtime dựa trên stat trước đây. Đây là commit counter của chính SQLite nên đơn điệu tăng, không bị ảnh hưởng bởi clock skew, và xử lý đúng cả WAL truncation lẫn rollback. Nó là một query nonblocking khoảng 3µs, và tôi đổi không phải vì hiệu năng mà vì độ chính xác. Thậm chí nó còn chậm hơn một chút. Rủi ro truncation hóa ra cũng thực tế hơn tôi tưởng
      Khi test thì C API SQLITE_FCNTL_DATA_VERSION không hoạt động giữa các connection. Vì vậy hiện giờ tôi vẫn phải chấp nhận chi phí đi qua VFS layer, và đang chủ động chấp nhận tradeoff đó
    2. Nếu query data_version thất bại thì sẽ thử reconnect, giả định các trường hợp như lỗi đĩa tạm thời, NFS hiccup, connection corruption, và để phòng ngừa thì cũng đánh thức subscriber
    3. Mỗi 100ms dùng stat để so sánh (dev, ino) với giá trị lúc startup nhằm phát hiện thay thế file. Đây là các trường hợp như atomic rename, litestream restore, volume remount; data_version đi theo fd đang mở nên nếu file đã bị thay thì nó vẫn tiếp tục nhìn inode cũ và không bắt được việc này
      Nhờ vậy Honker tốt hơn hẳn và tôi cũng học được rất nhiều
  • Nhân tiện quảng bá nhẹ, trong PostgreSQL 19 sắp tới, LISTEN/NOTIFY đã được tối ưu để scale tốt hơn nhiều cho selective signaling
    Đây là bản vá nhắm vào trường hợp có nhiều backend cùng listen các channel khác nhau
    https://git.postgresql.org/gitweb/?p=postgresql.git;a=commitdiff;h=282b1cde9

    • Quảng bá rất ổn, mà còn cực kỳ đúng chủ đề nữa
  • Tôi tự hỏi sao không theo dõi thay đổi WAL bằng inotify hoặc wrapper đa nền tảng để khỏi polling

    • Thế là hỏng đa nền tảng. Đặc biệt trên Mac có trường hợp nó âm thầm nuốt mất, nên khó mà tin cậy
      stat thì đơn giản là chạy ở khắp mọi nơi
  • Điều hấp dẫn hơn so với IPC riêng là nó được commit nguyên tử với dữ liệu nghiệp vụ
    Với cơ chế chuyển thông điệp bên ngoài lúc nào cũng có vấn đề kiểu “đã gửi thông báo nhưng transaction lại rollback”, và mọi thứ rất nhanh trở nên rối rắm
    Tôi có một thắc mắc về WAL checkpoint. Khi SQLite truncate WAL về lại 0 thì polling bằng stat() có xử lý đúng không. Tôi có cảm giác sẽ có một khoảng trống làm mất sự kiện

    • Tôi nghĩ tính nguyên tử gần như là tất cả
      Trước đây tôi từng rất khổ với tổ hợp Postgres+SQS vì trigger gửi enqueue trước khi commit trên connection khác trở nên nhìn thấy được. Phải thêm retry logic, thêm polling phía worker, cuối cùng lại phải đưa enqueue vào trong transaction, mà làm vậy thì rốt cuộc chỉ là dựng lại điều Honker đang làm nhưng với nhiều moving part hơn
      Các lỗi kiểu “notification đã gửi nhưng row vẫn chưa commit” thường im lặng và phụ thuộc timing, nên truy vết thực sự rất đau đầu
    • File WAL vẫn còn đó và chỉ bị truncate nên bản thân việc đó vẫn được bắt như một update
      Tuy nhiên phần này tôi vẫn chưa có test nên cần kiểm tra thêm. Đây là một điểm hay, tôi sẽ xem kỹ
  • Cảm ơn
    Các ứng dụng nhỏ dựa trên SQLite đã tăng lên rất nhiều, và phần lớn đều cần queue và scheduler
    Tôi đã tự vận hành vài thứ, nhưng lúc nào cũng thấy thiếu sự tao nhã của các giải pháp kiểu Postgres
    Tôi định sẽ thử cái này ngay

    • Cụm “sự sinh sôi nhỏ” mô tả quá đúng cái quần thể do thói quen làm side project của tôi tạo ra
      Nếu gặp vấn đề thì rất mong bạn mở PR hoặc issue trong repo
  • Ở đây tôi rất muốn dùng kqueue/FSEvents, nhưng tôi nhớ là Darwin sẽ làm rơi thông báo từ cùng một tiến trình
    Nếu publisher và listener ở cùng tiến trình thì có trường hợp listener hoàn toàn không được đánh thức, nên việc truy vết khá bẩn. Polling bằng stat nhìn thì xấu nhưng rốt cuộc lại là thứ thực sự chạy được ở mọi nơi
    Tôi cũng tò mò liệu khi WAL checkpoint làm file nhỏ lại thì có phát sinh wakeup không, hay poller lọc luôn trường hợp giảm kích thước

    • Bình luận này hoàn toàn sai
      Sự kiện VNODE của kqueue sẽ được gửi miễn là tiến trình đó có quyền truy cập file, không có bộ lọc nào loại vì cùng tiến trình cả
    • Cái này thực sự cần test
      Tôi sẽ kiểm tra rồi báo lại
  • Rất hay. Tôi tò mò khi có tải thì nút thắt cổ chai chủ yếu là throughput ghi của SQLite, hay là lớp thông báo WAL

    • Nút thắt là phía ghi và luồng claim/ack
      Nó cũng thay đổi khá nhiều tùy journal mode và synchronous mode
      Phần notification thì cực kỳ rẻ, dù là cách cũ dùng stat(2) hay cách mới dựa trên PRAGMA. Ở bình luận khác cũng đã nói stat(2) cỡ khoảng 1µs
  • Dự án hay đấy. Tôi cũng đang làm một thứ đẩy SQLite đi xa hơn rất nhiều so với cách nó thường được dùng
    Thật đáng khích lệ khi thấy ngày càng nhiều người khám phá xem SQLite thực sự có thể làm được đến đâu

  • Tôi tò mò liệu có thể tích hợp cả khi dùng SQLAlchemy hay không
    Nhìn hiện tại thì có vẻ nó muốn tự tạo DB connection riêng