Services and events
Give each service a database of its own, and let other services follow its writes as events.
A database per service
Run one tenant node and give each service its own tenant: a separate file that no other service can read. A token then decides which collections and rows each caller may touch.
Details
Each service keeps its own data and tells the others what changed. fenecdb gives a service a database of its own on a shared node, a token that reaches only that, and the writes on its disk as a stream of events — which is what an outbox table and a relay are usually built for.
A tenant node (fenec-server --dir) keeps a file per tenant, and
a tenant is named by the path, /t/<tenant>/ — never by
the query, so one service cannot read another's rows. Give each service its tenant,
and a router (fenec-shard) in front of the nodes when there are
several: see Tenants and sharding.
Within a database, a JSON Web Token held to a policy gives each caller
the collections, and the rows, its role may read or write
(Scoped access). A service that only
reads can be pointed at a replica, which refuses every write
(Replication), or hold a token whose policy
grants read alone.
Every write as an event
Turn on --cdc and every write that reaches disk becomes an
event a consumer can read. You do not need an outbox table: a batch's
rows are announced together.
fenec-server --file orders.fenec --http 127.0.0.1:8080 --cdc
# a consumer the server keeps the cursor of, made once
curl -X POST http://127.0.0.1:8080/_changes/consumers/billingDetails
An outbox table exists because a service must change its data and
announce the change together, or not at all. A fenecdb
/batch is one record on disk, so the rows it
writes are announced together already: GET /_changes hands
over every write once it is on disk, the documents with it
(Change data capture). The collection is
its own outbox. Where the event should say something the rows do not —
a command, an intent — write it into a collection of its own in the same
/batch, and it comes in the same record.
A consumer in Python:
from fenecdb import Client
db = Client("http://127.0.0.1:8080", token="...")
for batch in db.follow("billing"): # committed when the loop comes back
for change in batch:
if change["collection"] == "orders" and change["op"] == "put":
bill(change["doc"]) # an exception: the batch is read againOn to Kafka, or anywhere
The relay passes a consumer's events on, as JSON lines or to a webhook. Pipe the lines into Kafka with any tool that writes to a topic.
python -m fenecdb.relay http://127.0.0.1:8080 --consumer kafka \
| kcat -P -b broker:9092 -t ordersDetails
python -m fenecdb.relay reads a consumer's writes and hands
them on — to standard output, one JSON object a line, or to a webhook —
and moves the cursor only once they are out. Kafka takes them through any
tool that writes lines to a topic, as above; a webhook:
python -m fenecdb.relay http://127.0.0.1:8080 --consumer search \
--to https://indexer.internal/hookDelivery and replay
Each event arrives at least once, in order within one database. Make your handler safe to run twice; a consumer that falls too far behind starts over from a fresh read.
Details
- At least once. A consumer that crashes between reading and
committing reads those writes again. Key what you do by
seq, or by the collection andid: aputcarries the whole document, so applying one twice leaves what applying it once did. - In order, one database at a time.
seqis the database's change counter; a batch's writes come together. There is no order across tenants. - Kept for a while, not for ever. The server keeps as many writes
as
--replication-bufferholds, 64 MiB unless given. A consumer further behind is answered 410, never handed writes with a gap in the middle. To start over: take the last write on disk (Fenec-NextofGET /_changeswith nosince), read the collections whole, then go on from that number — a write read in both is applied twice, which aputby id makes harmless. - What is on disk. A write the server lost in a crash was never handed to anyone, as it was never sent to a replica.
- Reading one's own writes. A service that writes on the primary
and reads on a replica sends the write's
Fenec-Seqback asFenec-After, and the replica answers once it holds the write (Reading your own writes). - Retries. A service that sends a write again after a timeout
sends it with an
Idempotency-Key, and a write that landed is answered as before rather than made twice (Retrying a write).
What it is not
It is not a message broker. To keep events for a long time or share them with many teams, relay them into one.
Details
A consumer reads one database's writes; there are no topics, partitions or retention of their own, and a server keeps its writes in memory up to the buffer. For fan-out to many teams, a long history or events that outlive the database, relay them into a broker and let it keep them.