1pub mod backend;
5pub mod backup;
6pub mod lease;
7pub mod metrics;
8pub mod node;
9pub mod server;
10#[cfg(feature = "cljrs")]
11pub mod txfn;
12
13pub use backend::{LogBackend, NodeStore, StoreSpec};
14
15use corium_core::{EntityId, IndexOrder, KeywordInterner, Partition, Schema};
16use corium_db::{Db, FIRST_USER_ID, Idents};
17use corium_index::Segment;
18use corium_log::{LogError, TransactionLog, TxRecord};
19use corium_store::{BlobStore, RootStore, StoreError};
20use corium_tx::{PreparedTx, TxError, TxItem, prepare};
21use std::{
22 sync::{Arc, Mutex, mpsc},
23 time::{SystemTime, UNIX_EPOCH},
24};
25use thiserror::Error;
26
27#[derive(Clone, Debug)]
29pub struct TxReport {
30 pub db_before: Db,
32 pub db_after: Db,
34 pub tx: PreparedTx,
36 pub tx_instant: i64,
38}
39
40#[derive(Debug, Error)]
42pub enum TransactError {
43 #[error(transparent)]
45 Tx(#[from] TxError),
46 #[error(transparent)]
48 Log(#[from] LogError),
49 #[error(transparent)]
51 Store(#[from] StoreError),
52 #[error("system clock is before Unix epoch")]
54 Clock,
55 #[error("index task failed: {0}")]
57 IndexTask(String),
58 #[error("an asynchronous transaction is already in progress")]
60 AsyncTransactionPending,
61 #[error("deposed: database root is owned by lease version {published}")]
63 Deposed {
64 published: u64,
66 },
67}
68
69struct State {
70 db: Db,
71 next_user: u64,
72 last_instant: i64,
73 subscribers: Vec<mpsc::Sender<TxReport>>,
74 async_pending: bool,
75}
76
77struct AsyncPending<'a> {
78 state: &'a Mutex<State>,
79 active: bool,
80}
81
82impl Drop for AsyncPending<'_> {
83 fn drop(&mut self) {
84 if self.active {
85 self.state
86 .lock()
87 .unwrap_or_else(std::sync::PoisonError::into_inner)
88 .async_pending = false;
89 }
90 }
91}
92
93fn next_user_id<'a>(datoms: impl Iterator<Item = &'a corium_core::Datom>, floor: u64) -> u64 {
96 datoms
97 .filter(|d| d.e.partition() == Partition::User as u32)
98 .map(|d| d.e.sequence() + 1)
99 .fold(floor, u64::max)
100}
101
102pub struct EmbeddedTransactor {
104 log: Arc<dyn TransactionLog>,
105 state: Mutex<State>,
106 async_commit: tokio::sync::Mutex<()>,
107}
108impl EmbeddedTransactor {
109 pub fn recover(schema: Schema, log: Arc<dyn TransactionLog>) -> Result<Self, TransactError> {
114 Self::recover_from(Db::new(schema), log)
115 }
116
117 pub fn recover_from(base: Db, log: Arc<dyn TransactionLog>) -> Result<Self, TransactError> {
123 let mut db = base;
124 let mut last_instant = i64::MIN;
125 for record in log.replay()? {
126 db = db.with_transaction(record.t, &record.datoms);
127 last_instant = last_instant.max(record.tx_instant);
128 }
129 let next_user = next_user_id(db.recorded_datoms(), FIRST_USER_ID);
133 Ok(Self {
134 log,
135 state: Mutex::new(State {
136 db,
137 next_user,
138 last_instant,
139 subscribers: Vec::new(),
140 async_pending: false,
141 }),
142 async_commit: tokio::sync::Mutex::new(()),
143 })
144 }
145
146 pub async fn recover_from_async(
151 base: Db,
152 log: Arc<dyn TransactionLog>,
153 ) -> Result<Self, TransactError> {
154 let records = log.replay_async().await?;
155 Ok(Self::recover_from_records(base, log, records))
156 }
157
158 fn recover_from_records(
159 mut db: Db,
160 log: Arc<dyn TransactionLog>,
161 records: Vec<TxRecord>,
162 ) -> Self {
163 let mut last_instant = i64::MIN;
164 for record in records {
165 db = db.with_transaction(record.t, &record.datoms);
166 last_instant = last_instant.max(record.tx_instant);
167 }
168 let next_user = next_user_id(db.recorded_datoms(), FIRST_USER_ID);
169 Self {
170 log,
171 state: Mutex::new(State {
172 db,
173 next_user,
174 last_instant,
175 subscribers: Vec::new(),
176 async_pending: false,
177 }),
178 async_commit: tokio::sync::Mutex::new(()),
179 }
180 }
181
182 pub fn recover_from_snapshot(
207 snapshot: Db,
208 next_entity_id: u64,
209 last_tx_instant: i64,
210 log: Arc<dyn TransactionLog>,
211 ) -> Result<Self, TransactError> {
212 let mut db = snapshot;
213 let index_basis = db.basis_t();
214 let mut last_instant = last_tx_instant;
215 let mut next_user = next_entity_id.max(FIRST_USER_ID);
218 for record in log.tx_range(index_basis + 1, None)? {
219 db = db.with_transaction(record.t, &record.datoms);
220 last_instant = last_instant.max(record.tx_instant);
221 next_user = next_user.max(next_user_id(record.datoms.iter(), next_user));
222 }
223 Ok(Self {
224 log,
225 state: Mutex::new(State {
226 db,
227 next_user,
228 last_instant,
229 subscribers: Vec::new(),
230 async_pending: false,
231 }),
232 async_commit: tokio::sync::Mutex::new(()),
233 })
234 }
235
236 pub async fn recover_from_snapshot_async(
242 snapshot: Db,
243 next_entity_id: u64,
244 last_tx_instant: i64,
245 log: Arc<dyn TransactionLog>,
246 ) -> Result<Self, TransactError> {
247 let index_basis = snapshot.basis_t();
248 let records = log.tx_range_async(index_basis + 1, None).await?;
249 let mut db = snapshot;
250 let mut last_instant = last_tx_instant;
251 let mut next_user = next_entity_id.max(FIRST_USER_ID);
252 for record in records {
253 db = db.with_transaction(record.t, &record.datoms);
254 last_instant = last_instant.max(record.tx_instant);
255 next_user = next_user.max(next_user_id(record.datoms.iter(), next_user));
256 }
257 Ok(Self {
258 log,
259 state: Mutex::new(State {
260 db,
261 next_user,
262 last_instant,
263 subscribers: Vec::new(),
264 async_pending: false,
265 }),
266 async_commit: tokio::sync::Mutex::new(()),
267 })
268 }
269
270 fn recovery_snapshot(&self) -> (Db, u64, i64) {
274 let state = self
275 .state
276 .lock()
277 .unwrap_or_else(std::sync::PoisonError::into_inner);
278 (state.db.clone(), state.next_user, state.last_instant)
279 }
280 #[must_use]
282 pub fn db(&self) -> Db {
283 self.state
284 .lock()
285 .unwrap_or_else(std::sync::PoisonError::into_inner)
286 .db
287 .clone()
288 }
289 pub fn subscribe(&self) -> mpsc::Receiver<TxReport> {
291 let (tx, rx) = mpsc::channel();
292 self.state
293 .lock()
294 .unwrap_or_else(std::sync::PoisonError::into_inner)
295 .subscribers
296 .push(tx);
297 rx
298 }
299 pub fn transact(
305 &self,
306 items: impl IntoIterator<Item = TxItem>,
307 ) -> Result<TxReport, TransactError> {
308 let mut state = self
309 .state
310 .lock()
311 .unwrap_or_else(std::sync::PoisonError::into_inner);
312 if state.async_pending {
313 return Err(TransactError::AsyncTransactionPending);
314 }
315 let before = state.db.clone();
316 let t = before.basis_t() + 1;
317 let tx_id = EntityId::new(Partition::Tx as u32, t);
318 let prepared = prepare(&before, items, tx_id, state.next_user)?;
319 let millis = i64::try_from(
320 SystemTime::now()
321 .duration_since(UNIX_EPOCH)
322 .map_err(|_| TransactError::Clock)?
323 .as_millis(),
324 )
325 .unwrap_or(i64::MAX);
326 let tx_instant = millis.max(state.last_instant.saturating_add(1));
327 self.log.append(&TxRecord {
328 t,
329 tx_instant,
330 datoms: prepared.datoms.clone(),
331 })?;
332 state.db = before.with_transaction(t, &prepared.datoms);
333 state.last_instant = tx_instant;
334 state.next_user = prepared
335 .tempids
336 .values()
337 .filter(|e| e.partition() == Partition::User as u32)
338 .map(|e| e.sequence() + 1)
339 .max()
340 .unwrap_or(state.next_user)
341 .max(state.next_user);
342 let report = TxReport {
343 db_before: before,
344 db_after: state.db.clone(),
345 tx: prepared,
346 tx_instant,
347 };
348 state
349 .subscribers
350 .retain(|subscriber| subscriber.send(report.clone()).is_ok());
351 Ok(report)
352 }
353
354 pub async fn transact_async(
363 &self,
364 items: impl IntoIterator<Item = TxItem>,
365 ) -> Result<TxReport, TransactError> {
366 let _commit = self.async_commit.lock().await;
367 let (before, prepared, t, tx_instant) = {
368 let mut state = self
369 .state
370 .lock()
371 .unwrap_or_else(std::sync::PoisonError::into_inner);
372 if state.async_pending {
373 return Err(TransactError::AsyncTransactionPending);
374 }
375 let before = state.db.clone();
376 let t = before.basis_t() + 1;
377 let tx_id = EntityId::new(Partition::Tx as u32, t);
378 let prepared = prepare(&before, items, tx_id, state.next_user)?;
379 let millis = i64::try_from(
380 SystemTime::now()
381 .duration_since(UNIX_EPOCH)
382 .map_err(|_| TransactError::Clock)?
383 .as_millis(),
384 )
385 .unwrap_or(i64::MAX);
386 let tx_instant = millis.max(state.last_instant.saturating_add(1));
387 state.async_pending = true;
388 (before, prepared, t, tx_instant)
389 };
390 let record = TxRecord {
391 t,
392 tx_instant,
393 datoms: prepared.datoms.clone(),
394 };
395 let mut pending = AsyncPending {
396 state: &self.state,
397 active: true,
398 };
399 self.log.append_async(&record).await?;
400 let mut state = self
401 .state
402 .lock()
403 .unwrap_or_else(std::sync::PoisonError::into_inner);
404 debug_assert_eq!(state.db.basis_t() + 1, t);
405 state.db = state.db.clone().with_transaction(t, &prepared.datoms);
409 state.last_instant = tx_instant;
410 state.next_user = prepared
411 .tempids
412 .values()
413 .filter(|e| e.partition() == Partition::User as u32)
414 .map(|e| e.sequence() + 1)
415 .max()
416 .unwrap_or(state.next_user)
417 .max(state.next_user);
418 state.async_pending = false;
419 pending.active = false;
420 let report = TxReport {
421 db_before: before,
422 db_after: state.db.clone(),
423 tx: prepared,
424 tx_instant,
425 };
426 state
427 .subscribers
428 .retain(|subscriber| subscriber.send(report.clone()).is_ok());
429 Ok(report)
430 }
431 pub fn update_naming(&self, idents: Idents, interner: KeywordInterner) {
434 let mut state = self
435 .state
436 .lock()
437 .unwrap_or_else(std::sync::PoisonError::into_inner);
438 state.db = state.db.clone().with_naming(idents, interner);
439 }
440
441 pub async fn publish_indexes(
462 &self,
463 store: &(impl BlobStore + RootStore),
464 root_name: &str,
465 lease_version: u64,
466 ) -> Result<DbRoot, TransactError> {
467 let (snapshot, next_entity_id, last_tx_instant) = self.recovery_snapshot();
468 let datoms = snapshot.datoms();
469 let chunked = tokio::task::spawn_blocking(move || {
470 [
471 IndexOrder::Eavt,
472 IndexOrder::Aevt,
473 IndexOrder::Avet,
474 IndexOrder::Vaet,
475 ]
476 .into_iter()
477 .map(|order| {
478 let segment = Segment::build(order, datoms.clone());
479 corium_store::chunk_segment_keys(segment.entries().map(|(key, _)| key.as_slice()))
480 })
481 .collect::<Vec<_>>()
482 })
483 .await
484 .map_err(|error| TransactError::IndexTask(error.to_string()))?;
485 let mut ids = Vec::new();
486 for chunks in chunked {
487 let mut children = Vec::new();
488 for chunk in chunks {
489 children.push(store.put_if_absent(&chunk).await?);
490 }
491 let manifest = corium_store::encode_index_manifest(&children);
492 ids.push(store.put_if_absent(&manifest).await?);
493 }
494 let root = DbRoot {
495 format_version: corium_store::FORMAT_VERSION,
496 lease_version,
497 owner: String::new(),
498 lease_expires_unix_ms: 0,
499 owner_endpoint: String::new(),
500 index_basis_t: snapshot.basis_t(),
501 roots: Some([
502 ids[0].clone(),
503 ids[1].clone(),
504 ids[2].clone(),
505 ids[3].clone(),
506 ]),
507 next_entity_id,
509 last_tx_instant,
510 };
511 publish_root(store, root_name, &root).await?;
512 Ok(root)
513 }
514}
515
516pub async fn publish_root(
523 store: &dyn RootStore,
524 root_name: &str,
525 root: &DbRoot,
526) -> Result<(), TransactError> {
527 loop {
528 let previous = store.get_root(root_name).await?;
529 let stored = previous.as_deref().and_then(DbRoot::decode);
530 let mut next = root.clone();
531 if let Some(stored) = stored {
532 if stored.lease_version > root.lease_version {
533 return Err(TransactError::Deposed {
534 published: stored.lease_version,
535 });
536 }
537 if stored.lease_version == root.lease_version
538 && stored.index_basis_t >= root.index_basis_t
539 {
540 return Ok(());
541 }
542 if stored.lease_version == root.lease_version {
545 next.owner = stored.owner;
546 next.lease_expires_unix_ms = stored.lease_expires_unix_ms;
547 next.owner_endpoint = stored.owner_endpoint;
548 }
549 }
550 match store
551 .cas_root(root_name, previous.as_deref(), &next.encode())
552 .await
553 {
554 Ok(()) => return Ok(()),
555 Err(StoreError::CasFailed { .. }) => {}
556 Err(error) => return Err(error.into()),
557 }
558 }
559}
560
561pub use corium_store::{DbRoot, db_root_name};