Skip to main content

corium_transactor/
lib.rs

1//! Embedded single-writer transaction pipeline and index publisher, plus the
2//! networked transactor process (lease, gRPC services, indexing job).
3
4pub mod lease;
5pub mod node;
6pub mod server;
7
8use corium_core::{EntityId, IndexOrder, KeywordInterner, Partition, Schema};
9use corium_db::{Db, FIRST_USER_ID, Idents};
10use corium_index::Segment;
11use corium_log::{LogError, TransactionLog, TxRecord};
12use corium_store::{BlobStore, RootStore, StoreError};
13use corium_tx::{PreparedTx, TxError, TxItem, prepare};
14use std::{
15    sync::{Arc, Mutex, mpsc},
16    time::{SystemTime, UNIX_EPOCH},
17};
18use thiserror::Error;
19
20/// Result delivered after a transaction is durable and visible.
21#[derive(Clone, Debug)]
22pub struct TxReport {
23    /// Database before the transaction.
24    pub db_before: Db,
25    /// Database including the transaction.
26    pub db_after: Db,
27    /// Prepared transaction and tempid map.
28    pub tx: PreparedTx,
29    /// Commit timestamp.
30    pub tx_instant: i64,
31}
32
33/// Pipeline errors.
34#[derive(Debug, Error)]
35pub enum TransactError {
36    /// Transaction rejected before durability.
37    #[error(transparent)]
38    Tx(#[from] TxError),
39    /// Durable log failed.
40    #[error(transparent)]
41    Log(#[from] LogError),
42    /// Index/root store failed.
43    #[error(transparent)]
44    Store(#[from] StoreError),
45    /// System clock predates the Unix epoch.
46    #[error("system clock is before Unix epoch")]
47    Clock,
48    /// A newer lease version owns the database root; this writer is deposed.
49    #[error("deposed: database root is owned by lease version {published}")]
50    Deposed {
51        /// Lease version found on the published root.
52        published: u64,
53    },
54}
55
56struct State {
57    db: Db,
58    next_user: u64,
59    last_instant: i64,
60    subscribers: Vec<mpsc::Sender<TxReport>>,
61}
62
63/// A serialized, in-process transactor. The log append is the commit point.
64pub struct EmbeddedTransactor {
65    log: Arc<dyn TransactionLog>,
66    state: Mutex<State>,
67}
68impl EmbeddedTransactor {
69    /// Recovers a transactor by replaying the durable log exactly once.
70    ///
71    /// # Errors
72    /// Returns an error when the durable log cannot be replayed.
73    pub fn recover(schema: Schema, log: Arc<dyn TransactionLog>) -> Result<Self, TransactError> {
74        Self::recover_from(Db::new(schema), log)
75    }
76
77    /// Recovers from an empty base database value (schema plus naming) by
78    /// replaying the durable log exactly once.
79    ///
80    /// # Errors
81    /// Returns an error when the durable log cannot be replayed.
82    pub fn recover_from(base: Db, log: Arc<dyn TransactionLog>) -> Result<Self, TransactError> {
83        let mut db = base;
84        let mut last_instant = i64::MIN;
85        for record in log.replay()? {
86            db = db.with_transaction(record.t, &record.datoms);
87            last_instant = last_instant.max(record.tx_instant);
88        }
89        // Allocation must resume past every id that ever appeared in the log,
90        // not just ids with current datoms; otherwise a fully retracted
91        // entity's id would be reused after a restart.
92        let next_user = db
93            .recorded_datoms()
94            .iter()
95            .filter(|d| d.e.partition() == Partition::User as u32)
96            .map(|d| d.e.sequence() + 1)
97            .max()
98            .unwrap_or(FIRST_USER_ID);
99        Ok(Self {
100            log,
101            state: Mutex::new(State {
102                db,
103                next_user,
104                last_instant,
105                subscribers: Vec::new(),
106            }),
107        })
108    }
109    /// Returns the current immutable database value.
110    #[must_use]
111    pub fn db(&self) -> Db {
112        self.state
113            .lock()
114            .unwrap_or_else(std::sync::PoisonError::into_inner)
115            .db
116            .clone()
117    }
118    /// Subscribes to reports for transactions committed after this call.
119    pub fn subscribe(&self) -> mpsc::Receiver<TxReport> {
120        let (tx, rx) = mpsc::channel();
121        self.state
122            .lock()
123            .unwrap_or_else(std::sync::PoisonError::into_inner)
124            .subscribers
125            .push(tx);
126        rx
127    }
128    /// Validates, durably appends, applies, and reports a transaction.
129    ///
130    /// # Errors
131    /// Returns an error for rejected transaction data, clock failure, or when
132    /// the durable append fails. No report is sent on error.
133    pub fn transact(
134        &self,
135        items: impl IntoIterator<Item = TxItem>,
136    ) -> Result<TxReport, TransactError> {
137        let mut state = self
138            .state
139            .lock()
140            .unwrap_or_else(std::sync::PoisonError::into_inner);
141        let before = state.db.clone();
142        let t = before.basis_t() + 1;
143        let tx_id = EntityId::new(Partition::Tx as u32, t);
144        let prepared = prepare(&before, items, tx_id, state.next_user)?;
145        let millis = i64::try_from(
146            SystemTime::now()
147                .duration_since(UNIX_EPOCH)
148                .map_err(|_| TransactError::Clock)?
149                .as_millis(),
150        )
151        .unwrap_or(i64::MAX);
152        let tx_instant = millis.max(state.last_instant.saturating_add(1));
153        self.log.append(&TxRecord {
154            t,
155            tx_instant,
156            datoms: prepared.datoms.clone(),
157        })?;
158        state.db = before.with_transaction(t, &prepared.datoms);
159        state.last_instant = tx_instant;
160        state.next_user = prepared
161            .tempids
162            .values()
163            .filter(|e| e.partition() == Partition::User as u32)
164            .map(|e| e.sequence() + 1)
165            .max()
166            .unwrap_or(state.next_user)
167            .max(state.next_user);
168        let report = TxReport {
169            db_before: before,
170            db_after: state.db.clone(),
171            tx: prepared,
172            tx_instant,
173        };
174        state
175            .subscribers
176            .retain(|subscriber| subscriber.send(report.clone()).is_ok());
177        Ok(report)
178    }
179    /// Replaces the ident/keyword naming attached to the current database
180    /// value (used when the boundary interns new keywords).
181    pub fn update_naming(&self, idents: Idents, interner: KeywordInterner) {
182        let mut state = self
183            .state
184            .lock()
185            .unwrap_or_else(std::sync::PoisonError::into_inner);
186        state.db = state.db.clone().with_naming(idents, interner);
187    }
188
189    /// Builds a consistent snapshot of all four indexes and publishes their blob ids.
190    ///
191    /// Blobs are uploaded before the root CAS. Transactions may continue while the
192    /// immutable snapshot is encoded; a later run indexes any remaining log tail.
193    ///
194    /// Publication is fenced by `lease_version` and monotone in
195    /// `index_basis_t`: a root already published under a newer lease version
196    /// deposes this writer ([`TransactError::Deposed`]); a root at an equal
197    /// or newer basis (or one that wins a concurrent CAS race) leaves this
198    /// snapshot's blobs for garbage collection. The freshly built root is
199    /// returned when it, or a newer basis, is installed.
200    ///
201    /// # Errors
202    /// Returns an error if a blob upload, root read, or fenced publication fails.
203    pub fn publish_indexes(
204        &self,
205        store: &(impl BlobStore + RootStore),
206        root_name: &str,
207        lease_version: u64,
208    ) -> Result<DbRoot, TransactError> {
209        let snapshot = self.db();
210        let datoms = snapshot.datoms();
211        let mut ids = Vec::new();
212        for order in [
213            IndexOrder::Eavt,
214            IndexOrder::Aevt,
215            IndexOrder::Avet,
216            IndexOrder::Vaet,
217        ] {
218            let segment = Segment::build(order, datoms.clone());
219            let mut bytes = Vec::new();
220            for (key, _) in segment.entries() {
221                bytes.extend_from_slice(&(key.len() as u64).to_be_bytes());
222                bytes.extend_from_slice(key);
223            }
224            ids.push(store.put(&bytes)?);
225        }
226        let root = DbRoot {
227            lease_version,
228            index_basis_t: snapshot.basis_t(),
229            roots: Some([
230                ids[0].clone(),
231                ids[1].clone(),
232                ids[2].clone(),
233                ids[3].clone(),
234            ]),
235        };
236        publish_root(store, root_name, &root)?;
237        Ok(root)
238    }
239}
240
241/// Publishes `root` under the fencing rules described on
242/// [`EmbeddedTransactor::publish_indexes`].
243///
244/// # Errors
245/// Returns [`TransactError::Deposed`] when a newer lease version owns the
246/// root, or a store error when the CAS cannot be completed.
247pub fn publish_root(
248    store: &dyn RootStore,
249    root_name: &str,
250    root: &DbRoot,
251) -> Result<(), TransactError> {
252    let encoded = root.encode();
253    loop {
254        let previous = store.get_root(root_name)?;
255        if let Some(stored) = previous.as_deref().and_then(DbRoot::decode) {
256            if stored.lease_version > root.lease_version {
257                return Err(TransactError::Deposed {
258                    published: stored.lease_version,
259                });
260            }
261            if stored.lease_version == root.lease_version
262                && stored.index_basis_t >= root.index_basis_t
263            {
264                return Ok(());
265            }
266        }
267        match store.cas_root(root_name, previous.as_deref(), &encoded) {
268            Ok(()) => return Ok(()),
269            Err(StoreError::CasFailed { .. }) => {}
270            Err(error) => return Err(error.into()),
271        }
272    }
273}
274
275pub use corium_store::{DbRoot, db_root_name};