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