1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
//! 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`](https://crates.io/crates/tephra-server) TCP server and the
//! [`tephra-client`](https://crates.io/crates/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
//!
//! ```no_run
//! use tephra::{
//! AppendCondition, Event, EventType, Position, Query, QueryItem, SegmentConfig,
//! SegmentSet, Tag, Tags, WriteCoordinator, WriterConfig,
//! };
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! // 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();
//! # Ok(())
//! # }
//! ```
//!
//! # 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`](log::set::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.
//!
//! ```no_run
//! use std::sync::Arc;
//! use std::time::Duration;
//! use tephra::{Follower, FollowerConfig, SegmentConfig};
//!
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
//! 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));
//! # Ok(())
//! # }
//! ```
//!
//! 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.
pub use ;
pub use ;
pub use ;
pub use Matches;
pub use ;
pub use ;
pub use ;
pub use ;
/// The crate README and the workspace README, compiled as doctests so their code samples
/// cannot drift from the API. These items exist only during doctest builds (`cfg(doctest)`),
/// so they never appear in the published documentation.
;
;