Skip to content

Embedded

Running the server and talking to it with the client is the common path, and most of this site assumes it. You can also link the engine (tephra) directly into a Rust process and skip the wire entirely. This page is the whole of what that changes.

Embed when the code making decisions and the store live in the same process and you want no network hop: a single-process service, a command-line tool, a test harness. You give up the things the server provides: remote access over TCP, multiple client processes against one store, and the language-agnostic protobuf protocol. You gain the removal of serialisation and a socket from the write path.

What you do not gain is a second writer. The engine is one logical writer per store whether you embed it or run the server, so embedding is not a way around the single-writer constraint. It is the same store, reached directly.

SegmentSet::open opens (or creates) a log directory, and WriteCoordinator::start starts the single writer thread and hands back a cloneable WriteHandle. SegmentConfig and SegmentSet are re-exported at the crate root.

// Open (or create) a log directory and start the single-writer coordinator.
// SegmentConfig and SegmentSet are re-exported at the crate root.
let set =
SegmentSet::open(dir.path(), SegmentConfig::new(256 * 1024 * 1024)).expect("open store");
let (coordinator, handle) =
WriteCoordinator::start(set, WriterConfig::default()).expect("start coordinator");

There is no library default segment size: you pass one to SegmentConfig::new. The 256 MiB figure quoted elsewhere is the server’s default, not the engine’s.

The engine’s Event is a packed, zero-copy codec type, distinct from the client’s owned Event. It is built from &EventType, &Tags, and a payload, and the in-memory layout is the on-disk layout, so the events you build are the bytes that get written.

// The engine's packed Event is built from &EventType, &Tags, and an opaque payload.
let ty = EventType::new("CourseOpened").expect("type");
let tags = Tags::new([Tag::new("course:c1").expect("tag")]).expect("tags");
let event = Event::new(&ty, &tags, br#"{"course":"c1","seats":30}"#).expect("encode");
// Guard the append so it fails if any event already carries course:c1 (a uniqueness guard).
let guard = AppendCondition::new(Query::item(QueryItem::with_tags(
Tags::new([Tag::new("course:c1").expect("tag")]).expect("tags"),
)));
let range = handle.append(vec![event], Some(guard)).expect("append");

append blocks until the batch is durable and returns the position range. Under the async cargo feature there is also an append_async; it is feature-gated and off by default.

Reads run on the caller’s own thread over a snapshot the writer publishes at each commit. The read handle’s read returns Reads, a lending iterator: it yields a borrow of its own buffer per item, which is why it is consumed with while let rather than a for loop.

// Reads run on the caller's own thread over a snapshot published at each commit. Reads is 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").expect("tag")]).expect("tags"),
));
let mut reads = handle.read(query, Position::ZERO);
let mut count = 0usize;
while let Some(item) = reads.next() {
let seq = item.expect("decode");
println!("{} {}", seq.position, seq.event.event_type());
count += 1;
}

Reads::next is not std::iter::Iterator::next. That is deliberate: a standard iterator cannot yield a borrow of itself per item, and yielding an owned value would force an allocation and a copy on the highest-volume read path. The client’s read stream, by contrast, is a plain Iterator, because it hands back owned events decoded off the wire.

WriteCoordinator::shutdown signals the writer thread, joins it, and returns the SegmentSet. Dropping the coordinator does the same. Either way the writer thread is joined before the process exits, so no committed batch is left unflushed.

For the field-by-field configuration surface, durability semantics, and what recovery does on restart, see Operations. For why the engine is shaped this way, see Architecture.