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