Expand description
A DCB-compliant, immutable event store with global ordering.
Tephra is a Dynamic Consistency Boundary (DCB) event store. Instead of a static consistency
boundary baked into an aggregate, the boundary is derived per decision from a Query.
Events carry an EventType plus a set of Tags, so one event can belong to several
entities at once, and a decision reads exactly the events it depends on and guards exactly
those on append (an AppendCondition).
This crate is the embedded engine: the durable log, the single writer, the index, and the
read paths. Use it directly in-process, or reach it over the network with the
tephra-server TCP server and the
tephra-client client.
§Design
The log is the source of truth and everything else is derived. Data is written once, never
updated and never deleted, keyed by a dense monotonic Position assigned by the single
writer. Indexes need no write-ahead log and no fsync on the write path, because they can be
rebuilt by replaying the log. The ARCHITECTURE.md document in the repository records the
full rationale and the alternatives that were rejected.
§Example
use tephra::{
AppendCondition, Event, EventType, Position, Query, QueryItem, SegmentConfig,
SegmentSet, Tag, Tags, WriteCoordinator, WriterConfig,
};
// Open (or create) a log directory and start the single-writer coordinator.
let set = SegmentSet::open("tephra-data", SegmentConfig::new(256 * 1024 * 1024))?;
let (coordinator, handle) = WriteCoordinator::start(set, WriterConfig::default())?;
// Build a packed event, then append it guarded so it fails if course:c1 already exists.
let ty = EventType::new("CourseOpened")?;
let tags = Tags::new([Tag::new("course:c1")?])?;
let event = Event::new(&ty, &tags, br#"{"course":"c1","seats":30}"#)?;
let guard = AppendCondition::new(Query::item(QueryItem::with_tags(
Tags::new([Tag::new("course:c1")?])?,
)));
handle.append(vec![event], Some(guard))?;
// Reads run on the caller's thread over a snapshot published at each commit. `read` returns
// a lending iterator, so it is consumed with `while let`, not a `for` loop.
let query = Query::item(QueryItem::with_tags(Tags::new([Tag::new("course:c1")?])?));
let mut reads = handle.read(&query, Position::ZERO, None);
while let Some(item) = reads.next() {
let seq = item?;
println!("{} {}", seq.position, seq.event.event_type());
}
// Shutdown joins the writer thread and flushes cleanly.
coordinator.shutdown();§One writer per directory
A data directory takes one writer at a time, and that is now enforced: opening a
SegmentSet read-write takes a lock on a LOCK file in the directory and holds it for
the set’s lifetime. A second writer, in this process or any other, is refused with
LogError::Locked rather than quietly corrupting the log.
The kernel releases it however the process exits, so a leftover LOCK file blocks
nothing, and because it is a POSIX record lock rather than a descriptor-based one, a
forked child does not inherit it.
§Reading from a second process
Follower opens the same directory read-only and tracks a live writer, without
creating, deleting or writing anything and without taking any lock. It hands out an
ordinary ReadHandle, so queries, backward reads and subscriptions all work as they do
against a writer.
use std::sync::Arc;
use std::time::Duration;
use tephra::{Follower, FollowerConfig, SegmentConfig};
let follower = Follower::open(
"tephra-data",
FollowerConfig::new(SegmentConfig::new(256 * 1024 * 1024)),
)?;
// Advance to whatever the writer has committed, then read at that tip.
let tip = follower.refresh()?;
println!("following up to {tip}");
// Or let a background thread advance it, which is what makes subscriptions work.
let follower = Arc::new(follower);
let _poller = follower.poll_every(Duration::from_millis(10));What a follower sees is always a committed prefix: gap-free, duplicate-free, and only
growing. It is not a durability oracle, though, and it lags. See the follow module
docs for the full argument and the caveats before relying on one.
Re-exports§
pub use event::Event;pub use event::EventRef;pub use follow::Follower;pub use follow::FollowerConfig;pub use follow::FollowerError;pub use follow::FollowerPoller;pub use follow::PollerHealth;pub use log::set::PositionRange;pub use log::set::Refreshed;pub use log::set::SegmentConfig;pub use log::set::SegmentSet;pub use query::Matches;pub use read::DEFAULT_MAX_BATCH_EVENTS;pub use read::ReadConfig;pub use read::ReadError;pub use read::ReadHandle;pub use read::Subscription;pub use read::WaitOutcome;pub use writer::AppendError;pub use writer::ConflictClause;pub use writer::ConflictSite;pub use writer::WriteCoordinator;pub use writer::WriteHandle;pub use writer::WriterConfig;
Modules§
- event
- Event codec.
- follow
- Reading a store that another process is writing.
- index
- Layer 3: the derived index.
- log
- query
- Query match predicate over an encoded event.
- read
- Layer 5: off-thread read paths.
- writer
- Layer 2: the write coordinator.
Structs§
- Append
Condition - The guard on an
appendcall: two independent checks, OR’d, so the append is rejected if either fires. - Event
Type - An event type. An arbitrary opaque, non-empty string (the spec never parses it),
stored as an exact-sized
Box<str>. - Position
- Query
Item - One alternative in a
Query: a type constraint AND a tag constraint. - Tag
- A tag, e.g.
course:c1. An arbitrary opaque, non-empty string (the spec does not split it into key/value), stored as an exact-sizedBox<str>. LikeEventType, it has noDeref; useas_str,AsRef<str>, orDisplay. - Tags
- A sorted, duplicate-free set of tags.
Enums§
- Name
Error - Error constructing an
EventTypeorTag. - Query
- A query: a set of
QueryItems OR’d together, or the catch-allQuery::All. - Tags
Error - Error constructing
Tags.
Constants§
- MAX_
NAME_ LEN - Maximum length, in bytes, of an
EventTypeorTag. Each is stored with a fixed-widthu16length in the engine’s encoded header, so the field capacity is the limit.