honker (Ruby)
Ruby binding for Honker: durable queues, streams, pub/sub, and time-trigger scheduling on SQLite.
Full docs:
Install
Add it to your Gemfile:
# Gemfile
gem "honker"
then bundle install. Or install it directly:
gem install honker
Honker is a thin Ruby wrapper around a SQLite loadable extension written in Rust. How that extension reaches your gem install depends on your platform, with a fallback to compile the extension for other platforms.
Most platforms (x86_64 and arm64 for Linux, arm64 for macOS) get a platform gem with the extension already built and bundled inside it:
| Platform | Triple |
|---|---|
| x86_64 Linux | x86_64-linux |
| arm64 Linux | aarch64-linux |
| Apple Silicon macOS | arm64-darwin |
gem install honker
# Fetching honker-0.2.0-arm64-darwin.gem
Honker::Database.new("app.db") then finds the bundled extension
automatically.
Where the bundled extension lives
It is shipped inside the gem under lib/honker/, with a filename that
follows the host OS's native shared-library convention:
| Host OS | Extension file | Why this name |
|---|---|---|
| Linux | lib/honker/libhonker_ext.so |
lib prefix, .so (shared object) |
| macOS | lib/honker/libhonker_ext.dylib |
lib prefix, .dylib (dynamic library) |
| Windows | lib/honker/honker_ext.dll |
no lib prefix, .dll (dynamic-link lib) |
Honker::Database.new checks the host OS at load time and resolves to
the matching filename, so you usually do not need to think about it.
You only see the difference if you build the extension yourself or set
HONKER_EXTENSION_PATH, in which case the file you point at must match
the OS the Ruby process is running on.
To find the installed copy:
gem which honker # => …/gems/honker-0.2.0-arm64-darwin/lib/honker.rb
ls "$(dirname "$(gem which honker)")/honker"
# libhonker_ext.dylib ...
Overriding the extension path
To point at a different copy of the extension (a local dev build, a
sidecar mounted into a container, a system path) pass extension_path:
or set HONKER_EXTENSION_PATH; both override the bundled file.
Honker::Database.new("app.db", extension_path: "./libhonker_ext.dylib")
HONKER_EXTENSION_PATH=/opt/honker/libhonker_ext.so ruby app.rb
Source gem (compiles on install, needs Rust)
Every other platform (Intel macOS, Windows, anything else) gets the
generic gem, which ships the Rust crate source and compiles the
extension during gem install. Install rustc and cargo
first; otherwise the install fails with a clear message pointing at
rustup.
gem install honker
# Fetching honker-0.2.0.gem
# Building native extensions. This could take a while...
# honker: building the SQLite extension with cargo
The compiled artifact lands in the same lib/honker/ directory as the
prebuilt gems, so Honker::Database.new finds it the same way.
Installing from git
honker lives in a subdirectory of a polyglot monorepo, so a github:
dependency needs Bundler's glob: option to point at the gemspec. A
git checkout has no prebuilt extension, so it always takes the
compile-on-install path and needs rustc:
# Gemfile
gem "honker",
github: "russellromney/honker",
glob: "packages/honker-ruby/honker.gemspec"
To pin to a branch, tag, or commit, add branch:, tag:, or ref::
# Gemfile
gem "honker",
github: "russellromney/honker",
glob: "packages/honker-ruby/honker.gemspec",
branch: "main"
Using Honker alongside an ORM
honker-ruby opens its own SQLite3::Database handle (via the
sqlite3 gem) and loads the
extension into it. To share a single SQLite file (and a single
transaction) with Rails/ActiveRecord, Sequel, ROM, or Hanami::DB, get
the underlying sqlite3 gem connection your ORM is already using and
load the Honker extension into that handle, so honker_enqueue(...)
and friends are callable from inside one of its transactions.
Rails / ActiveRecord
Point ActiveRecord at the bundled extension via database.yml:
# config/database.yml
production:
adapter: sqlite3
database: app.sqlite3
extensions:
- <%= Honker.extension_path %>
# Gemfile
gem "honker"
That's it. Honker::Railtie runs Honker.bootstrap against
ActiveRecord::Base.connection in config.after_initialize, so no
initializer is required.
Multi-database apps still need to call Honker.bootstrap(some_other_conn)
themselves for non-primary connections.
Sequel (and ROM / Hanami::DB)
Honker.sequel_after_connect returns the right after_connect: proc,
so each new connection in the pool gets the extension loaded and the
schema bootstrapped:
require "sequel"
require "honker"
DB = Sequel.connect(
"sqlite://app.sqlite3",
after_connect: Honker.sequel_after_connect,
)
DB.synchronize do |conn|
conn.execute("SELECT honker_enqueue('emails', '{}', NULL, NULL, 0, 3, NULL)")
end
To bootstrap once from a migration instead of on every connect, pass
bootstrap: false:
DB = Sequel.connect(
"sqlite://app.sqlite3",
after_connect: Honker.sequel_after_connect(bootstrap: false),
)
Setup helpers
Both recipes above are built from the same module-level helpers, which
wrap the extension-load and honker_bootstrap() ceremony:
Honker.extension_path # => bundled path (or HONKER_EXTENSION_PATH override)
Honker.load_extension(conn) # enable_load_extension(true) → load → enable_load_extension(false)
Honker.bootstrap(conn) # SELECT honker_bootstrap()
Honker.setup(conn) # load_extension then bootstrap
Honker.sequel_after_connect # proc { |conn| Honker.setup(conn) } for Sequel/Rom/Hanami
See the per-ORM walkthroughs at honker.dev/guides/orm/ruby.
Watcher backends
Honker::Database.new(..., watcher_backend: "polling") accepts the
default polling backend aliases ("polling" / "poll"). Experimental
"kernel" / "shm" requests route through honker-core via the loaded
Honker extension and fail loudly if that extension was not built with
the matching feature.
Quick start
require "honker"
db = Honker::Database.new("app.db")
q = db.queue("emails")
q.enqueue({to: "alice@example.com"})
if (job = q.claim_one("worker-1"))
send_email(job.payload)
job.ack
end
Delayed jobs use run_at::
q.enqueue({to: "later@example.com"}, run_at: Time.now.to_i + 10)
Recurring schedules use schedule::
sched = db.scheduler
sched.add(name: "fast", queue: "emails", schedule: "@every 1s", payload: {kind: "tick"})
Supported schedule forms:
0 3 * * **/2 * * * * *@every 1s
schedule: is the canonical recurring name. cron: still works as a compatibility alias.