1pub 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#[derive(Clone, Debug)]
24pub struct TxReport {
25 pub db_before: Db,
27 pub db_after: Db,
29 pub tx: PreparedTx,
31 pub tx_instant: i64,
33}
34
35#[derive(Debug, Error)]
37pub enum TransactError {
38 #[error(transparent)]
40 Tx(#[from] TxError),
41 #[error(transparent)]
43 Log(#[from] LogError),
44 #[error(transparent)]
46 Store(#[from] StoreError),
47 #[error("system clock is before Unix epoch")]
49 Clock,
50 #[error("index task failed: {0}")]
52 IndexTask(String),
53 #[error("deposed: database root is owned by lease version {published}")]
55 Deposed {
56 published: u64,
58 },
59}
60
61struct State {
62 db: Db,
63 next_user: u64,
64 last_instant: i64,
65 subscribers: Vec<mpsc::Sender<TxReport>>,
66}
67
68pub struct EmbeddedTransactor {
70 log: Arc<dyn TransactionLog>,
71 state: Mutex<State>,
72}
73impl EmbeddedTransactor {
74 pub fn recover(schema: Schema, log: Arc<dyn TransactionLog>) -> Result<Self, TransactError> {
79 Self::recover_from(Db::new(schema), log)
80 }
81
82 pub fn recover_from(base: Db, log: Arc<dyn TransactionLog>) -> Result<Self, TransactError> {
88 let mut db = base;
89 let mut last_instant = i64::MIN;
90 for record in log.replay()? {
91 db = db.with_transaction(record.t, &record.datoms);
92 last_instant = last_instant.max(record.tx_instant);
93 }
94 let next_user = db
98 .recorded_datoms()
99 .iter()
100 .filter(|d| d.e.partition() == Partition::User as u32)
101 .map(|d| d.e.sequence() + 1)
102 .max()
103 .unwrap_or(FIRST_USER_ID);
104 Ok(Self {
105 log,
106 state: Mutex::new(State {
107 db,
108 next_user,
109 last_instant,
110 subscribers: Vec::new(),
111 }),
112 })
113 }
114 #[must_use]
116 pub fn db(&self) -> Db {
117 self.state
118 .lock()
119 .unwrap_or_else(std::sync::PoisonError::into_inner)
120 .db
121 .clone()
122 }
123 pub fn subscribe(&self) -> mpsc::Receiver<TxReport> {
125 let (tx, rx) = mpsc::channel();
126 self.state
127 .lock()
128 .unwrap_or_else(std::sync::PoisonError::into_inner)
129 .subscribers
130 .push(tx);
131 rx
132 }
133 pub fn transact(
139 &self,
140 items: impl IntoIterator<Item = TxItem>,
141 ) -> Result<TxReport, TransactError> {
142 let mut state = self
143 .state
144 .lock()
145 .unwrap_or_else(std::sync::PoisonError::into_inner);
146 let before = state.db.clone();
147 let t = before.basis_t() + 1;
148 let tx_id = EntityId::new(Partition::Tx as u32, t);
149 let prepared = prepare(&before, items, tx_id, state.next_user)?;
150 let millis = i64::try_from(
151 SystemTime::now()
152 .duration_since(UNIX_EPOCH)
153 .map_err(|_| TransactError::Clock)?
154 .as_millis(),
155 )
156 .unwrap_or(i64::MAX);
157 let tx_instant = millis.max(state.last_instant.saturating_add(1));
158 self.log.append(&TxRecord {
159 t,
160 tx_instant,
161 datoms: prepared.datoms.clone(),
162 })?;
163 state.db = before.with_transaction(t, &prepared.datoms);
164 state.last_instant = tx_instant;
165 state.next_user = prepared
166 .tempids
167 .values()
168 .filter(|e| e.partition() == Partition::User as u32)
169 .map(|e| e.sequence() + 1)
170 .max()
171 .unwrap_or(state.next_user)
172 .max(state.next_user);
173 let report = TxReport {
174 db_before: before,
175 db_after: state.db.clone(),
176 tx: prepared,
177 tx_instant,
178 };
179 state
180 .subscribers
181 .retain(|subscriber| subscriber.send(report.clone()).is_ok());
182 Ok(report)
183 }
184 pub fn update_naming(&self, idents: Idents, interner: KeywordInterner) {
187 let mut state = self
188 .state
189 .lock()
190 .unwrap_or_else(std::sync::PoisonError::into_inner);
191 state.db = state.db.clone().with_naming(idents, interner);
192 }
193
194 pub async fn publish_indexes(
209 &self,
210 store: &(impl BlobStore + RootStore),
211 root_name: &str,
212 lease_version: u64,
213 ) -> Result<DbRoot, TransactError> {
214 let snapshot = self.db();
215 let datoms = snapshot.datoms();
216 let segments = tokio::task::spawn_blocking(move || {
217 [
218 IndexOrder::Eavt,
219 IndexOrder::Aevt,
220 IndexOrder::Avet,
221 IndexOrder::Vaet,
222 ]
223 .into_iter()
224 .map(|order| {
225 let segment = Segment::build(order, datoms.clone());
226 let mut bytes = Vec::new();
227 for (key, _) in segment.entries() {
228 bytes.extend_from_slice(&(key.len() as u64).to_be_bytes());
229 bytes.extend_from_slice(key);
230 }
231 bytes
232 })
233 .collect::<Vec<_>>()
234 })
235 .await
236 .map_err(|error| TransactError::IndexTask(error.to_string()))?;
237 let mut ids = Vec::new();
238 for bytes in segments {
239 ids.push(store.put(&bytes).await?);
240 }
241 let root = DbRoot {
242 format_version: corium_store::FORMAT_VERSION,
243 lease_version,
244 owner: String::new(),
245 lease_expires_unix_ms: 0,
246 owner_endpoint: String::new(),
247 index_basis_t: snapshot.basis_t(),
248 roots: Some([
249 ids[0].clone(),
250 ids[1].clone(),
251 ids[2].clone(),
252 ids[3].clone(),
253 ]),
254 };
255 publish_root(store, root_name, &root).await?;
256 Ok(root)
257 }
258}
259
260pub async fn publish_root(
267 store: &dyn RootStore,
268 root_name: &str,
269 root: &DbRoot,
270) -> Result<(), TransactError> {
271 loop {
272 let previous = store.get_root(root_name).await?;
273 let stored = previous.as_deref().and_then(DbRoot::decode);
274 let mut next = root.clone();
275 if let Some(stored) = stored {
276 if stored.lease_version > root.lease_version {
277 return Err(TransactError::Deposed {
278 published: stored.lease_version,
279 });
280 }
281 if stored.lease_version == root.lease_version
282 && stored.index_basis_t >= root.index_basis_t
283 {
284 return Ok(());
285 }
286 if stored.lease_version == root.lease_version {
289 next.owner = stored.owner;
290 next.lease_expires_unix_ms = stored.lease_expires_unix_ms;
291 next.owner_endpoint = stored.owner_endpoint;
292 }
293 }
294 match store
295 .cas_root(root_name, previous.as_deref(), &next.encode())
296 .await
297 {
298 Ok(()) => return Ok(()),
299 Err(StoreError::CasFailed { .. }) => {}
300 Err(error) => return Err(error.into()),
301 }
302 }
303}
304
305pub use corium_store::{DbRoot, db_root_name};