1use std::collections::{BTreeSet, HashMap};
6use std::path::PathBuf;
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
10
11use corium_core::{KeywordInterner, Schema};
12use corium_db::{Db, Idents};
13use corium_log::{LogError, TransactionLog, TxRecord};
14use corium_protocol::codec::{self, CodecError};
15use corium_protocol::pb;
16use corium_protocol::schemaform::{SchemaFormError, schema_from_edn};
17use corium_protocol::txforms::{TxFormError, tx_items_from_edn};
18use corium_query::edn::Edn;
19use corium_store::{RootStore, StoreError, mark_and_sweep_retained, meta_root_name};
20use thiserror::Error;
21use tokio::sync::{broadcast, watch};
22use tracing::Instrument;
23
24use crate::backend::{LogBackend, NodeStore, StoreSpec};
25use crate::lease::{self, Lease, LeaseError};
26use crate::metrics::Metrics;
27use crate::{DbRoot, EmbeddedTransactor, TransactError, db_root_name};
28
29pub trait TxFnExpander: Send + Sync {
34 fn expand(&self, db: &Db, forms: Vec<Edn>) -> Result<Vec<Edn>, String>;
41}
42
43#[derive(Clone)]
45pub struct NodeConfig {
46 pub store: StoreSpec,
48 pub data_dir: PathBuf,
51 pub owner: String,
53 pub lease_ttl_ms: i64,
55 pub lease_wait_ms: i64,
57 pub ha: bool,
61 pub advertise: Option<String>,
64 pub index_interval: Duration,
66 pub index_backoff: u32,
72 pub index_tail_threshold: u64,
76 pub index_tail_deadline: Duration,
78 pub heartbeat_interval: Duration,
80 pub gc_interval: Option<Duration>,
82 pub gc_retention: Duration,
84 pub tx_fn_expander: Option<Arc<dyn TxFnExpander>>,
86}
87
88impl std::fmt::Debug for NodeConfig {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("NodeConfig")
91 .field("store", &self.store)
92 .field("data_dir", &self.data_dir)
93 .field("owner", &self.owner)
94 .field("lease_ttl_ms", &self.lease_ttl_ms)
95 .field("lease_wait_ms", &self.lease_wait_ms)
96 .field("ha", &self.ha)
97 .field("advertise", &self.advertise)
98 .field("index_interval", &self.index_interval)
99 .field("index_backoff", &self.index_backoff)
100 .field("index_tail_threshold", &self.index_tail_threshold)
101 .field("index_tail_deadline", &self.index_tail_deadline)
102 .field("heartbeat_interval", &self.heartbeat_interval)
103 .field("gc_interval", &self.gc_interval)
104 .field("gc_retention", &self.gc_retention)
105 .field("tx_fn_expander", &self.tx_fn_expander.is_some())
106 .finish()
107 }
108}
109
110impl NodeConfig {
111 #[must_use]
113 pub fn new(data_dir: PathBuf) -> Self {
114 Self {
115 store: StoreSpec::Fs,
116 data_dir,
117 owner: format!(
118 "transactor-{}",
119 std::env::var("HOSTNAME").unwrap_or_else(|_| "local".into())
120 ),
121 lease_ttl_ms: 5_000,
122 lease_wait_ms: 15_000,
123 ha: false,
124 advertise: None,
125 index_interval: Duration::from_secs(5),
126 index_backoff: 4,
127 index_tail_threshold: 0,
128 index_tail_deadline: Duration::from_secs(60),
129 heartbeat_interval: Duration::from_secs(10),
130 gc_interval: Some(Duration::from_secs(60 * 60)),
131 gc_retention: Duration::from_secs(72 * 60 * 60),
132 tx_fn_expander: None,
133 }
134 }
135}
136
137#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150pub struct IndexPolicy {
151 pub interval: Duration,
153 pub backoff: u32,
156 pub tail_threshold: u64,
159 pub tail_deadline: Duration,
162}
163
164#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
166pub struct IndexPolicyUpdate {
167 pub interval: Option<Duration>,
169 pub backoff: Option<u32>,
171 pub tail_threshold: Option<u64>,
173 pub tail_deadline: Option<Duration>,
175}
176
177impl IndexPolicy {
178 fn from_config(config: &NodeConfig) -> Self {
179 Self {
180 interval: config.index_interval,
181 backoff: config.index_backoff,
182 tail_threshold: config.index_tail_threshold,
183 tail_deadline: config.index_tail_deadline,
184 }
185 }
186
187 fn apply(&mut self, update: IndexPolicyUpdate) {
188 if let Some(interval) = update.interval {
189 self.interval = interval;
190 }
191 if let Some(backoff) = update.backoff {
192 self.backoff = backoff;
193 }
194 if let Some(tail_threshold) = update.tail_threshold {
195 self.tail_threshold = tail_threshold;
196 }
197 if let Some(tail_deadline) = update.tail_deadline {
198 self.tail_deadline = tail_deadline;
199 }
200 }
201
202 fn due(&self, since_publish: Duration, last_duration: Duration, pending: Option<u64>) -> bool {
209 let floor = self
210 .interval
211 .max(last_duration.saturating_mul(self.backoff));
212 if since_publish < floor {
213 return false;
214 }
215 match pending {
216 Some(pending) if pending < self.tail_threshold => since_publish >= self.tail_deadline,
217 _ => true,
218 }
219 }
220}
221
222#[derive(Debug, Error)]
224pub enum NodeError {
225 #[error("unknown database {0:?}")]
227 UnknownDb(String),
228 #[error("invalid database name {0:?}")]
230 InvalidName(String),
231 #[error("storage format {found} is newer than supported format {supported}")]
233 UnsupportedFormat {
234 found: u32,
236 supported: u32,
238 },
239 #[error("deposed: write lease for {0:?} is held elsewhere")]
241 Deposed(String),
242 #[error("standby for {db:?}: lease held by {owner} at {endpoint:?}")]
245 Standby {
246 db: String,
248 owner: String,
250 endpoint: String,
252 },
253 #[error(transparent)]
255 Codec(#[from] CodecError),
256 #[error(transparent)]
258 TxForm(#[from] TxFormError),
259 #[error(transparent)]
261 SchemaForm(#[from] SchemaFormError),
262 #[error(transparent)]
264 Transact(#[from] TransactError),
265 #[error(transparent)]
267 Store(#[from] StoreError),
268 #[error(transparent)]
270 Log(#[from] LogError),
271 #[error(transparent)]
273 Lease(#[from] LeaseError),
274 #[error("bad request: {0}")]
276 BadRequest(String),
277}
278
279struct Naming {
280 schema: Schema,
281 idents: Idents,
282 interner: KeywordInterner,
283}
284
285pub struct DbState {
287 name: String,
288 transactor: EmbeddedTransactor,
289 log: Arc<dyn TransactionLog>,
290 naming: Mutex<Naming>,
291 commit: tokio::sync::Mutex<()>,
292 broadcast: broadcast::Sender<pb::subscribe_item::Item>,
293 basis: watch::Sender<u64>,
294 index_basis: AtomicU64,
295 index_policy: Mutex<IndexPolicy>,
296 held_lease: Mutex<Lease>,
297 deposed: AtomicBool,
298}
299
300impl DbState {
301 #[must_use]
303 pub fn name(&self) -> &str {
304 &self.name
305 }
306
307 #[must_use]
309 pub fn db(&self) -> Db {
310 self.transactor.db()
311 }
312
313 #[must_use]
315 pub fn basis_watch(&self) -> watch::Receiver<u64> {
316 self.basis.subscribe()
317 }
318
319 #[must_use]
322 pub fn stream_items(&self) -> broadcast::Receiver<pb::subscribe_item::Item> {
323 self.broadcast.subscribe()
324 }
325
326 #[must_use]
328 pub fn index_basis(&self) -> u64 {
329 self.index_basis.load(Ordering::Acquire)
330 }
331
332 #[must_use]
334 pub fn index_policy(&self) -> IndexPolicy {
335 *self
336 .index_policy
337 .lock()
338 .unwrap_or_else(std::sync::PoisonError::into_inner)
339 }
340
341 #[must_use]
343 pub fn lease(&self) -> Lease {
344 self.held_lease
345 .lock()
346 .unwrap_or_else(std::sync::PoisonError::into_inner)
347 .clone()
348 }
349
350 #[must_use]
353 pub fn handshake_snapshot(&self) -> (Vec<u8>, KeywordInterner) {
354 let naming = self
355 .naming
356 .lock()
357 .unwrap_or_else(std::sync::PoisonError::into_inner);
358 (
359 codec::encode_schema(&naming.schema, &naming.idents),
360 naming.interner.clone(),
361 )
362 }
363
364 pub fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, NodeError> {
369 Ok(self.log.tx_range(start, end)?)
370 }
371
372 async fn check_lease(&self, store: &dyn RootStore) -> Result<Lease, NodeError> {
375 if self.deposed.load(Ordering::Acquire) {
376 return Err(NodeError::Deposed(self.name.clone()));
377 }
378 let held = self.lease();
379 match lease::verify(store, &self.name, &held).await {
380 Ok(()) => Ok(held),
381 Err(LeaseError::Lost) => {
382 self.deposed.store(true, Ordering::Release);
383 Err(NodeError::Deposed(self.name.clone()))
384 }
385 Err(error) => Err(error.into()),
386 }
387 }
388}
389
390pub struct TransactorNode {
392 config: NodeConfig,
393 store: Arc<NodeStore>,
394 log_backend: LogBackend,
395 dbs: std::sync::RwLock<HashMap<String, Arc<DbState>>>,
396 standby: std::sync::RwLock<BTreeSet<String>>,
399 gc_lock: tokio::sync::Mutex<()>,
400 fork_lock: tokio::sync::Mutex<()>,
403 metrics: Metrics,
404 shutdown: watch::Sender<Option<String>>,
405}
406
407fn now_unix_ms() -> i64 {
408 i64::try_from(
409 SystemTime::now()
410 .duration_since(UNIX_EPOCH)
411 .unwrap_or_default()
412 .as_millis(),
413 )
414 .unwrap_or(i64::MAX)
415}
416
417fn valid_db_name(name: &str) -> bool {
418 !name.is_empty()
419 && name.len() <= 128
420 && name
421 .bytes()
422 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
423}
424
425impl TransactorNode {
426 pub async fn open(config: NodeConfig) -> Result<Arc<Self>, NodeError> {
434 let store = Arc::new(NodeStore::open(&config.store, &config.data_dir).await?);
435 let log_backend = LogBackend::for_spec(&config.store, &config.data_dir, Arc::clone(&store));
436 let node = Arc::new(Self {
437 config,
438 store,
439 log_backend,
440 dbs: std::sync::RwLock::new(HashMap::new()),
441 standby: std::sync::RwLock::new(BTreeSet::new()),
442 gc_lock: tokio::sync::Mutex::new(()),
443 fork_lock: tokio::sync::Mutex::new(()),
444 metrics: Metrics::default(),
445 shutdown: watch::channel(None).0,
446 });
447 let names: Vec<String> = node
448 .store
449 .list_roots("meta:")
450 .await?
451 .into_iter()
452 .filter_map(|root| root.strip_prefix("meta:").map(str::to_owned))
453 .collect();
454 for name in names {
455 match node.open_db(&name).await {
456 Ok(state) => {
457 node.dbs
458 .write()
459 .unwrap_or_else(std::sync::PoisonError::into_inner)
460 .insert(name, state);
461 }
462 Err(NodeError::Lease(LeaseError::Held { owner, .. })) if node.config.ha => {
463 tracing::info!(db = %name, %owner, "standing by; lease held elsewhere");
464 node.standby
465 .write()
466 .unwrap_or_else(std::sync::PoisonError::into_inner)
467 .insert(name);
468 }
469 Err(error) => return Err(error),
470 }
471 }
472 node.spawn_standby_poller();
473 node.spawn_scheduled_gc();
474 Ok(node)
475 }
476
477 #[must_use]
479 pub fn store(&self) -> &Arc<NodeStore> {
480 &self.store
481 }
482
483 #[must_use]
485 pub fn config(&self) -> &NodeConfig {
486 &self.config
487 }
488
489 #[must_use]
491 pub const fn metrics(&self) -> &Metrics {
492 &self.metrics
493 }
494
495 fn spawn_scheduled_gc(self: &Arc<Self>) {
496 let Some(interval) = self.config.gc_interval else {
497 return;
498 };
499 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
500 return;
503 };
504 let node = Arc::clone(self);
505 runtime.spawn(async move {
506 let mut ticker = tokio::time::interval(interval);
507 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
508 ticker.tick().await;
510 loop {
511 ticker.tick().await;
512 if let Err(error) = node.gc_deleted().await {
513 tracing::warn!(%error, "scheduled garbage collection failed");
514 }
515 }
516 });
517 }
518
519 #[must_use]
521 pub fn shutdown_watch(&self) -> watch::Receiver<Option<String>> {
522 self.shutdown.subscribe()
523 }
524
525 fn depose(&self, state: &DbState, reason: &str) {
529 state.deposed.store(true, Ordering::Release);
530 if self.config.ha {
531 tracing::warn!(db = %state.name, reason, "deposed; returning to standby");
532 self.dbs
533 .write()
534 .unwrap_or_else(std::sync::PoisonError::into_inner)
535 .remove(&state.name);
536 self.standby
537 .write()
538 .unwrap_or_else(std::sync::PoisonError::into_inner)
539 .insert(state.name.clone());
540 } else {
541 let _ = self
542 .shutdown
543 .send(Some(format!("database {:?}: {reason}", state.name)));
544 }
545 }
546
547 fn advertised(&self) -> &str {
548 self.config.advertise.as_deref().unwrap_or("")
549 }
550
551 async fn acquire_lease(&self, name: &str) -> Result<Lease, NodeError> {
555 let deadline = now_unix_ms() + self.config.lease_wait_ms;
556 loop {
557 match lease::acquire(
558 self.store.as_ref(),
559 name,
560 &self.config.owner,
561 self.advertised(),
562 self.config.lease_ttl_ms,
563 now_unix_ms(),
564 )
565 .await
566 {
567 Ok(held) => return Ok(held),
568 Err(LeaseError::Held { .. }) if !self.config.ha && now_unix_ms() < deadline => {
569 tokio::time::sleep(Duration::from_millis(200)).await;
570 }
571 Err(error) => return Err(error.into()),
572 }
573 }
574 }
575
576 async fn open_db(self: &Arc<Self>, name: &str) -> Result<Arc<DbState>, NodeError> {
577 let meta = self
578 .store
579 .get_root(&meta_root_name(name))
580 .await?
581 .ok_or_else(|| NodeError::UnknownDb(name.to_owned()))?;
582 let (schema, idents, interner) = codec::decode_metadata(&meta)?;
583 let root_name = db_root_name(name);
584 let current = self
585 .store
586 .get_root(&root_name)
587 .await?
588 .as_deref()
589 .and_then(DbRoot::decode);
590 if let Some(root) = ¤t
591 && root.format_version > corium_store::FORMAT_VERSION
592 {
593 return Err(NodeError::UnsupportedFormat {
594 found: root.format_version,
595 supported: corium_store::FORMAT_VERSION,
596 });
597 }
598 let held = self.acquire_lease(name).await?;
602 let log = self.log_backend.open(name, held.version)?;
605 let base = Db::new(schema.clone()).with_naming(idents.clone(), interner.clone());
606 let transactor = EmbeddedTransactor::recover_from(base, Arc::clone(&log))?;
607 let basis_t = transactor.db().basis_t();
608 let index_basis = self
609 .store
610 .get_root(&root_name)
611 .await?
612 .as_deref()
613 .and_then(DbRoot::decode)
614 .map_or(0, |root| root.index_basis_t);
615 let state = Arc::new(DbState {
616 name: name.to_owned(),
617 transactor,
618 log,
619 naming: Mutex::new(Naming {
620 schema,
621 idents,
622 interner,
623 }),
624 commit: tokio::sync::Mutex::new(()),
625 broadcast: broadcast::channel(1024).0,
626 basis: watch::channel(basis_t).0,
627 index_basis: AtomicU64::new(index_basis),
628 index_policy: Mutex::new(IndexPolicy::from_config(&self.config)),
629 held_lease: Mutex::new(held),
630 deposed: AtomicBool::new(false),
631 });
632 self.spawn_maintenance(&state);
633 Ok(state)
634 }
635
636 fn spawn_standby_poller(self: &Arc<Self>) {
642 if !self.config.ha {
643 return;
644 }
645 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
646 return;
647 };
648 let ttl = self.config.lease_ttl_ms;
649 let poll_every = Duration::from_millis(u64::try_from(ttl / 3).unwrap_or(1).max(50));
650 let node = Arc::clone(self);
651 runtime.spawn(async move {
652 let mut ticker = tokio::time::interval(poll_every);
653 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
654 loop {
655 ticker.tick().await;
656 if let Err(error) = node.standby_scan().await {
657 tracing::warn!(%error, "standby scan failed");
658 }
659 }
660 });
661 }
662
663 async fn standby_scan(self: &Arc<Self>) -> Result<(), NodeError> {
666 let names: Vec<String> = self
667 .store
668 .list_roots("meta:")
669 .await?
670 .into_iter()
671 .filter_map(|root| root.strip_prefix("meta:").map(str::to_owned))
672 .collect();
673 {
674 let mut standby = self
675 .standby
676 .write()
677 .unwrap_or_else(std::sync::PoisonError::into_inner);
678 standby.retain(|name| names.contains(name));
679 }
680 for name in names {
681 if self
682 .dbs
683 .read()
684 .unwrap_or_else(std::sync::PoisonError::into_inner)
685 .contains_key(&name)
686 {
687 continue;
688 }
689 match self.open_db(&name).await {
690 Ok(state) => {
691 tracing::info!(db = %name, owner = %self.config.owner, "standby took over write lease");
692 self.standby
693 .write()
694 .unwrap_or_else(std::sync::PoisonError::into_inner)
695 .remove(&name);
696 self.dbs
697 .write()
698 .unwrap_or_else(std::sync::PoisonError::into_inner)
699 .insert(name, state);
700 }
701 Err(NodeError::Lease(LeaseError::Held { .. })) => {
702 self.standby
703 .write()
704 .unwrap_or_else(std::sync::PoisonError::into_inner)
705 .insert(name);
706 }
707 Err(error) => {
708 tracing::warn!(db = %name, %error, "standby takeover attempt failed");
709 }
710 }
711 }
712 Ok(())
713 }
714
715 fn spawn_maintenance(self: &Arc<Self>, state: &Arc<DbState>) {
716 let ttl = self.config.lease_ttl_ms;
717 let renew_every = Duration::from_millis(u64::try_from(ttl / 3).unwrap_or(1).max(50));
718 let node = Arc::clone(self);
720 let db = Arc::clone(state);
721 tokio::spawn(async move {
722 let mut ticker = tokio::time::interval(renew_every);
723 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
724 loop {
725 ticker.tick().await;
726 if db.deposed.load(Ordering::Acquire) {
727 return;
728 }
729 let _commit = db.commit.lock().await;
733 let held = db.lease();
734 let name = db.name.clone();
735 let renewed =
736 lease::renew(node.store.as_ref(), &name, &held, ttl, now_unix_ms()).await;
737 match renewed {
738 Ok(renewed) => {
739 *db.held_lease
740 .lock()
741 .unwrap_or_else(std::sync::PoisonError::into_inner) = renewed;
742 }
743 Err(LeaseError::Lost) => {
744 node.depose(&db, "write lease lost");
745 return;
746 }
747 Err(_) => {}
748 }
749 }
750 });
751 self.spawn_indexing(state);
752 let node = Arc::clone(self);
754 let db = Arc::clone(state);
755 tokio::spawn(async move {
756 let mut ticker = tokio::time::interval(node.config.heartbeat_interval);
757 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
758 loop {
759 ticker.tick().await;
760 if db.deposed.load(Ordering::Acquire) {
761 return;
762 }
763 let _ = db
764 .broadcast
765 .send(pb::subscribe_item::Item::Heartbeat(pb::Heartbeat {
766 basis_t: db.db().basis_t(),
767 }));
768 }
769 });
770 }
771
772 fn spawn_indexing(self: &Arc<Self>, state: &Arc<DbState>) {
776 const POLICY_POLL: Duration = Duration::from_secs(1);
780 let node = Arc::clone(self);
781 let db = Arc::clone(state);
782 tokio::spawn(async move {
783 let mut published_at = Instant::now();
784 let mut last_duration = Duration::ZERO;
785 let mut published_len: Option<u64> = None;
786 loop {
787 let policy = db.index_policy();
788 tokio::time::sleep(policy.interval.min(POLICY_POLL)).await;
789 if db.deposed.load(Ordering::Acquire) {
790 return;
791 }
792 let snapshot = db.db();
793 if snapshot.basis_t() <= db.index_basis() {
794 continue;
795 }
796 let recorded_len =
797 u64::try_from(snapshot.recorded_datoms().len()).unwrap_or(u64::MAX);
798 let pending = published_len.map(|len| recorded_len.saturating_sub(len));
799 if !policy.due(published_at.elapsed(), last_duration, pending) {
800 continue;
801 }
802 match node.publish_db_indexes(&db).await {
803 Ok((_, duration)) => {
804 last_duration = duration;
805 published_len = Some(recorded_len);
809 }
810 Err(NodeError::Deposed(_)) => return,
811 Err(_) => {}
812 }
813 published_at = Instant::now();
814 }
815 });
816 }
817
818 async fn publish_db_indexes(&self, db: &Arc<DbState>) -> Result<(u64, Duration), NodeError> {
823 let _gc = self.gc_lock.lock().await;
824 let version = db.lease().version;
825 let root_name = db_root_name(&db.name);
826 let started = Instant::now();
827 let published = db
828 .transactor
829 .publish_indexes(self.store.as_ref(), &root_name, version)
830 .await;
831 let duration = started.elapsed();
832 self.metrics.record_index(duration);
833 match published {
834 Ok(root) => {
835 tracing::debug!(db = %db.name, index_basis_t = root.index_basis_t, "published indexes");
836 db.index_basis.store(root.index_basis_t, Ordering::Release);
837 let _ = db
838 .broadcast
839 .send(pb::subscribe_item::Item::IndexBasis(pb::IndexBasis {
840 index_basis_t: root.index_basis_t,
841 }));
842 Ok((root.index_basis_t, duration))
843 }
844 Err(TransactError::Deposed { .. }) => {
845 self.depose(db, "database root fenced by a newer lease");
846 Err(NodeError::Deposed(db.name.clone()))
847 }
848 Err(error) => Err(error.into()),
849 }
850 }
851
852 pub async fn request_index(&self, name: &str) -> Result<u64, NodeError> {
861 let state = self.db_state(name).await?;
862 if state.db().basis_t() <= state.index_basis() {
863 return Ok(state.index_basis());
864 }
865 self.publish_db_indexes(&state)
866 .await
867 .map(|(index_basis_t, _)| index_basis_t)
868 }
869
870 pub async fn set_index_policy(
877 &self,
878 name: &str,
879 update: IndexPolicyUpdate,
880 ) -> Result<IndexPolicy, NodeError> {
881 let state = self.db_state(name).await?;
882 let mut policy = state
883 .index_policy
884 .lock()
885 .unwrap_or_else(std::sync::PoisonError::into_inner);
886 policy.apply(update);
887 Ok(*policy)
888 }
889
890 pub async fn db_state(&self, name: &str) -> Result<Arc<DbState>, NodeError> {
896 if let Some(state) = self
897 .dbs
898 .read()
899 .unwrap_or_else(std::sync::PoisonError::into_inner)
900 .get(name)
901 .cloned()
902 {
903 return Ok(state);
904 }
905 if self.config.ha
906 && self
907 .standby
908 .read()
909 .unwrap_or_else(std::sync::PoisonError::into_inner)
910 .contains(name)
911 {
912 let root = self
913 .store
914 .get_root(&db_root_name(name))
915 .await?
916 .as_deref()
917 .and_then(DbRoot::decode);
918 return Err(NodeError::Standby {
919 db: name.to_owned(),
920 owner: root.as_ref().map(|r| r.owner.clone()).unwrap_or_default(),
921 endpoint: root.map(|r| r.owner_endpoint).unwrap_or_default(),
922 });
923 }
924 Err(NodeError::UnknownDb(name.to_owned()))
925 }
926
927 #[must_use]
929 pub fn standby_dbs(&self) -> Vec<String> {
930 self.standby
931 .read()
932 .unwrap_or_else(std::sync::PoisonError::into_inner)
933 .iter()
934 .cloned()
935 .collect()
936 }
937
938 pub async fn create_db(
944 self: &Arc<Self>,
945 name: &str,
946 schema_edn: &[u8],
947 ) -> Result<bool, NodeError> {
948 if !valid_db_name(name) {
949 return Err(NodeError::InvalidName(name.to_owned()));
950 }
951 if self
952 .dbs
953 .read()
954 .unwrap_or_else(std::sync::PoisonError::into_inner)
955 .contains_key(name)
956 {
957 return Ok(false);
958 }
959 let forms = match codec::decode_edn(schema_edn)? {
960 Edn::Vector(items) | Edn::List(items) => items,
961 Edn::Nil => Vec::new(),
962 other => {
963 return Err(NodeError::BadRequest(format!(
964 "schema must be a vector of attribute maps, got {other}"
965 )));
966 }
967 };
968 let (schema, idents) = schema_from_edn(&forms)?;
969 let meta = codec::encode_metadata(&schema, &idents, &KeywordInterner::default());
970 match self
971 .store
972 .cas_root(&meta_root_name(name), None, &meta)
973 .await
974 {
975 Ok(()) => {}
976 Err(StoreError::CasFailed { .. }) => return Ok(false),
977 Err(error) => return Err(error.into()),
978 }
979 let state = self.open_db(name).await?;
980 self.dbs
981 .write()
982 .unwrap_or_else(std::sync::PoisonError::into_inner)
983 .insert(name.to_owned(), state);
984 Ok(true)
985 }
986
987 pub async fn fork_db(
998 self: &Arc<Self>,
999 source: &str,
1000 target: &str,
1001 as_of_t: u64,
1002 ) -> Result<Option<u64>, NodeError> {
1003 if !valid_db_name(target) {
1004 return Err(NodeError::InvalidName(target.to_owned()));
1005 }
1006 if source == target {
1007 return Err(NodeError::BadRequest(
1008 "fork target must differ from the source".into(),
1009 ));
1010 }
1011 let state = self.db_state(source).await?;
1012 let basis = state.db().basis_t();
1013 let t = if as_of_t == 0 { basis } else { as_of_t };
1014 if t > basis {
1015 return Err(NodeError::BadRequest(format!(
1016 "as-of t {t} is ahead of {source:?} basis {basis}"
1017 )));
1018 }
1019 let _guard = self.fork_lock.lock().await;
1020 if self
1021 .dbs
1022 .read()
1023 .unwrap_or_else(std::sync::PoisonError::into_inner)
1024 .contains_key(target)
1025 || self
1026 .store
1027 .get_root(&meta_root_name(target))
1028 .await?
1029 .is_some()
1030 || self.log_backend.exists(target)
1031 {
1032 return Ok(None);
1033 }
1034 let records = state.log.tx_range(0, Some(t + 1))?;
1040 let meta = self
1041 .store
1042 .get_root(&meta_root_name(source))
1043 .await?
1044 .ok_or_else(|| NodeError::UnknownDb(source.to_owned()))?;
1045 let log = self.log_backend.open(target, 0)?;
1050 for record in &records {
1051 log.append(record)?;
1052 }
1053 drop(log);
1054 match self
1055 .store
1056 .cas_root(&meta_root_name(target), None, &meta)
1057 .await
1058 {
1059 Ok(()) => {}
1060 Err(StoreError::CasFailed { .. }) => {
1061 self.log_backend.delete_all(target)?;
1063 return Ok(None);
1064 }
1065 Err(error) => return Err(error.into()),
1066 }
1067 let state = self.open_db(target).await?;
1068 self.dbs
1069 .write()
1070 .unwrap_or_else(std::sync::PoisonError::into_inner)
1071 .insert(target.to_owned(), state);
1072 Ok(Some(t))
1073 }
1074
1075 pub async fn delete_db(&self, name: &str) -> Result<bool, NodeError> {
1081 let Some(state) = self
1082 .dbs
1083 .write()
1084 .unwrap_or_else(std::sync::PoisonError::into_inner)
1085 .remove(name)
1086 else {
1087 return Ok(false);
1088 };
1089 state.deposed.store(true, Ordering::Release);
1090 self.standby
1091 .write()
1092 .unwrap_or_else(std::sync::PoisonError::into_inner)
1093 .remove(name);
1094 self.store.delete_root(&db_root_name(name)).await?;
1095 self.store.delete_root(&meta_root_name(name)).await?;
1096 self.log_backend.delete_all(name)?;
1097 Ok(true)
1098 }
1099
1100 #[must_use]
1102 pub fn list_dbs(&self) -> Vec<String> {
1103 let mut names: Vec<String> = self
1104 .dbs
1105 .read()
1106 .unwrap_or_else(std::sync::PoisonError::into_inner)
1107 .keys()
1108 .cloned()
1109 .collect();
1110 names.sort();
1111 names
1112 }
1113
1114 pub async fn gc_deleted(&self) -> Result<u64, NodeError> {
1120 self.gc_deleted_with_retention(self.config.gc_retention)
1121 .await
1122 }
1123
1124 pub async fn gc_deleted_with_retention(&self, retention: Duration) -> Result<u64, NodeError> {
1129 let _gc = self.gc_lock.lock().await;
1130 let mut live = Vec::new();
1131 for root_name in self.store.list_roots("db:").await? {
1132 if let Some(root) = self
1133 .store
1134 .get_root(&root_name)
1135 .await?
1136 .as_deref()
1137 .and_then(DbRoot::decode)
1138 && let Some(roots) = root.roots
1139 {
1140 live.extend(roots);
1141 }
1142 }
1143 let report = mark_and_sweep_retained(
1144 self.store.as_ref(),
1145 live,
1146 |_, bytes| corium_store::index_blob_children(bytes),
1147 retention,
1148 SystemTime::now(),
1149 )
1150 .await?;
1151 self.metrics
1152 .record_gc(report.swept as u64, report.retained as u64);
1153 tracing::info!(
1154 marked = report.marked,
1155 swept = report.swept,
1156 retained = report.retained,
1157 "garbage collection completed"
1158 );
1159 Ok(report.swept as u64)
1160 }
1161
1162 pub async fn transact(
1169 &self,
1170 name: &str,
1171 tx_data: &[u8],
1172 ) -> Result<pb::TransactResponse, NodeError> {
1173 let started = Instant::now();
1174 let result = self
1175 .transact_inner(name, tx_data)
1176 .instrument(tracing::info_span!("transact", db = name))
1177 .await;
1178 self.metrics.record_tx(started.elapsed(), result.is_ok());
1179 if let Err(error) = &result {
1180 tracing::warn!(%error, "transaction failed");
1181 }
1182 result
1183 }
1184
1185 async fn transact_inner(
1186 &self,
1187 name: &str,
1188 tx_data: &[u8],
1189 ) -> Result<pb::TransactResponse, NodeError> {
1190 let state = self.db_state(name).await?;
1191 let decoded = codec::decode_edn(tx_data)?;
1192 let forms = decoded
1193 .as_seq()
1194 .ok_or_else(|| NodeError::BadRequest("tx-data must be a vector".into()))?
1195 .to_vec();
1196 let queued = self.metrics.queue_waiter();
1197 let commit = state.commit.lock().await;
1198 drop(queued);
1199 let _commit = commit;
1200 state.check_lease(self.store.as_ref()).await?;
1201 let forms = if let Some(expander) = &self.config.tx_fn_expander {
1206 let expander = Arc::clone(expander);
1207 let db = state.transactor.db();
1208 tokio::task::spawn_blocking(move || expander.expand(&db, forms))
1209 .await
1210 .map_err(|error| NodeError::BadRequest(format!("expander task failed: {error}")))?
1211 .map_err(NodeError::BadRequest)?
1212 } else {
1213 forms
1214 };
1215 let (items, naming_changed, idents, interner, schema) = {
1217 let mut naming = state
1218 .naming
1219 .lock()
1220 .unwrap_or_else(std::sync::PoisonError::into_inner);
1221 let db = state.transactor.db();
1222 let before = naming.interner.len();
1223 let items = tx_items_from_edn(&db, &mut naming.interner, &forms)?;
1224 (
1225 items,
1226 naming.interner.len() > before,
1227 naming.idents.clone(),
1228 naming.interner.clone(),
1229 naming.schema.clone(),
1230 )
1231 };
1232 if naming_changed {
1233 let meta = codec::encode_metadata(&schema, &idents, &interner);
1236 loop {
1237 let current = self.store.get_root(&meta_root_name(name)).await?;
1238 match self
1239 .store
1240 .cas_root(&meta_root_name(name), current.as_deref(), &meta)
1241 .await
1242 {
1243 Ok(()) => break,
1244 Err(StoreError::CasFailed { .. }) => {}
1245 Err(error) => return Err(error.into()),
1246 }
1247 }
1248 state.transactor.update_naming(idents, interner.clone());
1249 }
1250 let worker = Arc::clone(&state);
1251 let report = tokio::task::spawn_blocking(move || worker.transactor.transact(items))
1252 .await
1253 .map_err(|error| NodeError::BadRequest(format!("transact task failed: {error}")))??;
1254 if let Err(error) = state.check_lease(self.store.as_ref()).await {
1261 if matches!(error, NodeError::Deposed(_)) {
1262 self.depose(&state, "write lease lost after durable append");
1263 }
1264 return Err(error);
1268 }
1269 let t = report.db_after.basis_t();
1270 let datoms = codec::encode_datoms(&report.tx.datoms, &interner)?;
1271 let tempids = codec::encode_edn(&Edn::Map(
1272 report
1273 .tx
1274 .tempids
1275 .iter()
1276 .map(|(tempid, eid)| {
1277 (
1278 Edn::Str(tempid.clone()),
1279 Edn::Long(i64::try_from(eid.raw()).unwrap_or(i64::MAX)),
1280 )
1281 })
1282 .collect(),
1283 ));
1284 let _ = state
1285 .broadcast
1286 .send(pb::subscribe_item::Item::Report(pb::TxReport {
1287 t,
1288 tx_instant: report.tx_instant,
1289 datoms: datoms.clone(),
1290 }));
1291 let _ = state.basis.send(t);
1292 Ok(pb::TransactResponse {
1293 basis_before: report.db_before.basis_t(),
1294 basis_t: t,
1295 tx_instant: report.tx_instant,
1296 tempids,
1297 tx_data: datoms,
1298 })
1299 }
1300
1301 pub async fn status(&self, name: &str) -> Result<pb::StatusResponse, NodeError> {
1306 let state = self.db_state(name).await?;
1307 let db = state.db();
1308 let counts = db.stats();
1309 let held = state.lease();
1310 let metrics = self.metrics.snapshot();
1311 Ok(pb::StatusResponse {
1312 basis_t: db.basis_t(),
1313 index_basis_t: state.index_basis(),
1314 lease_owner: held.owner,
1315 lease_version: held.version,
1316 lease_expires_unix_ms: held.expires_unix_ms,
1317 datom_count: counts.datoms as u64,
1318 entity_count: counts.entities as u64,
1319 attribute_count: counts.attributes as u64,
1320 transaction_count: metrics.tx_total,
1321 transaction_failure_count: metrics.tx_failed,
1322 transaction_queue_depth: metrics.queue_depth,
1323 index_lag: db.basis_t().saturating_sub(state.index_basis()),
1324 indexing_runs: metrics.index_runs,
1325 gc_runs: metrics.gc_runs,
1326 gc_swept_blobs: metrics.gc_swept,
1327 lease_owner_endpoint: held.endpoint,
1328 })
1329 }
1330
1331 pub async fn release_leases(&self) {
1336 let states: Vec<Arc<DbState>> = self
1337 .dbs
1338 .write()
1339 .unwrap_or_else(std::sync::PoisonError::into_inner)
1340 .drain()
1341 .map(|(_, state)| state)
1342 .collect();
1343 for state in states {
1344 state.deposed.store(true, Ordering::Release);
1345 if let Err(error) =
1346 lease::release(self.store.as_ref(), &state.name, &state.lease()).await
1347 {
1348 tracing::warn!(db = %state.name, %error, "lease release failed at shutdown");
1349 }
1350 }
1351 }
1352
1353 pub async fn sync(&self, name: &str, t: u64) -> Result<u64, NodeError> {
1358 let state = self.db_state(name).await?;
1359 let mut basis = state.basis_watch();
1360 let target = if t == 0 { *basis.borrow() } else { t };
1361 loop {
1362 let current = *basis.borrow();
1363 if current >= target {
1364 return Ok(current);
1365 }
1366 if basis.changed().await.is_err() {
1367 return Ok(*basis.borrow());
1368 }
1369 }
1370 }
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375 use super::IndexPolicy;
1376 use std::time::Duration;
1377
1378 fn pacing(interval_ms: u64, backoff: u32, threshold: u64, deadline_ms: u64) -> IndexPolicy {
1379 IndexPolicy {
1380 interval: Duration::from_millis(interval_ms),
1381 backoff,
1382 tail_threshold: threshold,
1383 tail_deadline: Duration::from_millis(deadline_ms),
1384 }
1385 }
1386
1387 #[test]
1388 fn base_interval_gates_publication() {
1389 let pacing = pacing(100, 4, 0, 60_000);
1390 assert!(!pacing.due(Duration::from_millis(99), Duration::ZERO, None));
1391 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, None));
1392 }
1393
1394 #[test]
1395 fn backoff_stretches_the_floor_past_the_interval() {
1396 let pacing = pacing(100, 4, 0, 60_000);
1397 let last = Duration::from_millis(300);
1398 assert!(!pacing.due(Duration::from_millis(1_199), last, Some(10)));
1399 assert!(pacing.due(Duration::from_millis(1_200), last, Some(10)));
1400 }
1401
1402 #[test]
1403 fn zero_backoff_keeps_the_base_interval() {
1404 let pacing = pacing(100, 0, 0, 60_000);
1405 assert!(pacing.due(
1406 Duration::from_millis(100),
1407 Duration::from_secs(30),
1408 Some(10)
1409 ));
1410 }
1411
1412 #[test]
1413 fn fast_publications_leave_the_interval_untouched() {
1414 let pacing = pacing(5_000, 4, 0, 60_000);
1415 assert!(pacing.due(Duration::from_secs(5), Duration::from_millis(3), Some(1)));
1416 }
1417
1418 #[test]
1419 fn small_tail_defers_until_the_deadline() {
1420 let pacing = pacing(100, 4, 1_000, 60_000);
1421 assert!(!pacing.due(Duration::from_secs(30), Duration::ZERO, Some(999)));
1422 assert!(pacing.due(Duration::from_secs(60), Duration::ZERO, Some(999)));
1423 }
1424
1425 #[test]
1426 fn tail_at_threshold_publishes_at_base_pacing() {
1427 let pacing = pacing(100, 4, 1_000, 60_000);
1428 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, Some(1_000)));
1429 }
1430
1431 #[test]
1432 fn unknown_tail_publishes_at_base_pacing() {
1433 let pacing = pacing(100, 4, 1_000, 60_000);
1434 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, None));
1435 }
1436
1437 #[test]
1438 fn deadline_never_overrides_the_backoff_floor() {
1439 let pacing = pacing(100, 4, 1_000, 200);
1440 let last = Duration::from_millis(300);
1441 assert!(!pacing.due(Duration::from_millis(400), last, Some(1)));
1442 assert!(pacing.due(Duration::from_millis(1_200), last, Some(1)));
1443 }
1444}