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::{Datom, IndexOrder, 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::{
20 BlobId, BlobStore, RootStore, StoreError, decode_index_manifest, decode_segment_keys,
21 is_index_manifest, mark_and_sweep_retained, meta_root_name,
22};
23use thiserror::Error;
24use tokio::sync::{broadcast, watch};
25use tracing::Instrument;
26
27use crate::backend::{LogBackend, NodeStore, StoreSpec};
28use crate::lease::{self, Lease, LeaseError};
29use crate::metrics::Metrics;
30use crate::{DbRoot, EmbeddedTransactor, TransactError, db_root_name};
31
32pub trait TxFnExpander: Send + Sync {
37 fn expand(&self, db: &Db, forms: Vec<Edn>) -> Result<Vec<Edn>, String>;
44}
45
46#[derive(Clone)]
48pub struct NodeConfig {
49 pub store: StoreSpec,
51 pub data_dir: PathBuf,
54 pub owner: String,
56 pub lease_ttl_ms: i64,
58 pub lease_wait_ms: i64,
60 pub ha: bool,
64 pub advertise: Option<String>,
67 pub index_interval: Duration,
69 pub index_backoff: u32,
75 pub index_tail_threshold: u64,
79 pub index_tail_deadline: Duration,
81 pub heartbeat_interval: Duration,
83 pub gc_interval: Option<Duration>,
85 pub gc_retention: Duration,
87 pub tx_fn_expander: Option<Arc<dyn TxFnExpander>>,
89}
90
91impl std::fmt::Debug for NodeConfig {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("NodeConfig")
94 .field("store", &self.store)
95 .field("data_dir", &self.data_dir)
96 .field("owner", &self.owner)
97 .field("lease_ttl_ms", &self.lease_ttl_ms)
98 .field("lease_wait_ms", &self.lease_wait_ms)
99 .field("ha", &self.ha)
100 .field("advertise", &self.advertise)
101 .field("index_interval", &self.index_interval)
102 .field("index_backoff", &self.index_backoff)
103 .field("index_tail_threshold", &self.index_tail_threshold)
104 .field("index_tail_deadline", &self.index_tail_deadline)
105 .field("heartbeat_interval", &self.heartbeat_interval)
106 .field("gc_interval", &self.gc_interval)
107 .field("gc_retention", &self.gc_retention)
108 .field("tx_fn_expander", &self.tx_fn_expander.is_some())
109 .finish()
110 }
111}
112
113impl NodeConfig {
114 #[must_use]
116 pub fn new(data_dir: PathBuf) -> Self {
117 Self {
118 store: StoreSpec::Fs,
119 data_dir,
120 owner: format!(
121 "transactor-{}",
122 std::env::var("HOSTNAME").unwrap_or_else(|_| "local".into())
123 ),
124 lease_ttl_ms: 5_000,
125 lease_wait_ms: 15_000,
126 ha: false,
127 advertise: None,
128 index_interval: Duration::from_secs(5),
129 index_backoff: 4,
130 index_tail_threshold: 0,
131 index_tail_deadline: Duration::from_secs(60),
132 heartbeat_interval: Duration::from_secs(10),
133 gc_interval: Some(Duration::from_secs(60 * 60)),
134 gc_retention: Duration::from_secs(72 * 60 * 60),
135 tx_fn_expander: None,
136 }
137 }
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq)]
153pub struct IndexPolicy {
154 pub interval: Duration,
156 pub backoff: u32,
159 pub tail_threshold: u64,
162 pub tail_deadline: Duration,
165}
166
167#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
169pub struct IndexPolicyUpdate {
170 pub interval: Option<Duration>,
172 pub backoff: Option<u32>,
174 pub tail_threshold: Option<u64>,
176 pub tail_deadline: Option<Duration>,
178}
179
180impl IndexPolicy {
181 fn from_config(config: &NodeConfig) -> Self {
182 Self {
183 interval: config.index_interval,
184 backoff: config.index_backoff,
185 tail_threshold: config.index_tail_threshold,
186 tail_deadline: config.index_tail_deadline,
187 }
188 }
189
190 fn apply(&mut self, update: IndexPolicyUpdate) {
191 if let Some(interval) = update.interval {
192 self.interval = interval;
193 }
194 if let Some(backoff) = update.backoff {
195 self.backoff = backoff;
196 }
197 if let Some(tail_threshold) = update.tail_threshold {
198 self.tail_threshold = tail_threshold;
199 }
200 if let Some(tail_deadline) = update.tail_deadline {
201 self.tail_deadline = tail_deadline;
202 }
203 }
204
205 fn due(&self, since_publish: Duration, last_duration: Duration, pending: Option<u64>) -> bool {
212 let floor = self
213 .interval
214 .max(last_duration.saturating_mul(self.backoff));
215 if since_publish < floor {
216 return false;
217 }
218 match pending {
219 Some(pending) if pending < self.tail_threshold => since_publish >= self.tail_deadline,
220 _ => true,
221 }
222 }
223}
224
225#[derive(Debug, Error)]
227pub enum NodeError {
228 #[error("unknown database {0:?}")]
230 UnknownDb(String),
231 #[error("invalid database name {0:?}")]
233 InvalidName(String),
234 #[error("storage format {found} is newer than supported format {supported}")]
236 UnsupportedFormat {
237 found: u32,
239 supported: u32,
241 },
242 #[error("deposed: write lease for {0:?} is held elsewhere")]
244 Deposed(String),
245 #[error("standby for {db:?}: lease held by {owner} at {endpoint:?}")]
248 Standby {
249 db: String,
251 owner: String,
253 endpoint: String,
255 },
256 #[error(transparent)]
258 Codec(#[from] CodecError),
259 #[error(transparent)]
261 TxForm(#[from] TxFormError),
262 #[error(transparent)]
264 SchemaForm(#[from] SchemaFormError),
265 #[error(transparent)]
267 Transact(#[from] TransactError),
268 #[error(transparent)]
270 Store(#[from] StoreError),
271 #[error(transparent)]
273 Log(#[from] LogError),
274 #[error(transparent)]
276 Lease(#[from] LeaseError),
277 #[error("bad request: {0}")]
279 BadRequest(String),
280}
281
282struct Naming {
283 schema: Schema,
284 idents: Idents,
285 interner: KeywordInterner,
286}
287
288pub struct DbState {
290 name: String,
291 transactor: EmbeddedTransactor,
292 log: Arc<dyn TransactionLog>,
293 naming: Mutex<Naming>,
294 commit: tokio::sync::Mutex<()>,
295 broadcast: broadcast::Sender<pb::subscribe_item::Item>,
296 basis: watch::Sender<u64>,
297 index_basis: AtomicU64,
298 index_policy: Mutex<IndexPolicy>,
299 held_lease: Mutex<Lease>,
300 deposed: AtomicBool,
301}
302
303impl DbState {
304 #[must_use]
306 pub fn name(&self) -> &str {
307 &self.name
308 }
309
310 #[must_use]
312 pub fn db(&self) -> Db {
313 self.transactor.db()
314 }
315
316 #[must_use]
318 pub fn basis_watch(&self) -> watch::Receiver<u64> {
319 self.basis.subscribe()
320 }
321
322 #[must_use]
325 pub fn stream_items(&self) -> broadcast::Receiver<pb::subscribe_item::Item> {
326 self.broadcast.subscribe()
327 }
328
329 #[must_use]
331 pub fn index_basis(&self) -> u64 {
332 self.index_basis.load(Ordering::Acquire)
333 }
334
335 #[must_use]
337 pub fn index_policy(&self) -> IndexPolicy {
338 *self
339 .index_policy
340 .lock()
341 .unwrap_or_else(std::sync::PoisonError::into_inner)
342 }
343
344 #[must_use]
346 pub fn lease(&self) -> Lease {
347 self.held_lease
348 .lock()
349 .unwrap_or_else(std::sync::PoisonError::into_inner)
350 .clone()
351 }
352
353 #[must_use]
356 pub fn handshake_snapshot(&self) -> (Vec<u8>, KeywordInterner) {
357 let naming = self
358 .naming
359 .lock()
360 .unwrap_or_else(std::sync::PoisonError::into_inner);
361 (
362 codec::encode_schema(&naming.schema, &naming.idents),
363 naming.interner.clone(),
364 )
365 }
366
367 pub async fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, NodeError> {
372 Ok(self.log.tx_range_async(start, end).await?)
373 }
374
375 async fn check_lease(&self, store: &dyn RootStore) -> Result<Lease, NodeError> {
378 if self.deposed.load(Ordering::Acquire) {
379 return Err(NodeError::Deposed(self.name.clone()));
380 }
381 let held = self.lease();
382 match lease::verify(store, &self.name, &held).await {
383 Ok(()) => Ok(held),
384 Err(LeaseError::Lost) => {
385 self.deposed.store(true, Ordering::Release);
386 Err(NodeError::Deposed(self.name.clone()))
387 }
388 Err(error) => Err(error.into()),
389 }
390 }
391}
392
393pub struct TransactorNode {
395 config: NodeConfig,
396 store: Arc<NodeStore>,
397 log_backend: LogBackend,
398 dbs: std::sync::RwLock<HashMap<String, Arc<DbState>>>,
399 standby: std::sync::RwLock<BTreeSet<String>>,
402 gc_lock: tokio::sync::Mutex<()>,
403 fork_lock: tokio::sync::Mutex<()>,
406 metrics: Metrics,
407 shutdown: watch::Sender<Option<String>>,
408}
409
410fn now_unix_ms() -> i64 {
411 i64::try_from(
412 SystemTime::now()
413 .duration_since(UNIX_EPOCH)
414 .unwrap_or_default()
415 .as_millis(),
416 )
417 .unwrap_or(i64::MAX)
418}
419
420fn valid_db_name(name: &str) -> bool {
421 !name.is_empty()
422 && name.len() <= 128
423 && name
424 .bytes()
425 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
426}
427
428impl TransactorNode {
429 pub async fn open(config: NodeConfig) -> Result<Arc<Self>, NodeError> {
437 let store = Arc::new(NodeStore::open(&config.store, &config.data_dir).await?);
438 let log_backend = LogBackend::for_spec(&config.store, &config.data_dir, Arc::clone(&store));
439 let node = Arc::new(Self {
440 config,
441 store,
442 log_backend,
443 dbs: std::sync::RwLock::new(HashMap::new()),
444 standby: std::sync::RwLock::new(BTreeSet::new()),
445 gc_lock: tokio::sync::Mutex::new(()),
446 fork_lock: tokio::sync::Mutex::new(()),
447 metrics: Metrics::default(),
448 shutdown: watch::channel(None).0,
449 });
450 let names: Vec<String> = node
451 .store
452 .list_roots("meta:")
453 .await?
454 .into_iter()
455 .filter_map(|root| root.strip_prefix("meta:").map(str::to_owned))
456 .collect();
457 for name in names {
458 match node.open_db(&name).await {
459 Ok(state) => {
460 node.dbs
461 .write()
462 .unwrap_or_else(std::sync::PoisonError::into_inner)
463 .insert(name, state);
464 }
465 Err(NodeError::Lease(LeaseError::Held { owner, .. })) if node.config.ha => {
466 tracing::info!(db = %name, %owner, "standing by; lease held elsewhere");
467 node.standby
468 .write()
469 .unwrap_or_else(std::sync::PoisonError::into_inner)
470 .insert(name);
471 }
472 Err(error) => return Err(error),
473 }
474 }
475 node.spawn_standby_poller();
476 node.spawn_scheduled_gc();
477 Ok(node)
478 }
479
480 #[must_use]
482 pub fn store(&self) -> &Arc<NodeStore> {
483 &self.store
484 }
485
486 #[must_use]
488 pub fn config(&self) -> &NodeConfig {
489 &self.config
490 }
491
492 #[must_use]
494 pub const fn metrics(&self) -> &Metrics {
495 &self.metrics
496 }
497
498 fn spawn_scheduled_gc(self: &Arc<Self>) {
499 let Some(interval) = self.config.gc_interval else {
500 return;
501 };
502 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
503 return;
506 };
507 let node = Arc::clone(self);
508 runtime.spawn(async move {
509 let mut ticker = tokio::time::interval(interval);
510 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
511 ticker.tick().await;
513 loop {
514 ticker.tick().await;
515 if let Err(error) = node.gc_deleted().await {
516 tracing::warn!(%error, "scheduled garbage collection failed");
517 }
518 }
519 });
520 }
521
522 #[must_use]
524 pub fn shutdown_watch(&self) -> watch::Receiver<Option<String>> {
525 self.shutdown.subscribe()
526 }
527
528 fn depose(&self, state: &DbState, reason: &str) {
532 state.deposed.store(true, Ordering::Release);
533 if self.config.ha {
534 tracing::warn!(db = %state.name, reason, "deposed; returning to standby");
535 self.dbs
536 .write()
537 .unwrap_or_else(std::sync::PoisonError::into_inner)
538 .remove(&state.name);
539 self.standby
540 .write()
541 .unwrap_or_else(std::sync::PoisonError::into_inner)
542 .insert(state.name.clone());
543 } else {
544 let _ = self
545 .shutdown
546 .send(Some(format!("database {:?}: {reason}", state.name)));
547 }
548 }
549
550 fn advertised(&self) -> &str {
551 self.config.advertise.as_deref().unwrap_or("")
552 }
553
554 async fn acquire_lease(&self, name: &str) -> Result<Lease, NodeError> {
558 let deadline = now_unix_ms() + self.config.lease_wait_ms;
559 loop {
560 match lease::acquire(
561 self.store.as_ref(),
562 name,
563 &self.config.owner,
564 self.advertised(),
565 self.config.lease_ttl_ms,
566 now_unix_ms(),
567 )
568 .await
569 {
570 Ok(held) => return Ok(held),
571 Err(LeaseError::Held { .. }) if !self.config.ha && now_unix_ms() < deadline => {
572 tokio::time::sleep(Duration::from_millis(200)).await;
573 }
574 Err(error) => return Err(error.into()),
575 }
576 }
577 }
578
579 async fn open_db(self: &Arc<Self>, name: &str) -> Result<Arc<DbState>, NodeError> {
580 let meta = self
581 .store
582 .get_root(&meta_root_name(name))
583 .await?
584 .ok_or_else(|| NodeError::UnknownDb(name.to_owned()))?;
585 let (schema, idents, interner) = codec::decode_metadata(&meta)?;
586 let root_name = db_root_name(name);
587 let current = self
588 .store
589 .get_root(&root_name)
590 .await?
591 .as_deref()
592 .and_then(DbRoot::decode);
593 if let Some(root) = ¤t
594 && root.format_version > corium_store::FORMAT_VERSION
595 {
596 return Err(NodeError::UnsupportedFormat {
597 found: root.format_version,
598 supported: corium_store::FORMAT_VERSION,
599 });
600 }
601 let held = self.acquire_lease(name).await?;
607 let log = self.log_backend.open(name, held.version).await?;
610 let post_fence = self
611 .store
612 .get_root(&root_name)
613 .await?
614 .as_deref()
615 .and_then(DbRoot::decode);
616 let transactor = self
617 .recover_transactor(name, &schema, &idents, &interner, post_fence.as_ref(), &log)
618 .await?;
619 let basis_t = transactor.db().basis_t();
620 let index_basis = post_fence.map_or(0, |root| root.index_basis_t);
621 let state = Arc::new(DbState {
622 name: name.to_owned(),
623 transactor,
624 log,
625 naming: Mutex::new(Naming {
626 schema,
627 idents,
628 interner,
629 }),
630 commit: tokio::sync::Mutex::new(()),
631 broadcast: broadcast::channel(1024).0,
632 basis: watch::channel(basis_t).0,
633 index_basis: AtomicU64::new(index_basis),
634 index_policy: Mutex::new(IndexPolicy::from_config(&self.config)),
635 held_lease: Mutex::new(held),
636 deposed: AtomicBool::new(false),
637 });
638 self.spawn_maintenance(&state);
639 Ok(state)
640 }
641
642 async fn recover_transactor(
651 &self,
652 name: &str,
653 schema: &Schema,
654 idents: &Idents,
655 interner: &KeywordInterner,
656 root: Option<&DbRoot>,
657 log: &Arc<dyn TransactionLog>,
658 ) -> Result<EmbeddedTransactor, NodeError> {
659 if let Some(root) = root
662 && let Some(roots) = &root.roots
663 && root.next_entity_id != 0
664 {
665 match self
666 .load_current_snapshot(
667 root,
668 &roots[IndexOrder::Eavt as usize],
669 schema,
670 idents,
671 interner,
672 )
673 .await
674 {
675 Ok(snapshot) => {
676 return Ok(EmbeddedTransactor::recover_from_snapshot_async(
677 snapshot,
678 root.next_entity_id,
679 root.last_tx_instant,
680 Arc::clone(log),
681 )
682 .await?);
683 }
684 Err(error) => {
685 tracing::warn!(
686 db = %name,
687 %error,
688 "index-root recovery failed; falling back to full-log replay"
689 );
690 }
691 }
692 }
693 let base = Db::new(schema.clone()).with_naming(idents.clone(), interner.clone());
694 Ok(EmbeddedTransactor::recover_from_async(base, Arc::clone(log)).await?)
695 }
696
697 async fn load_current_snapshot(
702 &self,
703 root: &DbRoot,
704 eavt: &BlobId,
705 schema: &Schema,
706 idents: &Idents,
707 interner: &KeywordInterner,
708 ) -> Result<Db, StoreError> {
709 let datoms = self
710 .load_index_keys(eavt)
711 .await?
712 .into_iter()
713 .map(|key| Datom::from_key(IndexOrder::Eavt, &key))
714 .collect::<Result<Vec<_>, _>>()
715 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
716 Ok(Db::from_current_snapshot(
717 root.index_basis_t,
718 schema.clone(),
719 idents.clone(),
720 interner.clone(),
721 datoms,
722 ))
723 }
724
725 async fn load_index_keys(&self, id: &BlobId) -> Result<Vec<Vec<u8>>, StoreError> {
728 let blob = self
729 .store
730 .get(id)
731 .await?
732 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
733 if !is_index_manifest(&blob) {
734 return decode_segment_keys(&blob);
735 }
736 let mut keys = Vec::new();
737 for child in decode_index_manifest(&blob)? {
738 let chunk = self
739 .store
740 .get(&child)
741 .await?
742 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
743 keys.extend(decode_segment_keys(&chunk)?);
744 }
745 Ok(keys)
746 }
747
748 fn spawn_standby_poller(self: &Arc<Self>) {
754 if !self.config.ha {
755 return;
756 }
757 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
758 return;
759 };
760 let ttl = self.config.lease_ttl_ms;
761 let poll_every = Duration::from_millis(u64::try_from(ttl / 3).unwrap_or(1).max(50));
762 let node = Arc::clone(self);
763 runtime.spawn(async move {
764 let mut ticker = tokio::time::interval(poll_every);
765 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
766 loop {
767 ticker.tick().await;
768 if let Err(error) = node.standby_scan().await {
769 tracing::warn!(%error, "standby scan failed");
770 }
771 }
772 });
773 }
774
775 async fn standby_scan(self: &Arc<Self>) -> Result<(), NodeError> {
778 let names: Vec<String> = self
779 .store
780 .list_roots("meta:")
781 .await?
782 .into_iter()
783 .filter_map(|root| root.strip_prefix("meta:").map(str::to_owned))
784 .collect();
785 {
786 let mut standby = self
787 .standby
788 .write()
789 .unwrap_or_else(std::sync::PoisonError::into_inner);
790 standby.retain(|name| names.contains(name));
791 }
792 for name in names {
793 if self
794 .dbs
795 .read()
796 .unwrap_or_else(std::sync::PoisonError::into_inner)
797 .contains_key(&name)
798 {
799 continue;
800 }
801 match self.open_db(&name).await {
802 Ok(state) => {
803 tracing::info!(db = %name, owner = %self.config.owner, "standby took over write lease");
804 self.standby
805 .write()
806 .unwrap_or_else(std::sync::PoisonError::into_inner)
807 .remove(&name);
808 self.dbs
809 .write()
810 .unwrap_or_else(std::sync::PoisonError::into_inner)
811 .insert(name, state);
812 }
813 Err(NodeError::Lease(LeaseError::Held { .. })) => {
814 self.standby
815 .write()
816 .unwrap_or_else(std::sync::PoisonError::into_inner)
817 .insert(name);
818 }
819 Err(error) => {
820 tracing::warn!(db = %name, %error, "standby takeover attempt failed");
821 }
822 }
823 }
824 Ok(())
825 }
826
827 fn spawn_maintenance(self: &Arc<Self>, state: &Arc<DbState>) {
828 let ttl = self.config.lease_ttl_ms;
829 let renew_every = Duration::from_millis(u64::try_from(ttl / 3).unwrap_or(1).max(50));
830 let node = Arc::clone(self);
832 let db = Arc::clone(state);
833 tokio::spawn(async move {
834 let mut ticker = tokio::time::interval(renew_every);
835 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
836 loop {
837 ticker.tick().await;
838 if db.deposed.load(Ordering::Acquire) {
839 return;
840 }
841 let _commit = db.commit.lock().await;
845 let held = db.lease();
846 let name = db.name.clone();
847 let renewed =
848 lease::renew(node.store.as_ref(), &name, &held, ttl, now_unix_ms()).await;
849 match renewed {
850 Ok(renewed) => {
851 *db.held_lease
852 .lock()
853 .unwrap_or_else(std::sync::PoisonError::into_inner) = renewed;
854 }
855 Err(LeaseError::Lost) => {
856 node.depose(&db, "write lease lost");
857 return;
858 }
859 Err(_) => {}
860 }
861 }
862 });
863 self.spawn_indexing(state);
864 let node = Arc::clone(self);
866 let db = Arc::clone(state);
867 tokio::spawn(async move {
868 let mut ticker = tokio::time::interval(node.config.heartbeat_interval);
869 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
870 loop {
871 ticker.tick().await;
872 if db.deposed.load(Ordering::Acquire) {
873 return;
874 }
875 let _ = db
876 .broadcast
877 .send(pb::subscribe_item::Item::Heartbeat(pb::Heartbeat {
878 basis_t: db.db().basis_t(),
879 }));
880 }
881 });
882 }
883
884 fn spawn_indexing(self: &Arc<Self>, state: &Arc<DbState>) {
888 const POLICY_POLL: Duration = Duration::from_secs(1);
892 let node = Arc::clone(self);
893 let db = Arc::clone(state);
894 tokio::spawn(async move {
895 let mut published_at = Instant::now();
896 let mut last_duration = Duration::ZERO;
897 let mut published_len: Option<u64> = None;
898 loop {
899 let policy = db.index_policy();
900 tokio::time::sleep(policy.interval.min(POLICY_POLL)).await;
901 if db.deposed.load(Ordering::Acquire) {
902 return;
903 }
904 let snapshot = db.db();
905 if snapshot.basis_t() <= db.index_basis() {
906 continue;
907 }
908 let recorded_len = u64::try_from(snapshot.recorded_len()).unwrap_or(u64::MAX);
909 let pending = published_len.map(|len| recorded_len.saturating_sub(len));
910 if !policy.due(published_at.elapsed(), last_duration, pending) {
911 continue;
912 }
913 match node.publish_db_indexes(&db).await {
914 Ok((_, duration)) => {
915 last_duration = duration;
916 published_len = Some(recorded_len);
920 }
921 Err(NodeError::Deposed(_)) => return,
922 Err(_) => {}
923 }
924 published_at = Instant::now();
925 }
926 });
927 }
928
929 async fn publish_db_indexes(&self, db: &Arc<DbState>) -> Result<(u64, Duration), NodeError> {
934 let _gc = self.gc_lock.lock().await;
935 let version = db.lease().version;
936 let root_name = db_root_name(&db.name);
937 let started = Instant::now();
938 let published = db
939 .transactor
940 .publish_indexes(self.store.as_ref(), &root_name, version)
941 .await;
942 let duration = started.elapsed();
943 self.metrics.record_index(duration);
944 match published {
945 Ok(root) => {
946 tracing::debug!(db = %db.name, index_basis_t = root.index_basis_t, "published indexes");
947 db.index_basis.store(root.index_basis_t, Ordering::Release);
948 let _ = db
949 .broadcast
950 .send(pb::subscribe_item::Item::IndexBasis(pb::IndexBasis {
951 index_basis_t: root.index_basis_t,
952 }));
953 Ok((root.index_basis_t, duration))
954 }
955 Err(TransactError::Deposed { .. }) => {
956 self.depose(db, "database root fenced by a newer lease");
957 Err(NodeError::Deposed(db.name.clone()))
958 }
959 Err(error) => Err(error.into()),
960 }
961 }
962
963 pub async fn request_index(&self, name: &str) -> Result<u64, NodeError> {
972 let state = self.db_state(name).await?;
973 if state.db().basis_t() <= state.index_basis() {
974 return Ok(state.index_basis());
975 }
976 self.publish_db_indexes(&state)
977 .await
978 .map(|(index_basis_t, _)| index_basis_t)
979 }
980
981 pub async fn set_index_policy(
988 &self,
989 name: &str,
990 update: IndexPolicyUpdate,
991 ) -> Result<IndexPolicy, NodeError> {
992 let state = self.db_state(name).await?;
993 let mut policy = state
994 .index_policy
995 .lock()
996 .unwrap_or_else(std::sync::PoisonError::into_inner);
997 policy.apply(update);
998 Ok(*policy)
999 }
1000
1001 pub async fn db_state(&self, name: &str) -> Result<Arc<DbState>, NodeError> {
1007 if let Some(state) = self
1008 .dbs
1009 .read()
1010 .unwrap_or_else(std::sync::PoisonError::into_inner)
1011 .get(name)
1012 .cloned()
1013 {
1014 return Ok(state);
1015 }
1016 if self.config.ha
1017 && self
1018 .standby
1019 .read()
1020 .unwrap_or_else(std::sync::PoisonError::into_inner)
1021 .contains(name)
1022 {
1023 let root = self
1024 .store
1025 .get_root(&db_root_name(name))
1026 .await?
1027 .as_deref()
1028 .and_then(DbRoot::decode);
1029 return Err(NodeError::Standby {
1030 db: name.to_owned(),
1031 owner: root.as_ref().map(|r| r.owner.clone()).unwrap_or_default(),
1032 endpoint: root.map(|r| r.owner_endpoint).unwrap_or_default(),
1033 });
1034 }
1035 Err(NodeError::UnknownDb(name.to_owned()))
1036 }
1037
1038 #[must_use]
1040 pub fn standby_dbs(&self) -> Vec<String> {
1041 self.standby
1042 .read()
1043 .unwrap_or_else(std::sync::PoisonError::into_inner)
1044 .iter()
1045 .cloned()
1046 .collect()
1047 }
1048
1049 pub async fn create_db(
1055 self: &Arc<Self>,
1056 name: &str,
1057 schema_edn: &[u8],
1058 ) -> Result<bool, NodeError> {
1059 if !valid_db_name(name) {
1060 return Err(NodeError::InvalidName(name.to_owned()));
1061 }
1062 if self
1063 .dbs
1064 .read()
1065 .unwrap_or_else(std::sync::PoisonError::into_inner)
1066 .contains_key(name)
1067 {
1068 return Ok(false);
1069 }
1070 let forms = match codec::decode_edn(schema_edn)? {
1071 Edn::Vector(items) | Edn::List(items) => items,
1072 Edn::Nil => Vec::new(),
1073 other => {
1074 return Err(NodeError::BadRequest(format!(
1075 "schema must be a vector of attribute maps, got {other}"
1076 )));
1077 }
1078 };
1079 let (schema, idents) = schema_from_edn(&forms)?;
1080 let meta = codec::encode_metadata(&schema, &idents, &KeywordInterner::default());
1081 match self
1082 .store
1083 .cas_root(&meta_root_name(name), None, &meta)
1084 .await
1085 {
1086 Ok(()) => {}
1087 Err(StoreError::CasFailed { .. }) => return Ok(false),
1088 Err(error) => return Err(error.into()),
1089 }
1090 let state = self.open_db(name).await?;
1091 self.dbs
1092 .write()
1093 .unwrap_or_else(std::sync::PoisonError::into_inner)
1094 .insert(name.to_owned(), state);
1095 Ok(true)
1096 }
1097
1098 pub async fn fork_db(
1109 self: &Arc<Self>,
1110 source: &str,
1111 target: &str,
1112 as_of_t: u64,
1113 ) -> Result<Option<u64>, NodeError> {
1114 if !valid_db_name(target) {
1115 return Err(NodeError::InvalidName(target.to_owned()));
1116 }
1117 if source == target {
1118 return Err(NodeError::BadRequest(
1119 "fork target must differ from the source".into(),
1120 ));
1121 }
1122 let state = self.db_state(source).await?;
1123 let basis = state.db().basis_t();
1124 let t = if as_of_t == 0 { basis } else { as_of_t };
1125 if t > basis {
1126 return Err(NodeError::BadRequest(format!(
1127 "as-of t {t} is ahead of {source:?} basis {basis}"
1128 )));
1129 }
1130 let _guard = self.fork_lock.lock().await;
1131 if self
1132 .dbs
1133 .read()
1134 .unwrap_or_else(std::sync::PoisonError::into_inner)
1135 .contains_key(target)
1136 || self
1137 .store
1138 .get_root(&meta_root_name(target))
1139 .await?
1140 .is_some()
1141 || self.log_backend.exists(target).await
1142 {
1143 return Ok(None);
1144 }
1145 let records = state.log.tx_range_async(0, Some(t + 1)).await?;
1151 let meta = self
1152 .store
1153 .get_root(&meta_root_name(source))
1154 .await?
1155 .ok_or_else(|| NodeError::UnknownDb(source.to_owned()))?;
1156 let log = self.log_backend.open(target, 0).await?;
1161 for record in &records {
1162 log.append_async(record).await?;
1163 }
1164 drop(log);
1165 match self
1166 .store
1167 .cas_root(&meta_root_name(target), None, &meta)
1168 .await
1169 {
1170 Ok(()) => {}
1171 Err(StoreError::CasFailed { .. }) => {
1172 self.log_backend.delete_all(target).await?;
1174 return Ok(None);
1175 }
1176 Err(error) => return Err(error.into()),
1177 }
1178 let state = self.open_db(target).await?;
1179 self.dbs
1180 .write()
1181 .unwrap_or_else(std::sync::PoisonError::into_inner)
1182 .insert(target.to_owned(), state);
1183 Ok(Some(t))
1184 }
1185
1186 pub async fn delete_db(&self, name: &str) -> Result<bool, NodeError> {
1192 let Some(state) = self
1193 .dbs
1194 .write()
1195 .unwrap_or_else(std::sync::PoisonError::into_inner)
1196 .remove(name)
1197 else {
1198 return Ok(false);
1199 };
1200 state.deposed.store(true, Ordering::Release);
1201 self.standby
1202 .write()
1203 .unwrap_or_else(std::sync::PoisonError::into_inner)
1204 .remove(name);
1205 self.store.delete_root(&db_root_name(name)).await?;
1206 self.store.delete_root(&meta_root_name(name)).await?;
1207 self.log_backend.delete_all(name).await?;
1208 Ok(true)
1209 }
1210
1211 #[must_use]
1213 pub fn list_dbs(&self) -> Vec<String> {
1214 let mut names: Vec<String> = self
1215 .dbs
1216 .read()
1217 .unwrap_or_else(std::sync::PoisonError::into_inner)
1218 .keys()
1219 .cloned()
1220 .collect();
1221 names.sort();
1222 names
1223 }
1224
1225 pub async fn gc_deleted(&self) -> Result<u64, NodeError> {
1231 self.gc_deleted_with_retention(self.config.gc_retention)
1232 .await
1233 }
1234
1235 pub async fn gc_deleted_with_retention(&self, retention: Duration) -> Result<u64, NodeError> {
1240 let _gc = self.gc_lock.lock().await;
1241 let mut live = Vec::new();
1242 for root_name in self.store.list_roots("db:").await? {
1243 if let Some(root) = self
1244 .store
1245 .get_root(&root_name)
1246 .await?
1247 .as_deref()
1248 .and_then(DbRoot::decode)
1249 && let Some(roots) = root.roots
1250 {
1251 live.extend(roots);
1252 }
1253 }
1254 let report = mark_and_sweep_retained(
1255 self.store.as_ref(),
1256 live,
1257 |_, bytes| corium_store::index_blob_children(bytes),
1258 retention,
1259 SystemTime::now(),
1260 )
1261 .await?;
1262 self.metrics
1263 .record_gc(report.swept as u64, report.retained as u64);
1264 tracing::info!(
1265 marked = report.marked,
1266 swept = report.swept,
1267 retained = report.retained,
1268 "garbage collection completed"
1269 );
1270 Ok(report.swept as u64)
1271 }
1272
1273 pub async fn transact(
1280 &self,
1281 name: &str,
1282 tx_data: &[u8],
1283 ) -> Result<pb::TransactResponse, NodeError> {
1284 let started = Instant::now();
1285 let result = self
1286 .transact_inner(name, tx_data)
1287 .instrument(tracing::info_span!("transact", db = name))
1288 .await;
1289 self.metrics.record_tx(started.elapsed(), result.is_ok());
1290 if let Err(error) = &result {
1291 tracing::warn!(%error, "transaction failed");
1292 }
1293 result
1294 }
1295
1296 async fn transact_inner(
1297 &self,
1298 name: &str,
1299 tx_data: &[u8],
1300 ) -> Result<pb::TransactResponse, NodeError> {
1301 let state = self.db_state(name).await?;
1302 let decoded = codec::decode_edn(tx_data)?;
1303 let forms = decoded
1304 .as_seq()
1305 .ok_or_else(|| NodeError::BadRequest("tx-data must be a vector".into()))?
1306 .to_vec();
1307 let queued = self.metrics.queue_waiter();
1308 let commit = state.commit.lock().await;
1309 drop(queued);
1310 let _commit = commit;
1311 state.check_lease(self.store.as_ref()).await?;
1312 let forms = if let Some(expander) = &self.config.tx_fn_expander {
1317 let expander = Arc::clone(expander);
1318 let db = state.transactor.db();
1319 tokio::task::spawn_blocking(move || expander.expand(&db, forms))
1320 .await
1321 .map_err(|error| NodeError::BadRequest(format!("expander task failed: {error}")))?
1322 .map_err(NodeError::BadRequest)?
1323 } else {
1324 forms
1325 };
1326 let (items, naming_changed, idents, interner, schema) = {
1328 let mut naming = state
1329 .naming
1330 .lock()
1331 .unwrap_or_else(std::sync::PoisonError::into_inner);
1332 let db = state.transactor.db();
1333 let before = naming.interner.len();
1334 let items = tx_items_from_edn(&db, &mut naming.interner, &forms)?;
1335 (
1336 items,
1337 naming.interner.len() > before,
1338 naming.idents.clone(),
1339 naming.interner.clone(),
1340 naming.schema.clone(),
1341 )
1342 };
1343 if naming_changed {
1344 let meta = codec::encode_metadata(&schema, &idents, &interner);
1347 loop {
1348 let current = self.store.get_root(&meta_root_name(name)).await?;
1349 match self
1350 .store
1351 .cas_root(&meta_root_name(name), current.as_deref(), &meta)
1352 .await
1353 {
1354 Ok(()) => break,
1355 Err(StoreError::CasFailed { .. }) => {}
1356 Err(error) => return Err(error.into()),
1357 }
1358 }
1359 state.transactor.update_naming(idents, interner.clone());
1360 }
1361 let report = state.transactor.transact_async(items).await?;
1362 if let Err(error) = state.check_lease(self.store.as_ref()).await {
1369 if matches!(error, NodeError::Deposed(_)) {
1370 self.depose(&state, "write lease lost after durable append");
1371 }
1372 return Err(error);
1376 }
1377 let t = report.db_after.basis_t();
1378 let datoms = codec::encode_datoms(&report.tx.datoms, &interner)?;
1379 let tempids = codec::encode_edn(&Edn::Map(
1380 report
1381 .tx
1382 .tempids
1383 .iter()
1384 .map(|(tempid, eid)| {
1385 (
1386 Edn::Str(tempid.clone()),
1387 Edn::Long(i64::try_from(eid.raw()).unwrap_or(i64::MAX)),
1388 )
1389 })
1390 .collect(),
1391 ));
1392 let _ = state
1393 .broadcast
1394 .send(pb::subscribe_item::Item::Report(pb::TxReport {
1395 t,
1396 tx_instant: report.tx_instant,
1397 datoms: datoms.clone(),
1398 }));
1399 let _ = state.basis.send(t);
1400 Ok(pb::TransactResponse {
1401 basis_before: report.db_before.basis_t(),
1402 basis_t: t,
1403 tx_instant: report.tx_instant,
1404 tempids,
1405 tx_data: datoms,
1406 })
1407 }
1408
1409 pub async fn status(&self, name: &str) -> Result<pb::StatusResponse, NodeError> {
1414 let state = self.db_state(name).await?;
1415 let db = state.db();
1416 let counts = db.stats();
1417 let held = state.lease();
1418 let metrics = self.metrics.snapshot();
1419 Ok(pb::StatusResponse {
1420 basis_t: db.basis_t(),
1421 index_basis_t: state.index_basis(),
1422 lease_owner: held.owner,
1423 lease_version: held.version,
1424 lease_expires_unix_ms: held.expires_unix_ms,
1425 datom_count: counts.datoms as u64,
1426 entity_count: counts.entities as u64,
1427 attribute_count: counts.attributes as u64,
1428 transaction_count: metrics.tx_total,
1429 transaction_failure_count: metrics.tx_failed,
1430 transaction_queue_depth: metrics.queue_depth,
1431 index_lag: db.basis_t().saturating_sub(state.index_basis()),
1432 indexing_runs: metrics.index_runs,
1433 gc_runs: metrics.gc_runs,
1434 gc_swept_blobs: metrics.gc_swept,
1435 lease_owner_endpoint: held.endpoint,
1436 })
1437 }
1438
1439 pub async fn release_leases(&self) {
1444 let states: Vec<Arc<DbState>> = self
1445 .dbs
1446 .write()
1447 .unwrap_or_else(std::sync::PoisonError::into_inner)
1448 .drain()
1449 .map(|(_, state)| state)
1450 .collect();
1451 for state in states {
1452 state.deposed.store(true, Ordering::Release);
1453 if let Err(error) =
1454 lease::release(self.store.as_ref(), &state.name, &state.lease()).await
1455 {
1456 tracing::warn!(db = %state.name, %error, "lease release failed at shutdown");
1457 }
1458 }
1459 }
1460
1461 pub async fn sync(&self, name: &str, t: u64) -> Result<u64, NodeError> {
1466 let state = self.db_state(name).await?;
1467 let mut basis = state.basis_watch();
1468 let target = if t == 0 { *basis.borrow() } else { t };
1469 loop {
1470 let current = *basis.borrow();
1471 if current >= target {
1472 return Ok(current);
1473 }
1474 if basis.changed().await.is_err() {
1475 return Ok(*basis.borrow());
1476 }
1477 }
1478 }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use super::IndexPolicy;
1484 use std::time::Duration;
1485
1486 fn pacing(interval_ms: u64, backoff: u32, threshold: u64, deadline_ms: u64) -> IndexPolicy {
1487 IndexPolicy {
1488 interval: Duration::from_millis(interval_ms),
1489 backoff,
1490 tail_threshold: threshold,
1491 tail_deadline: Duration::from_millis(deadline_ms),
1492 }
1493 }
1494
1495 #[test]
1496 fn base_interval_gates_publication() {
1497 let pacing = pacing(100, 4, 0, 60_000);
1498 assert!(!pacing.due(Duration::from_millis(99), Duration::ZERO, None));
1499 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, None));
1500 }
1501
1502 #[test]
1503 fn backoff_stretches_the_floor_past_the_interval() {
1504 let pacing = pacing(100, 4, 0, 60_000);
1505 let last = Duration::from_millis(300);
1506 assert!(!pacing.due(Duration::from_millis(1_199), last, Some(10)));
1507 assert!(pacing.due(Duration::from_millis(1_200), last, Some(10)));
1508 }
1509
1510 #[test]
1511 fn zero_backoff_keeps_the_base_interval() {
1512 let pacing = pacing(100, 0, 0, 60_000);
1513 assert!(pacing.due(
1514 Duration::from_millis(100),
1515 Duration::from_secs(30),
1516 Some(10)
1517 ));
1518 }
1519
1520 #[test]
1521 fn fast_publications_leave_the_interval_untouched() {
1522 let pacing = pacing(5_000, 4, 0, 60_000);
1523 assert!(pacing.due(Duration::from_secs(5), Duration::from_millis(3), Some(1)));
1524 }
1525
1526 #[test]
1527 fn small_tail_defers_until_the_deadline() {
1528 let pacing = pacing(100, 4, 1_000, 60_000);
1529 assert!(!pacing.due(Duration::from_secs(30), Duration::ZERO, Some(999)));
1530 assert!(pacing.due(Duration::from_secs(60), Duration::ZERO, Some(999)));
1531 }
1532
1533 #[test]
1534 fn tail_at_threshold_publishes_at_base_pacing() {
1535 let pacing = pacing(100, 4, 1_000, 60_000);
1536 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, Some(1_000)));
1537 }
1538
1539 #[test]
1540 fn unknown_tail_publishes_at_base_pacing() {
1541 let pacing = pacing(100, 4, 1_000, 60_000);
1542 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, None));
1543 }
1544
1545 #[test]
1546 fn deadline_never_overrides_the_backoff_floor() {
1547 let pacing = pacing(100, 4, 1_000, 200);
1548 let last = Duration::from_millis(300);
1549 assert!(!pacing.due(Duration::from_millis(400), last, Some(1)));
1550 assert!(pacing.due(Duration::from_millis(1_200), last, Some(1)));
1551 }
1552}