I wrote MQDB, an MQTT broker with an embedded reactive database, built on Fjall #279
fabracht
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
I've been building MQDB, a Rust MQTT 5.0 broker that embeds a reactive database directly into the broker process. Every write produces an MQTT event, and every MQTT topic can be a query or a CRUD endpoint. Fjall is the storage engine behind it on native targets.
I just wanted to share how Fjall is being used in case it's interesting, and to say thanks for the library.
Where Fjall fits
Storage in MQDB is behind a
StorageBackendtrait so the same code runs on:fjall = "3.0")The Fjall backend is one file (
crates/mqdb-core/src/storage/fjall_backend.rs) and uses a singleDatabasewith oneKeyspacecalled"main". Everything in MQDB — entities, secondary indexes, subscriptions, sessions, retained messages, dedup cache, Raft log, partition state — lives in that one keyspace, separated by key prefixes:Single keyspace + prefix encoding turned out to be a really comfortable model to build on. Prefix scans are the workhorse: listing entities, walking an index, paginating, counting — they're all
prefix(...)orrange(start..)overdb.snapshot().What I lean on Fjall for
Snapshots for reads. Every read path (
get,prefix_scan,prefix_count,range_scan) takes a freshdb.snapshot()and iterates. This gives me consistent reads while writes happen concurrently, without me having to think about it.Atomic batches with preconditions. This is the most useful primitive for me. MQDB needs optimistic concurrency for things like unique constraints and CAS-style updates, so the batch wrapper exposes an
expect_value(key, expected)method. On commit it re-reads each precondition under a fresh snapshot and bails out with aConflictif any value has changed; otherwise it submits the batched insert/remove operations throughdb.batch(). Single API, no locks above the storage layer.Configurable durability. MQDB has three modes:
Immediate(callpersist(PersistMode::SyncAll)after every write),PeriodicMs(n), andNone. Immediate mode is what you want for the Raft log; periodic is enough for most user data.Cheap iteration. Pagination uses
range(start..)with a sentinel-byte trick (after_key + 0x00) and breaks out as soon as the prefix no longer matches. Counting is a key-only scan. Both have been fast enough that I haven't had to add caching anywhere.Things I appreciated as a user of Fjall
Slicereturning by reference fromsnapshot.getmade the precondition check a one-liner.Storage::memory()vsStorage::open(path, durability). No conditional logic anywhere upstream.Numbers
On a single node, sync-on-every-write disabled, MQDB does roughly:
These are end-to-end through MQTT (parse, route, write, ack), so the storage layer is comfortably faster than what the broker can feed it. I've never seen Fjall be the bottleneck.
What MQDB itself is, briefly
A single binary that speaks MQTT 5.0 and exposes a reactive database over it. You publish to
$DB/users/createwith a JSON payload, you subscribe to$DB/users/events/#to watch changes, you query with filters and indexes. Schemas, unique constraints, foreign keys with cascade, secondary indexes, TTLs, consumer groups, persistent wildcard subscriptions. Agent edition is open source (AGPL); there's also a clustered edition with Raft + QUIC + 256 partitions on top of the same Fjall-backed core.Repo: MQDB repo here
Storage backend file:
crates/mqdb-core/src/storage/fjall_backend.rsThanks for Fjall — it's been a really pleasant dependency to live with.
Beta Was this translation helpful? Give feedback.
All reactions