1use std::collections::{BTreeSet, HashMap, VecDeque};
6use std::path::PathBuf;
7use std::sync::atomic::{AtomicBool, AtomicU32, 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_crypt::{KeyId, Keyring};
13use corium_db::{Db, Idents};
14use corium_log::{LogError, TransactionLog, TxRecord};
15use corium_protocol::codec::{self, CodecError};
16use corium_protocol::pb;
17use corium_protocol::schemaform::{SchemaFormError, schema_from_edn};
18use corium_protocol::txforms::{TxFormError, tx_items_from_edn};
19use corium_query::edn::Edn;
20use corium_store::{
21 BlobId, BlobStore, KeyManifest, RootStore, StoreError, decode_index_manifest,
22 decode_segment_keys, is_index_manifest, keys_root_name, load_key_manifest, mark_reachable,
23 meta_root_name, publish_key_manifest, sweep_unmarked,
24};
25use thiserror::Error;
26use tokio::sync::{broadcast, oneshot, watch};
27use tracing::Instrument;
28
29use crate::backend::{LogBackend, NodeStore, StorageInfoConfig, StoreSpec};
30use crate::keys::{DbCrypto, DbStore, KeyWiringError, reload_db_crypto, resolve_db_crypto};
31use crate::lease::{self, Lease, LeaseError};
32use crate::metrics::Metrics;
33use crate::{DbRoot, EmbeddedTransactor, Prepared, TransactError, db_root_name};
34
35pub trait TxFnExpander: Send + Sync {
40 fn expand(&self, db: &Db, forms: Vec<Edn>) -> Result<Vec<Edn>, String>;
47}
48
49#[derive(Clone)]
51pub struct NodeConfig {
52 pub store: StoreSpec,
54 pub storage_info: StorageInfoConfig,
57 pub data_dir: PathBuf,
60 pub owner: String,
62 pub lease_ttl_ms: i64,
64 pub lease_wait_ms: i64,
66 pub ha: bool,
70 pub advertise: Option<String>,
73 pub index_interval: Duration,
75 pub index_backoff: u32,
81 pub index_tail_threshold: u64,
85 pub index_tail_deadline: Duration,
87 pub heartbeat_interval: Duration,
89 pub gc_interval: Option<Duration>,
91 pub gc_retention: Duration,
93 pub max_commit_batch: usize,
99 pub max_commit_batch_bytes: usize,
104 pub tx_fn_expander: Option<Arc<dyn TxFnExpander>>,
106 pub keyring: Option<Arc<dyn Keyring>>,
111}
112
113impl std::fmt::Debug for NodeConfig {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("NodeConfig")
116 .field("store", &self.store)
117 .field("storage_info", &self.storage_info)
118 .field("data_dir", &self.data_dir)
119 .field("owner", &self.owner)
120 .field("lease_ttl_ms", &self.lease_ttl_ms)
121 .field("lease_wait_ms", &self.lease_wait_ms)
122 .field("ha", &self.ha)
123 .field("advertise", &self.advertise)
124 .field("index_interval", &self.index_interval)
125 .field("index_backoff", &self.index_backoff)
126 .field("index_tail_threshold", &self.index_tail_threshold)
127 .field("index_tail_deadline", &self.index_tail_deadline)
128 .field("heartbeat_interval", &self.heartbeat_interval)
129 .field("gc_interval", &self.gc_interval)
130 .field("gc_retention", &self.gc_retention)
131 .field("max_commit_batch", &self.max_commit_batch)
132 .field("max_commit_batch_bytes", &self.max_commit_batch_bytes)
133 .field("tx_fn_expander", &self.tx_fn_expander.is_some())
134 .field("keyring", &self.keyring.is_some())
135 .finish()
136 }
137}
138
139impl NodeConfig {
140 #[must_use]
142 pub fn new(data_dir: PathBuf) -> Self {
143 Self {
144 store: StoreSpec::Fs,
145 storage_info: StorageInfoConfig::default(),
146 data_dir,
147 owner: format!(
148 "transactor-{}",
149 std::env::var("HOSTNAME").unwrap_or_else(|_| "local".into())
150 ),
151 lease_ttl_ms: 5_000,
152 lease_wait_ms: 15_000,
153 ha: false,
154 advertise: None,
155 index_interval: Duration::from_secs(5),
156 index_backoff: 4,
157 index_tail_threshold: 0,
158 index_tail_deadline: Duration::from_secs(60),
159 heartbeat_interval: Duration::from_secs(10),
160 gc_interval: Some(Duration::from_secs(60 * 60)),
161 gc_retention: Duration::from_secs(72 * 60 * 60),
162 max_commit_batch: 256,
163 max_commit_batch_bytes: 4 * 1024 * 1024,
164 #[cfg(feature = "cljrs")]
165 tx_fn_expander: Some(Arc::new(crate::txfn::DbFnExpander::default())),
166 #[cfg(not(feature = "cljrs"))]
167 tx_fn_expander: None,
168 keyring: None,
169 }
170 }
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
186pub struct IndexPolicy {
187 pub interval: Duration,
189 pub backoff: u32,
192 pub tail_threshold: u64,
195 pub tail_deadline: Duration,
198}
199
200#[derive(Debug)]
206pub struct KeyStatus {
207 pub manifest: Option<KeyManifest>,
209 pub basis_t: u64,
211 pub keys_unavailable: bool,
214 pub keys_fenced: bool,
217}
218
219#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
221pub struct IndexPolicyUpdate {
222 pub interval: Option<Duration>,
224 pub backoff: Option<u32>,
226 pub tail_threshold: Option<u64>,
228 pub tail_deadline: Option<Duration>,
230}
231
232impl IndexPolicy {
233 fn from_config(config: &NodeConfig) -> Self {
234 Self {
235 interval: config.index_interval,
236 backoff: config.index_backoff,
237 tail_threshold: config.index_tail_threshold,
238 tail_deadline: config.index_tail_deadline,
239 }
240 }
241
242 fn apply(&mut self, update: IndexPolicyUpdate) {
243 if let Some(interval) = update.interval {
244 self.interval = interval;
245 }
246 if let Some(backoff) = update.backoff {
247 self.backoff = backoff;
248 }
249 if let Some(tail_threshold) = update.tail_threshold {
250 self.tail_threshold = tail_threshold;
251 }
252 if let Some(tail_deadline) = update.tail_deadline {
253 self.tail_deadline = tail_deadline;
254 }
255 }
256
257 fn due(&self, since_publish: Duration, last_duration: Duration, pending: Option<u64>) -> bool {
264 let floor = self
265 .interval
266 .max(last_duration.saturating_mul(self.backoff));
267 if since_publish < floor {
268 return false;
269 }
270 match pending {
271 Some(pending) if pending < self.tail_threshold => since_publish >= self.tail_deadline,
272 _ => true,
273 }
274 }
275}
276
277#[derive(Debug, Error)]
279pub enum NodeError {
280 #[error("unknown database {0:?}")]
282 UnknownDb(String),
283 #[error("invalid database name {0:?}")]
285 InvalidName(String),
286 #[error("storage format {found} is newer than supported format {supported}")]
288 UnsupportedFormat {
289 found: u32,
291 supported: u32,
293 },
294 #[error("deposed: write lease for {0:?} is held elsewhere")]
296 Deposed(String),
297 #[error("standby for {db:?}: lease held by {owner} at {endpoint:?}")]
300 Standby {
301 db: String,
303 owner: String,
305 endpoint: String,
307 },
308 #[error(transparent)]
310 Codec(#[from] CodecError),
311 #[error(transparent)]
313 TxForm(#[from] TxFormError),
314 #[error(transparent)]
316 SchemaForm(#[from] SchemaFormError),
317 #[error(transparent)]
319 Transact(#[from] TransactError),
320 #[error(transparent)]
322 Store(#[from] StoreError),
323 #[error(transparent)]
325 Log(#[from] LogError),
326 #[error(transparent)]
328 Lease(#[from] LeaseError),
329 #[error(transparent)]
331 Keys(#[from] KeyWiringError),
332 #[error(
335 "database {db:?} writes under storage-key epoch {writing}, but the key manifest has opened epoch {active} and this node cannot load it; fix --storage-key so the new epoch resolves"
336 )]
337 KeysFenced {
338 db: String,
340 active: u32,
342 writing: u32,
344 },
345 #[error("bad request: {0}")]
347 BadRequest(String),
348 #[error("basis changed: expected {expected}, current basis is {actual}")]
350 BasisMismatch {
351 expected: u64,
353 actual: u64,
355 },
356 #[error("group commit aborted: {0}")]
361 GroupCommit(String),
362}
363
364struct Naming {
365 schema: Schema,
366 idents: Idents,
367 interner: KeywordInterner,
368}
369
370struct CommitRequest {
373 forms: Vec<Edn>,
374 expected_basis_t: Option<u64>,
375 resp: oneshot::Sender<Result<pb::TransactResponse, NodeError>>,
376}
377
378pub struct DbState {
380 name: String,
381 transactor: EmbeddedTransactor,
382 log: Arc<dyn TransactionLog>,
383 crypto: std::sync::RwLock<Arc<DbCrypto>>,
387 key_manifest_version: AtomicU64,
391 keys_unavailable: AtomicBool,
397 keys_fenced: AtomicBool,
404 fenced_active_epoch: AtomicU32,
407 naming: Mutex<Naming>,
408 commit: tokio::sync::Mutex<()>,
412 pending: Mutex<VecDeque<CommitRequest>>,
414 broadcast: broadcast::Sender<pb::subscribe_item::Item>,
415 basis: watch::Sender<u64>,
416 index_basis: AtomicU64,
417 index_policy: Mutex<IndexPolicy>,
418 held_lease: Mutex<Lease>,
419 deposed: AtomicBool,
420}
421
422impl DbState {
423 #[must_use]
425 pub fn name(&self) -> &str {
426 &self.name
427 }
428
429 #[must_use]
432 pub fn store(&self) -> Arc<DbStore> {
433 Arc::clone(
434 &self
435 .crypto
436 .read()
437 .unwrap_or_else(std::sync::PoisonError::into_inner)
438 .store,
439 )
440 }
441
442 fn clear_key_alarms(&self) -> bool {
445 self.keys_unavailable.swap(false, Ordering::AcqRel)
447 | self.keys_fenced.swap(false, Ordering::AcqRel)
448 }
449
450 fn key_alarm_raised(&self) -> bool {
452 self.keys_unavailable.load(Ordering::Acquire) || self.keys_fenced.load(Ordering::Acquire)
453 }
454
455 fn keys_fenced_error(&self) -> Option<NodeError> {
457 if !self.keys_fenced.load(Ordering::Acquire) {
458 return None;
459 }
460 let crypto = self.crypto();
461 Some(NodeError::KeysFenced {
462 db: self.name.clone(),
463 active: self.fenced_active_epoch.load(Ordering::Acquire),
464 writing: crypto.store.storage_epoch().unwrap_or_default(),
465 })
466 }
467
468 fn crypto(&self) -> Arc<DbCrypto> {
469 Arc::clone(
470 &self
471 .crypto
472 .read()
473 .unwrap_or_else(std::sync::PoisonError::into_inner),
474 )
475 }
476
477 #[must_use]
479 pub fn is_encrypted(&self) -> bool {
480 self.store().storage_epoch().is_some()
481 }
482
483 #[must_use]
486 pub fn keys_unavailable(&self) -> bool {
487 self.keys_unavailable.load(Ordering::Acquire)
488 }
489
490 #[must_use]
493 pub fn keys_fenced(&self) -> bool {
494 self.keys_fenced.load(Ordering::Acquire)
495 }
496
497 #[must_use]
499 pub fn db(&self) -> Db {
500 self.transactor.db()
501 }
502
503 #[must_use]
505 pub fn basis_watch(&self) -> watch::Receiver<u64> {
506 self.basis.subscribe()
507 }
508
509 #[must_use]
512 pub fn stream_items(&self) -> broadcast::Receiver<pb::subscribe_item::Item> {
513 self.broadcast.subscribe()
514 }
515
516 #[must_use]
518 pub fn index_basis(&self) -> u64 {
519 self.index_basis.load(Ordering::Acquire)
520 }
521
522 #[must_use]
524 pub fn index_policy(&self) -> IndexPolicy {
525 *self
526 .index_policy
527 .lock()
528 .unwrap_or_else(std::sync::PoisonError::into_inner)
529 }
530
531 #[must_use]
533 pub fn lease(&self) -> Lease {
534 self.held_lease
535 .lock()
536 .unwrap_or_else(std::sync::PoisonError::into_inner)
537 .clone()
538 }
539
540 #[must_use]
543 pub fn handshake_snapshot(&self) -> (Vec<u8>, KeywordInterner) {
544 let naming = self
545 .naming
546 .lock()
547 .unwrap_or_else(std::sync::PoisonError::into_inner);
548 (
549 codec::encode_schema(&naming.schema, &naming.idents),
550 naming.interner.clone(),
551 )
552 }
553
554 pub async fn tx_range(&self, start: u64, end: Option<u64>) -> Result<Vec<TxRecord>, NodeError> {
559 Ok(self.log.tx_range_async(start, end).await?)
560 }
561
562 async fn check_lease(&self, store: &dyn RootStore) -> Result<Lease, NodeError> {
565 if self.deposed.load(Ordering::Acquire) {
566 return Err(NodeError::Deposed(self.name.clone()));
567 }
568 let held = self.lease();
569 match lease::verify(store, &self.name, &held).await {
570 Ok(()) => Ok(held),
571 Err(LeaseError::Lost) => {
572 self.deposed.store(true, Ordering::Release);
573 Err(NodeError::Deposed(self.name.clone()))
574 }
575 Err(error) => Err(error.into()),
576 }
577 }
578}
579
580pub struct TransactorNode {
582 config: NodeConfig,
583 store: Arc<NodeStore>,
584 log_backend: LogBackend,
585 dbs: std::sync::RwLock<HashMap<String, Arc<DbState>>>,
586 standby: std::sync::RwLock<BTreeSet<String>>,
589 gc_lock: tokio::sync::Mutex<()>,
590 fork_lock: tokio::sync::Mutex<()>,
593 metrics: Metrics,
594 shutdown: watch::Sender<Option<String>>,
595}
596
597fn now_unix_ms() -> i64 {
598 i64::try_from(
599 SystemTime::now()
600 .duration_since(UNIX_EPOCH)
601 .unwrap_or_default()
602 .as_millis(),
603 )
604 .unwrap_or(i64::MAX)
605}
606
607fn batch_abort_error(name: &str, error: &NodeError) -> NodeError {
613 match error {
614 NodeError::Deposed(_) => NodeError::Deposed(name.to_owned()),
615 other => NodeError::GroupCommit(other.to_string()),
616 }
617}
618
619fn valid_db_name(name: &str) -> bool {
620 !name.is_empty()
621 && name.len() <= 128
622 && name
623 .bytes()
624 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
625}
626
627impl TransactorNode {
628 pub async fn open(config: NodeConfig) -> Result<Arc<Self>, NodeError> {
636 #[cfg(feature = "s3")]
637 if matches!(config.store, StoreSpec::S3 { .. }) && config.storage_info.s3.is_none() {
638 tracing::warn!(
639 "S3 read-only credentials are not configured; GetStorageInfo will reject \
640 peer bootstrap and direct-storage backup requests"
641 );
642 }
643 config.storage_info.initialize().await;
644 let store = Arc::new(NodeStore::open(&config.store, &config.data_dir).await?);
645 let log_backend = LogBackend::for_spec(&config.store, &config.data_dir, Arc::clone(&store));
646 let node = Arc::new(Self {
647 config,
648 store,
649 log_backend,
650 dbs: std::sync::RwLock::new(HashMap::new()),
651 standby: std::sync::RwLock::new(BTreeSet::new()),
652 gc_lock: tokio::sync::Mutex::new(()),
653 fork_lock: tokio::sync::Mutex::new(()),
654 metrics: Metrics::default(),
655 shutdown: watch::channel(None).0,
656 });
657 let names: Vec<String> = node
658 .store
659 .list_roots("meta:")
660 .await?
661 .into_iter()
662 .filter_map(|root| root.strip_prefix("meta:").map(str::to_owned))
663 .collect();
664 for name in names {
665 match node.open_db(&name).await {
666 Ok(state) => {
667 node.dbs
668 .write()
669 .unwrap_or_else(std::sync::PoisonError::into_inner)
670 .insert(name, state);
671 }
672 Err(NodeError::Lease(LeaseError::Held { owner, .. })) if node.config.ha => {
673 tracing::info!(db = %name, %owner, "standing by; lease held elsewhere");
674 node.standby
675 .write()
676 .unwrap_or_else(std::sync::PoisonError::into_inner)
677 .insert(name);
678 }
679 Err(error) => return Err(error),
680 }
681 }
682 node.spawn_standby_poller();
683 node.spawn_scheduled_gc();
684 Ok(node)
685 }
686
687 #[must_use]
689 pub fn store(&self) -> &Arc<NodeStore> {
690 &self.store
691 }
692
693 #[must_use]
695 pub fn config(&self) -> &NodeConfig {
696 &self.config
697 }
698
699 #[must_use]
701 pub const fn metrics(&self) -> &Metrics {
702 &self.metrics
703 }
704
705 fn spawn_scheduled_gc(self: &Arc<Self>) {
706 let Some(interval) = self.config.gc_interval else {
707 return;
708 };
709 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
710 return;
713 };
714 let node = Arc::clone(self);
715 runtime.spawn(async move {
716 let mut ticker = tokio::time::interval(interval);
717 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
718 ticker.tick().await;
720 loop {
721 ticker.tick().await;
722 if let Err(error) = node.gc_deleted().await {
723 tracing::warn!(%error, "scheduled garbage collection failed");
724 }
725 }
726 });
727 }
728
729 #[must_use]
731 pub fn shutdown_watch(&self) -> watch::Receiver<Option<String>> {
732 self.shutdown.subscribe()
733 }
734
735 fn depose(&self, state: &DbState, reason: &str) {
739 state.deposed.store(true, Ordering::Release);
740 if self.config.ha {
741 tracing::warn!(db = %state.name, reason, "deposed; returning to standby");
742 self.dbs
743 .write()
744 .unwrap_or_else(std::sync::PoisonError::into_inner)
745 .remove(&state.name);
746 self.standby
747 .write()
748 .unwrap_or_else(std::sync::PoisonError::into_inner)
749 .insert(state.name.clone());
750 } else {
751 let _ = self
752 .shutdown
753 .send(Some(format!("database {:?}: {reason}", state.name)));
754 }
755 }
756
757 fn advertised(&self) -> &str {
758 self.config.advertise.as_deref().unwrap_or("")
759 }
760
761 async fn acquire_lease(&self, name: &str) -> Result<Lease, NodeError> {
765 let deadline = now_unix_ms() + self.config.lease_wait_ms;
766 loop {
767 match lease::acquire(
768 self.store.as_ref(),
769 name,
770 &self.config.owner,
771 self.advertised(),
772 self.config.lease_ttl_ms,
773 now_unix_ms(),
774 )
775 .await
776 {
777 Ok(held) => return Ok(held),
778 Err(LeaseError::Held { .. }) if !self.config.ha && now_unix_ms() < deadline => {
779 tokio::time::sleep(Duration::from_millis(200)).await;
780 }
781 Err(error) => return Err(error.into()),
782 }
783 }
784 }
785
786 async fn open_db(self: &Arc<Self>, name: &str) -> Result<Arc<DbState>, NodeError> {
787 let meta = self
788 .store
789 .get_root(&meta_root_name(name))
790 .await?
791 .ok_or_else(|| NodeError::UnknownDb(name.to_owned()))?;
792 let (schema, idents, interner) = codec::decode_metadata(&meta)?;
793 let root_name = db_root_name(name);
794 let current = self
795 .store
796 .get_root(&root_name)
797 .await?
798 .as_deref()
799 .and_then(DbRoot::decode);
800 if let Some(root) = ¤t
801 && root.format_version > corium_store::FORMAT_VERSION
802 {
803 return Err(NodeError::UnsupportedFormat {
804 found: root.format_version,
805 supported: corium_store::FORMAT_VERSION,
806 });
807 }
808 let manifest = load_key_manifest(self.store.as_ref(), name).await?;
812 let crypto = resolve_db_crypto(
813 name,
814 &self.store,
815 manifest.as_ref(),
816 self.config.keyring.as_ref(),
817 )
818 .await?;
819 let held = self.acquire_lease(name).await?;
825 let log = self
828 .log_backend
829 .open(name, held.version, crypto.cipher.clone())
830 .await?;
831 let post_fence = self
832 .store
833 .get_root(&root_name)
834 .await?
835 .as_deref()
836 .and_then(DbRoot::decode);
837 let transactor = self
838 .recover_transactor(
839 name,
840 &schema,
841 &idents,
842 &interner,
843 post_fence.as_ref(),
844 &log,
845 crypto.store.as_ref(),
846 )
847 .await?;
848 let basis_t = transactor.db().basis_t();
849 let key_manifest_version = post_fence
850 .as_ref()
851 .map_or(0, |root| root.key_manifest_version);
852 let index_basis = post_fence.map_or(0, |root| root.index_basis_t);
853 let state = Arc::new(DbState {
854 name: name.to_owned(),
855 transactor,
856 log,
857 crypto: std::sync::RwLock::new(Arc::new(crypto)),
858 key_manifest_version: AtomicU64::new(key_manifest_version),
859 keys_unavailable: AtomicBool::new(false),
860 keys_fenced: AtomicBool::new(false),
861 fenced_active_epoch: AtomicU32::new(0),
862 naming: Mutex::new(Naming {
863 schema,
864 idents,
865 interner,
866 }),
867 commit: tokio::sync::Mutex::new(()),
868 pending: Mutex::new(VecDeque::new()),
869 broadcast: broadcast::channel(1024).0,
870 basis: watch::channel(basis_t).0,
871 index_basis: AtomicU64::new(index_basis),
872 index_policy: Mutex::new(IndexPolicy::from_config(&self.config)),
873 held_lease: Mutex::new(held),
874 deposed: AtomicBool::new(false),
875 });
876 self.spawn_maintenance(&state);
877 Ok(state)
878 }
879
880 #[allow(clippy::too_many_arguments)]
889 async fn recover_transactor(
890 &self,
891 name: &str,
892 schema: &Schema,
893 idents: &Idents,
894 interner: &KeywordInterner,
895 root: Option<&DbRoot>,
896 log: &Arc<dyn TransactionLog>,
897 store: &DbStore,
898 ) -> Result<EmbeddedTransactor, NodeError> {
899 if let Some(root) = root
902 && let Some(roots) = &root.roots
903 && root.next_entity_id != 0
904 {
905 match Self::load_current_snapshot(
906 store,
907 root,
908 &roots[IndexOrder::Eavt as usize],
909 schema,
910 idents,
911 interner,
912 )
913 .await
914 {
915 Ok(snapshot) => {
916 return Ok(EmbeddedTransactor::recover_from_snapshot_async(
917 snapshot,
918 root.next_entity_id,
919 root.last_tx_instant,
920 Arc::clone(log),
921 )
922 .await?);
923 }
924 Err(error) => {
925 tracing::warn!(
926 db = %name,
927 %error,
928 "index-root recovery failed; falling back to full-log replay"
929 );
930 }
931 }
932 }
933 let base = Db::new(schema.clone()).with_naming(idents.clone(), interner.clone());
934 Ok(EmbeddedTransactor::recover_from_async(base, Arc::clone(log)).await?)
935 }
936
937 async fn load_current_snapshot(
942 store: &DbStore,
943 root: &DbRoot,
944 eavt: &BlobId,
945 schema: &Schema,
946 idents: &Idents,
947 interner: &KeywordInterner,
948 ) -> Result<Db, StoreError> {
949 let datoms = Self::load_index_keys(store, eavt)
950 .await?
951 .into_iter()
952 .map(|key| Datom::from_key(IndexOrder::Eavt, &key))
953 .collect::<Result<Vec<_>, _>>()
954 .map_err(|error| StoreError::Io(std::io::Error::other(error.to_string())))?;
955 Ok(Db::from_current_snapshot(
956 root.index_basis_t,
957 schema.clone(),
958 idents.clone(),
959 interner.clone(),
960 datoms,
961 ))
962 }
963
964 async fn load_index_keys(store: &DbStore, id: &BlobId) -> Result<Vec<Vec<u8>>, StoreError> {
967 let blob = store
968 .get(id)
969 .await?
970 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
971 if !is_index_manifest(&blob) {
972 return decode_segment_keys(&blob);
973 }
974 let mut keys = Vec::new();
975 for child in decode_index_manifest(&blob)? {
976 let chunk = store
977 .get(&child)
978 .await?
979 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
980 keys.extend(decode_segment_keys(&chunk)?);
981 }
982 Ok(keys)
983 }
984
985 fn spawn_standby_poller(self: &Arc<Self>) {
991 if !self.config.ha {
992 return;
993 }
994 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
995 return;
996 };
997 let ttl = self.config.lease_ttl_ms;
998 let poll_every = Duration::from_millis(u64::try_from(ttl / 3).unwrap_or(1).max(50));
999 let node = Arc::clone(self);
1000 runtime.spawn(async move {
1001 let mut ticker = tokio::time::interval(poll_every);
1002 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1003 loop {
1004 ticker.tick().await;
1005 if let Err(error) = node.standby_scan().await {
1006 tracing::warn!(%error, "standby scan failed");
1007 }
1008 }
1009 });
1010 }
1011
1012 async fn standby_scan(self: &Arc<Self>) -> Result<(), NodeError> {
1015 let names: Vec<String> = self
1016 .store
1017 .list_roots("meta:")
1018 .await?
1019 .into_iter()
1020 .filter_map(|root| root.strip_prefix("meta:").map(str::to_owned))
1021 .collect();
1022 {
1023 let mut standby = self
1024 .standby
1025 .write()
1026 .unwrap_or_else(std::sync::PoisonError::into_inner);
1027 standby.retain(|name| names.contains(name));
1028 }
1029 for name in names {
1030 if self
1031 .dbs
1032 .read()
1033 .unwrap_or_else(std::sync::PoisonError::into_inner)
1034 .contains_key(&name)
1035 {
1036 continue;
1037 }
1038 match self.open_db(&name).await {
1039 Ok(state) => {
1040 tracing::info!(db = %name, owner = %self.config.owner, "standby took over write lease");
1041 self.standby
1042 .write()
1043 .unwrap_or_else(std::sync::PoisonError::into_inner)
1044 .remove(&name);
1045 self.dbs
1046 .write()
1047 .unwrap_or_else(std::sync::PoisonError::into_inner)
1048 .insert(name, state);
1049 }
1050 Err(NodeError::Lease(LeaseError::Held { .. })) => {
1051 self.standby
1052 .write()
1053 .unwrap_or_else(std::sync::PoisonError::into_inner)
1054 .insert(name);
1055 }
1056 Err(error) => {
1057 tracing::warn!(db = %name, %error, "standby takeover attempt failed");
1058 }
1059 }
1060 }
1061 Ok(())
1062 }
1063
1064 fn spawn_maintenance(self: &Arc<Self>, state: &Arc<DbState>) {
1065 let ttl = self.config.lease_ttl_ms;
1066 let renew_every = Duration::from_millis(u64::try_from(ttl / 3).unwrap_or(1).max(50));
1067 let node = Arc::clone(self);
1069 let db = Arc::clone(state);
1070 tokio::spawn(async move {
1071 let mut ticker = tokio::time::interval(renew_every);
1072 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1073 loop {
1074 ticker.tick().await;
1075 if db.deposed.load(Ordering::Acquire) {
1076 return;
1077 }
1078 let _commit = db.commit.lock().await;
1082 let held = db.lease();
1083 let name = db.name.clone();
1084 let renewed =
1085 lease::renew(node.store.as_ref(), &name, &held, ttl, now_unix_ms()).await;
1086 match renewed {
1087 Ok(renewed) => {
1088 *db.held_lease
1089 .lock()
1090 .unwrap_or_else(std::sync::PoisonError::into_inner) = renewed;
1091 }
1092 Err(LeaseError::Lost) => {
1093 node.depose(&db, "write lease lost");
1094 return;
1095 }
1096 Err(_) => {}
1097 }
1098 if let Err(error) = node.refresh_keys_if_stale(&db).await {
1101 tracing::warn!(db = %name, %error, "cannot reload storage keys");
1102 }
1103 }
1104 });
1105 self.spawn_indexing(state);
1106 let node = Arc::clone(self);
1108 let db = Arc::clone(state);
1109 tokio::spawn(async move {
1110 let mut ticker = tokio::time::interval(node.config.heartbeat_interval);
1111 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
1112 loop {
1113 ticker.tick().await;
1114 if db.deposed.load(Ordering::Acquire) {
1115 return;
1116 }
1117 let _ = db
1118 .broadcast
1119 .send(pb::subscribe_item::Item::Heartbeat(pb::Heartbeat {
1120 basis_t: db.db().basis_t(),
1121 }));
1122 }
1123 });
1124 }
1125
1126 fn spawn_indexing(self: &Arc<Self>, state: &Arc<DbState>) {
1130 const POLICY_POLL: Duration = Duration::from_secs(1);
1134 let node = Arc::clone(self);
1135 let db = Arc::clone(state);
1136 tokio::spawn(async move {
1137 let mut published_at = Instant::now();
1138 let mut last_duration = Duration::ZERO;
1139 let mut published_len: Option<u64> = None;
1140 loop {
1141 let policy = db.index_policy();
1142 tokio::time::sleep(policy.interval.min(POLICY_POLL)).await;
1143 if db.deposed.load(Ordering::Acquire) {
1144 return;
1145 }
1146 let snapshot = db.db();
1147 if snapshot.basis_t() <= db.index_basis() {
1148 continue;
1149 }
1150 let recorded_len = u64::try_from(snapshot.recorded_len()).unwrap_or(u64::MAX);
1151 let pending = published_len.map(|len| recorded_len.saturating_sub(len));
1152 if !policy.due(published_at.elapsed(), last_duration, pending) {
1153 continue;
1154 }
1155 match node.publish_db_indexes(&db).await {
1156 Ok((_, duration)) => {
1157 last_duration = duration;
1158 published_len = Some(recorded_len);
1162 }
1163 Err(NodeError::Deposed(_)) => return,
1164 Err(_) => {}
1165 }
1166 published_at = Instant::now();
1167 }
1168 });
1169 }
1170
1171 async fn publish_db_indexes(&self, db: &Arc<DbState>) -> Result<(u64, Duration), NodeError> {
1176 let _gc = self.gc_lock.lock().await;
1177 let version = db.lease().version;
1178 let root_name = db_root_name(&db.name);
1179 let started = Instant::now();
1180 let published = db
1181 .transactor
1182 .publish_indexes(db.store().as_ref(), &root_name, version)
1183 .await;
1184 let duration = started.elapsed();
1185 self.metrics.record_index(duration);
1186 match published {
1187 Ok(root) => {
1188 tracing::debug!(db = %db.name, index_basis_t = root.index_basis_t, "published indexes");
1189 db.index_basis.store(root.index_basis_t, Ordering::Release);
1190 let _ = db
1191 .broadcast
1192 .send(pb::subscribe_item::Item::IndexBasis(pb::IndexBasis {
1193 index_basis_t: root.index_basis_t,
1194 }));
1195 Ok((root.index_basis_t, duration))
1196 }
1197 Err(TransactError::Deposed { .. }) => {
1198 self.depose(db, "database root fenced by a newer lease");
1199 Err(NodeError::Deposed(db.name.clone()))
1200 }
1201 Err(error) => Err(error.into()),
1202 }
1203 }
1204
1205 pub async fn request_index(&self, name: &str) -> Result<u64, NodeError> {
1214 let state = self.db_state(name).await?;
1215 if state.db().basis_t() <= state.index_basis() {
1216 return Ok(state.index_basis());
1217 }
1218 self.publish_db_indexes(&state)
1219 .await
1220 .map(|(index_basis_t, _)| index_basis_t)
1221 }
1222
1223 pub async fn set_index_policy(
1230 &self,
1231 name: &str,
1232 update: IndexPolicyUpdate,
1233 ) -> Result<IndexPolicy, NodeError> {
1234 let state = self.db_state(name).await?;
1235 let mut policy = state
1236 .index_policy
1237 .lock()
1238 .unwrap_or_else(std::sync::PoisonError::into_inner);
1239 policy.apply(update);
1240 Ok(*policy)
1241 }
1242
1243 pub async fn db_state(&self, name: &str) -> Result<Arc<DbState>, NodeError> {
1249 if let Some(state) = self
1250 .dbs
1251 .read()
1252 .unwrap_or_else(std::sync::PoisonError::into_inner)
1253 .get(name)
1254 .cloned()
1255 {
1256 return Ok(state);
1257 }
1258 if self.config.ha
1259 && self
1260 .standby
1261 .read()
1262 .unwrap_or_else(std::sync::PoisonError::into_inner)
1263 .contains(name)
1264 {
1265 let root = self
1266 .store
1267 .get_root(&db_root_name(name))
1268 .await?
1269 .as_deref()
1270 .and_then(DbRoot::decode);
1271 return Err(NodeError::Standby {
1272 db: name.to_owned(),
1273 owner: root.as_ref().map(|r| r.owner.clone()).unwrap_or_default(),
1274 endpoint: root.map(|r| r.owner_endpoint).unwrap_or_default(),
1275 });
1276 }
1277 Err(NodeError::UnknownDb(name.to_owned()))
1278 }
1279
1280 #[must_use]
1282 pub fn standby_dbs(&self) -> Vec<String> {
1283 self.standby
1284 .read()
1285 .unwrap_or_else(std::sync::PoisonError::into_inner)
1286 .iter()
1287 .cloned()
1288 .collect()
1289 }
1290
1291 pub async fn create_db(
1303 self: &Arc<Self>,
1304 name: &str,
1305 schema_edn: &[u8],
1306 storage_key: Option<KeyId>,
1307 ) -> Result<bool, NodeError> {
1308 if !valid_db_name(name) {
1309 return Err(NodeError::InvalidName(name.to_owned()));
1310 }
1311 if self
1312 .dbs
1313 .read()
1314 .unwrap_or_else(std::sync::PoisonError::into_inner)
1315 .contains_key(name)
1316 {
1317 return Ok(false);
1318 }
1319 let forms = match codec::decode_edn(schema_edn)? {
1320 Edn::Vector(items) | Edn::List(items) => items,
1321 Edn::Nil => Vec::new(),
1322 other => {
1323 return Err(NodeError::BadRequest(format!(
1324 "schema must be a vector of attribute maps, got {other}"
1325 )));
1326 }
1327 };
1328 let (schema, idents) = schema_from_edn(&forms)?;
1329 let meta = codec::encode_metadata(&schema, &idents, &KeywordInterner::default());
1330 let encrypted = storage_key.is_some();
1335 if let Some(kek) = storage_key {
1336 self.create_key_manifest(name, kek).await?;
1337 }
1338 match self
1339 .store
1340 .cas_root(&meta_root_name(name), None, &meta)
1341 .await
1342 {
1343 Ok(()) => {}
1344 Err(StoreError::CasFailed { .. }) => {
1345 if encrypted {
1348 self.store.delete_root(&keys_root_name(name)).await?;
1349 }
1350 return Ok(false);
1351 }
1352 Err(error) => return Err(error.into()),
1353 }
1354 let state = self.open_db(name).await?;
1355 self.dbs
1356 .write()
1357 .unwrap_or_else(std::sync::PoisonError::into_inner)
1358 .insert(name.to_owned(), state);
1359 Ok(true)
1360 }
1361
1362 pub async fn fork_db(
1373 self: &Arc<Self>,
1374 source: &str,
1375 target: &str,
1376 as_of_t: u64,
1377 ) -> Result<Option<u64>, NodeError> {
1378 if !valid_db_name(target) {
1379 return Err(NodeError::InvalidName(target.to_owned()));
1380 }
1381 if source == target {
1382 return Err(NodeError::BadRequest(
1383 "fork target must differ from the source".into(),
1384 ));
1385 }
1386 let state = self.db_state(source).await?;
1387 let basis = state.db().basis_t();
1388 let t = if as_of_t == 0 { basis } else { as_of_t };
1389 if t > basis {
1390 return Err(NodeError::BadRequest(format!(
1391 "as-of t {t} is ahead of {source:?} basis {basis}"
1392 )));
1393 }
1394 let _guard = self.fork_lock.lock().await;
1395 if self
1396 .dbs
1397 .read()
1398 .unwrap_or_else(std::sync::PoisonError::into_inner)
1399 .contains_key(target)
1400 || self
1401 .store
1402 .get_root(&meta_root_name(target))
1403 .await?
1404 .is_some()
1405 || self.log_backend.exists(target).await
1406 {
1407 return Ok(None);
1408 }
1409 let records = state.log.tx_range_async(0, Some(t + 1)).await?;
1415 let meta = self
1416 .store
1417 .get_root(&meta_root_name(source))
1418 .await?
1419 .ok_or_else(|| NodeError::UnknownDb(source.to_owned()))?;
1420 let target_crypto = match load_key_manifest(self.store.as_ref(), source).await? {
1429 Some(source_manifest) => {
1430 let manifest = self
1431 .create_key_manifest(target, source_manifest.kek)
1432 .await?;
1433 Some(
1434 resolve_db_crypto(
1435 target,
1436 &self.store,
1437 Some(&manifest),
1438 self.config.keyring.as_ref(),
1439 )
1440 .await?,
1441 )
1442 }
1443 None => None,
1444 };
1445 let log = self
1446 .log_backend
1447 .open(
1448 target,
1449 0,
1450 target_crypto
1451 .as_ref()
1452 .and_then(|crypto| crypto.cipher.clone()),
1453 )
1454 .await?;
1455 for record in &records {
1456 log.append_async(record).await?;
1457 }
1458 drop(log);
1459 match self
1460 .store
1461 .cas_root(&meta_root_name(target), None, &meta)
1462 .await
1463 {
1464 Ok(()) => {}
1465 Err(StoreError::CasFailed { .. }) => {
1466 self.log_backend.delete_all(target).await?;
1469 if target_crypto.is_some() {
1470 self.store.delete_root(&keys_root_name(target)).await?;
1471 }
1472 return Ok(None);
1473 }
1474 Err(error) => return Err(error.into()),
1475 }
1476 let state = self.open_db(target).await?;
1477 self.dbs
1478 .write()
1479 .unwrap_or_else(std::sync::PoisonError::into_inner)
1480 .insert(target.to_owned(), state);
1481 Ok(Some(t))
1482 }
1483
1484 pub async fn delete_db(&self, name: &str) -> Result<bool, NodeError> {
1490 let Some(state) = self
1491 .dbs
1492 .write()
1493 .unwrap_or_else(std::sync::PoisonError::into_inner)
1494 .remove(name)
1495 else {
1496 return Ok(false);
1497 };
1498 state.deposed.store(true, Ordering::Release);
1499 self.standby
1500 .write()
1501 .unwrap_or_else(std::sync::PoisonError::into_inner)
1502 .remove(name);
1503 self.store.delete_root(&db_root_name(name)).await?;
1504 self.store.delete_root(&meta_root_name(name)).await?;
1505 self.store.delete_root(&keys_root_name(name)).await?;
1509 self.log_backend.delete_all(name).await?;
1510 Ok(true)
1511 }
1512
1513 #[must_use]
1515 pub fn list_dbs(&self) -> Vec<String> {
1516 let mut names: Vec<String> = self
1517 .dbs
1518 .read()
1519 .unwrap_or_else(std::sync::PoisonError::into_inner)
1520 .keys()
1521 .cloned()
1522 .collect();
1523 names.sort();
1524 names
1525 }
1526
1527 async fn gc_store(&self, db: &str) -> Result<Arc<DbStore>, NodeError> {
1534 if let Some(state) = self
1535 .dbs
1536 .read()
1537 .unwrap_or_else(std::sync::PoisonError::into_inner)
1538 .get(db)
1539 .cloned()
1540 {
1541 return Ok(state.store());
1542 }
1543 let manifest = load_key_manifest(self.store.as_ref(), db).await?;
1544 Ok(resolve_db_crypto(
1545 db,
1546 &self.store,
1547 manifest.as_ref(),
1548 self.config.keyring.as_ref(),
1549 )
1550 .await?
1551 .store)
1552 }
1553
1554 async fn create_key_manifest(&self, db: &str, kek: KeyId) -> Result<KeyManifest, NodeError> {
1556 let keyring = self.keyring()?;
1557 let manifest = KeyManifest::create(keyring.as_ref(), kek, now_unix_ms()).await?;
1558 match publish_key_manifest(self.store.as_ref(), db, None, &manifest).await {
1559 Ok(()) => Ok(manifest),
1560 Err(StoreError::CasFailed { .. }) => Err(NodeError::BadRequest(format!(
1561 "database {db:?} already has a key manifest; \
1562 remove the stale {} root before recreating it",
1563 keys_root_name(db)
1564 ))),
1565 Err(error) => Err(error.into()),
1566 }
1567 }
1568
1569 fn keyring(&self) -> Result<&Arc<dyn Keyring>, NodeError> {
1570 self.config.keyring.as_ref().ok_or_else(|| {
1571 NodeError::BadRequest(
1572 "this transactor holds no storage keys; start it with --storage-key".into(),
1573 )
1574 })
1575 }
1576
1577 pub async fn key_status(&self, name: &str) -> Result<KeyStatus, NodeError> {
1584 let state = self.db_state(name).await?;
1585 Ok(KeyStatus {
1586 manifest: load_key_manifest(self.store.as_ref(), name).await?,
1587 basis_t: state.db().basis_t(),
1588 keys_unavailable: state.keys_unavailable(),
1589 keys_fenced: state.keys_fenced(),
1590 })
1591 }
1592
1593 pub async fn rotate_storage_key(&self, name: &str) -> Result<u32, NodeError> {
1606 let state = self.db_state(name).await?;
1607 let keyring = Arc::clone(self.keyring()?);
1608 let _commit = state.commit.lock().await;
1609 let previous = self.require_key_manifest(name).await?;
1610 let mut manifest = previous.clone();
1611 let epoch = manifest
1612 .rotate_storage_key(keyring.as_ref(), now_unix_ms(), state.db().basis_t())
1613 .await?;
1614 self.install_key_manifest(&state, Some(&previous), &manifest)
1615 .await?;
1616 tracing::info!(db = %name, epoch, "opened a new storage-key epoch");
1617 Ok(epoch)
1618 }
1619
1620 pub async fn rewrap_keys(&self, name: &str, kek: KeyId) -> Result<(), NodeError> {
1626 let state = self.db_state(name).await?;
1627 let keyring = Arc::clone(self.keyring()?);
1628 let _commit = state.commit.lock().await;
1629 let previous = self.require_key_manifest(name).await?;
1630 let mut manifest = previous.clone();
1631 manifest.rewrap(keyring.as_ref(), kek.clone()).await?;
1632 self.install_key_manifest(&state, Some(&previous), &manifest)
1633 .await?;
1634 tracing::info!(db = %name, %kek, "re-wrapped storage keys under a new KEK");
1635 Ok(())
1636 }
1637
1638 async fn require_key_manifest(&self, name: &str) -> Result<KeyManifest, NodeError> {
1639 load_key_manifest(self.store.as_ref(), name)
1640 .await?
1641 .filter(|manifest| !manifest.storage_keys.is_empty())
1642 .ok_or_else(|| {
1643 NodeError::BadRequest(format!(
1644 "database {name:?} is not encrypted; encryption is fixed at creation \
1645 (corium db create --storage-key)"
1646 ))
1647 })
1648 }
1649
1650 async fn install_key_manifest(
1658 &self,
1659 state: &Arc<DbState>,
1660 previous: Option<&KeyManifest>,
1661 manifest: &KeyManifest,
1662 ) -> Result<(), NodeError> {
1663 publish_key_manifest(self.store.as_ref(), &state.name, previous, manifest).await?;
1664 let version = self.bump_key_manifest_version(&state.name).await?;
1665 let crypto = state.crypto();
1666 let store =
1667 reload_db_crypto(&state.name, &crypto, manifest, self.config.keyring.as_ref()).await?;
1668 *state
1669 .crypto
1670 .write()
1671 .unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::new(DbCrypto {
1672 store,
1673 cipher: crypto.cipher.clone(),
1674 });
1675 state.key_manifest_version.store(version, Ordering::Release);
1676 if state.clear_key_alarms() {
1679 self.metrics.record_keys_available();
1680 }
1681 Ok(())
1682 }
1683
1684 async fn bump_key_manifest_version(&self, name: &str) -> Result<u64, NodeError> {
1693 const ATTEMPTS: usize = 5;
1694 let root_name = db_root_name(name);
1695 for attempt in 1..=ATTEMPTS {
1696 let stored = self.store.get_root(&root_name).await?;
1697 let mut root = stored
1698 .as_deref()
1699 .and_then(DbRoot::decode)
1700 .ok_or_else(|| NodeError::UnknownDb(name.to_owned()))?;
1701 root.key_manifest_version = root.key_manifest_version.saturating_add(1);
1702 match self
1703 .store
1704 .cas_root(&root_name, stored.as_deref(), &root.encode())
1705 .await
1706 {
1707 Ok(()) => return Ok(root.key_manifest_version),
1708 Err(StoreError::CasFailed { .. }) if attempt < ATTEMPTS => {}
1709 Err(error) => return Err(error.into()),
1710 }
1711 }
1712 unreachable!("the final attempt returns its result")
1713 }
1714
1715 async fn refresh_keys_if_stale(&self, state: &Arc<DbState>) -> Result<(), NodeError> {
1731 let Some(root) = self
1732 .store
1733 .get_root(&db_root_name(&state.name))
1734 .await?
1735 .as_deref()
1736 .and_then(DbRoot::decode)
1737 else {
1738 return Ok(());
1739 };
1740 if root.key_manifest_version == state.key_manifest_version.load(Ordering::Acquire) {
1741 return Ok(());
1742 }
1743 let Some(manifest) = load_key_manifest(self.store.as_ref(), &state.name).await? else {
1744 return Ok(());
1745 };
1746 let crypto = state.crypto();
1747 match reload_db_crypto(
1748 &state.name,
1749 &crypto,
1750 &manifest,
1751 self.config.keyring.as_ref(),
1752 )
1753 .await
1754 {
1755 Ok(store) => {
1756 *state
1757 .crypto
1758 .write()
1759 .unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::new(DbCrypto {
1760 store,
1761 cipher: crypto.cipher.clone(),
1762 });
1763 state
1764 .key_manifest_version
1765 .store(root.key_manifest_version, Ordering::Release);
1766 if state.clear_key_alarms() {
1767 self.metrics.record_keys_available();
1768 }
1769 tracing::info!(
1770 db = %state.name,
1771 key_manifest_version = root.key_manifest_version,
1772 "reloaded storage keys after a manifest change"
1773 );
1774 Ok(())
1775 }
1776 Err(error) => {
1777 self.raise_key_alarm(state, &crypto, &manifest, &error);
1778 Ok(())
1779 }
1780 }
1781 }
1782
1783 fn raise_key_alarm(
1790 &self,
1791 state: &Arc<DbState>,
1792 crypto: &DbCrypto,
1793 manifest: &KeyManifest,
1794 error: &KeyWiringError,
1795 ) {
1796 let writing = crypto.store.storage_epoch();
1797 let active = manifest.active_storage_epoch();
1798 let was_alarmed = state.key_alarm_raised();
1799 let fenced =
1800 matches!((active, writing), (Some(active), Some(writing)) if active != writing);
1801 if fenced {
1802 let active = active.unwrap_or_default();
1803 state.fenced_active_epoch.store(active, Ordering::Release);
1804 if !state.keys_fenced.swap(true, Ordering::AcqRel) {
1805 tracing::error!(
1806 db = %state.name,
1807 active_epoch = active,
1808 writing_epoch = writing.unwrap_or_default(),
1809 %error,
1810 "storage keys are fenced: the manifest opened an epoch this node cannot \
1811 load, so writes are refused until --storage-key resolves it"
1812 );
1813 }
1814 } else if !state.keys_unavailable.swap(true, Ordering::AcqRel) {
1815 tracing::warn!(
1816 db = %state.name,
1817 %error,
1818 "cannot load the changed key manifest; continuing on the keys already held \
1819 (they still open this database, and the epoch new writes use is unchanged)"
1820 );
1821 }
1822 if !was_alarmed {
1823 self.metrics.record_keys_unavailable();
1824 }
1825 }
1826
1827 pub async fn gc_deleted(&self) -> Result<u64, NodeError> {
1833 self.gc_deleted_with_retention(self.config.gc_retention)
1834 .await
1835 }
1836
1837 pub async fn gc_deleted_with_retention(&self, retention: Duration) -> Result<u64, NodeError> {
1842 let _gc = self.gc_lock.lock().await;
1843 let mut marked = std::collections::HashSet::new();
1849 for root_name in self.store.list_roots("db:").await? {
1850 let Some(root) = self
1851 .store
1852 .get_root(&root_name)
1853 .await?
1854 .as_deref()
1855 .and_then(DbRoot::decode)
1856 else {
1857 continue;
1858 };
1859 let Some(roots) = root.roots else {
1860 continue;
1861 };
1862 let db = root_name.strip_prefix("db:").unwrap_or(&root_name);
1863 let store = self.gc_store(db).await?;
1864 mark_reachable(
1865 store.as_ref(),
1866 roots,
1867 |_, bytes| corium_store::index_blob_children(bytes),
1868 &mut marked,
1869 )
1870 .await?;
1871 }
1872 let report =
1873 sweep_unmarked(self.store.as_ref(), &marked, retention, SystemTime::now()).await?;
1874 self.metrics
1875 .record_gc(report.swept as u64, report.retained as u64);
1876 tracing::info!(
1877 marked = report.marked,
1878 swept = report.swept,
1879 retained = report.retained,
1880 "garbage collection completed"
1881 );
1882 Ok(report.swept as u64)
1883 }
1884
1885 pub async fn transact(
1892 &self,
1893 name: &str,
1894 tx_data: &[u8],
1895 ) -> Result<pb::TransactResponse, NodeError> {
1896 self.transact_at(name, tx_data, None).await
1897 }
1898
1899 pub async fn transact_at(
1909 &self,
1910 name: &str,
1911 tx_data: &[u8],
1912 expected_basis_t: Option<u64>,
1913 ) -> Result<pb::TransactResponse, NodeError> {
1914 let started = Instant::now();
1915 let result = self
1916 .transact_inner(name, tx_data, expected_basis_t)
1917 .instrument(tracing::info_span!("transact", db = name))
1918 .await;
1919 self.metrics.record_tx(started.elapsed(), result.is_ok());
1920 if let Err(error) = &result {
1921 tracing::warn!(%error, "transaction failed");
1922 }
1923 result
1924 }
1925
1926 async fn transact_inner(
1927 &self,
1928 name: &str,
1929 tx_data: &[u8],
1930 expected_basis_t: Option<u64>,
1931 ) -> Result<pb::TransactResponse, NodeError> {
1932 let state = self.db_state(name).await?;
1933 let decoded = codec::decode_edn(tx_data)?;
1934 let forms = decoded
1935 .as_seq()
1936 .ok_or_else(|| NodeError::BadRequest("tx-data must be a vector".into()))?
1937 .to_vec();
1938 let (resp_tx, mut resp_rx) = oneshot::channel();
1944 state
1945 .pending
1946 .lock()
1947 .unwrap_or_else(std::sync::PoisonError::into_inner)
1948 .push_back(CommitRequest {
1949 forms,
1950 expected_basis_t,
1951 resp: resp_tx,
1952 });
1953 let queued = self.metrics.queue_waiter();
1954 loop {
1955 let commit = state.commit.lock().await;
1956 match resp_rx.try_recv() {
1958 Ok(result) => {
1959 drop(commit);
1960 drop(queued);
1961 return result;
1962 }
1963 Err(oneshot::error::TryRecvError::Empty) => {}
1964 Err(oneshot::error::TryRecvError::Closed) => {
1965 drop(commit);
1966 drop(queued);
1967 return Err(NodeError::GroupCommit("commit response dropped".into()));
1968 }
1969 }
1970 self.flush_commit_batch(&state).await;
1972 drop(commit);
1973 match resp_rx.try_recv() {
1974 Ok(result) => {
1975 drop(queued);
1976 return result;
1977 }
1978 Err(oneshot::error::TryRecvError::Empty) => {}
1981 Err(oneshot::error::TryRecvError::Closed) => {
1982 drop(queued);
1983 return Err(NodeError::GroupCommit("commit response dropped".into()));
1984 }
1985 }
1986 }
1987 }
1988
1989 #[allow(clippy::too_many_lines)]
1998 async fn flush_commit_batch(&self, state: &Arc<DbState>) {
1999 let max_count = self.config.max_commit_batch.max(1);
2000 let max_bytes = self.config.max_commit_batch_bytes;
2001
2002 let mut batch: VecDeque<CommitRequest> = {
2003 let mut pending = state
2004 .pending
2005 .lock()
2006 .unwrap_or_else(std::sync::PoisonError::into_inner);
2007 std::mem::take(&mut *pending)
2008 };
2009 if batch.is_empty() {
2010 return;
2011 }
2012 if let Some(error) = state.keys_fenced_error() {
2017 for request in batch {
2018 let _ = request
2019 .resp
2020 .send(Err(batch_abort_error(&state.name, &error)));
2021 }
2022 return;
2023 }
2024 let now_ms = now_unix_ms();
2033 let mut cursor = state.transactor.batch_cursor();
2034 let mut resps: Vec<oneshot::Sender<Result<pb::TransactResponse, NodeError>>> = Vec::new();
2035 let mut prepared: Vec<Prepared> = Vec::new();
2036 let mut batch_bytes: usize = 0;
2037 let mut measure = Vec::new();
2038 let mut naming_changed = false;
2039 while let Some(request) = batch.pop_front() {
2040 if let Some(expected) = request.expected_basis_t {
2041 let actual = cursor.db().basis_t();
2042 if actual != expected {
2043 let _ = request
2044 .resp
2045 .send(Err(NodeError::BasisMismatch { expected, actual }));
2046 continue;
2047 }
2048 }
2049 let forms = if let Some(expander) = &self.config.tx_fn_expander {
2053 let expander = Arc::clone(expander);
2054 let db = cursor.db().clone();
2055 let forms = request.forms;
2056 match tokio::task::spawn_blocking(move || expander.expand(&db, forms)).await {
2057 Ok(Ok(forms)) => forms,
2058 Ok(Err(message)) => {
2059 let _ = request.resp.send(Err(NodeError::BadRequest(message)));
2060 continue;
2061 }
2062 Err(error) => {
2063 let _ = request.resp.send(Err(NodeError::BadRequest(format!(
2064 "expander task failed: {error}"
2065 ))));
2066 continue;
2067 }
2068 }
2069 } else {
2070 request.forms
2071 };
2072 let (items, this_changed) = {
2075 let mut naming = state
2076 .naming
2077 .lock()
2078 .unwrap_or_else(std::sync::PoisonError::into_inner);
2079 let before = naming.interner.len();
2080 match tx_items_from_edn(cursor.db(), &mut naming.interner, &forms) {
2081 Ok(items) => (items, naming.interner.len() > before),
2082 Err(error) => {
2083 drop(naming);
2084 let _ = request.resp.send(Err(error.into()));
2085 continue;
2086 }
2087 }
2088 };
2089 match cursor.prepare(items, now_ms) {
2090 Ok(prep) => {
2091 measure.clear();
2092 let _ = corium_log::append_framed_record(&mut measure, &prep.record);
2093 batch_bytes += measure.len();
2094 resps.push(request.resp);
2095 prepared.push(prep);
2096 }
2097 Err(error) => {
2098 let _ = request.resp.send(Err(NodeError::Transact(error.into())));
2099 continue;
2100 }
2101 }
2102 if this_changed {
2103 naming_changed = true;
2104 break;
2105 }
2106 if prepared.len() >= max_count || batch_bytes >= max_bytes {
2110 break;
2111 }
2112 }
2113 if !batch.is_empty() {
2115 let mut pending = state
2116 .pending
2117 .lock()
2118 .unwrap_or_else(std::sync::PoisonError::into_inner);
2119 while let Some(request) = batch.pop_back() {
2120 pending.push_front(request);
2121 }
2122 }
2123 if prepared.is_empty() {
2124 return;
2125 }
2126 let interner = {
2129 let naming = state
2130 .naming
2131 .lock()
2132 .unwrap_or_else(std::sync::PoisonError::into_inner);
2133 naming.interner.clone()
2134 };
2135 let changed_idents = if naming_changed {
2136 if let Err(error) = state.check_lease(self.store.as_ref()).await {
2140 if matches!(error, NodeError::Deposed(_)) {
2141 self.depose(state, "write lease lost before metadata publish");
2142 }
2143 for resp in resps {
2144 let _ = resp.send(Err(batch_abort_error(&state.name, &error)));
2145 }
2146 return;
2147 }
2148 let (idents, schema) = {
2149 let naming = state
2150 .naming
2151 .lock()
2152 .unwrap_or_else(std::sync::PoisonError::into_inner);
2153 (naming.idents.clone(), naming.schema.clone())
2154 };
2155 let meta = codec::encode_metadata(&schema, &idents, &interner);
2158 loop {
2159 let cas = match self.store.get_root(&meta_root_name(&state.name)).await {
2160 Ok(current) => {
2161 self.store
2162 .cas_root(&meta_root_name(&state.name), current.as_deref(), &meta)
2163 .await
2164 }
2165 Err(error) => Err(error),
2166 };
2167 match cas {
2168 Ok(()) => break,
2169 Err(StoreError::CasFailed { .. }) => {}
2170 Err(error) => {
2171 let error = NodeError::Store(error);
2172 for resp in resps {
2173 let _ = resp.send(Err(batch_abort_error(&state.name, &error)));
2174 }
2175 return;
2176 }
2177 }
2178 }
2179 Some(idents)
2180 } else {
2181 None
2182 };
2183 let records: Vec<TxRecord> = prepared.iter().map(|prep| prep.record.clone()).collect();
2185 if let Err(error) = state.log.append_batch_async(&records).await {
2186 let error = NodeError::Log(error);
2187 for resp in resps {
2188 let _ = resp.send(Err(batch_abort_error(&state.name, &error)));
2189 }
2190 return;
2191 }
2192 let reports = state.transactor.install_batch(cursor, prepared);
2198 if let Some(idents) = changed_idents {
2199 state.transactor.update_naming(idents, interner.clone());
2200 }
2201 if let Err(error) = state.check_lease(self.store.as_ref()).await {
2210 if matches!(error, NodeError::Deposed(_)) {
2211 self.depose(state, "write lease lost after durable append");
2212 }
2213 for resp in resps {
2214 let _ = resp.send(Err(batch_abort_error(&state.name, &error)));
2215 }
2216 return;
2217 }
2218 let mut last_t = 0;
2219 for (resp, report) in resps.into_iter().zip(reports) {
2220 let t = report.db_after.basis_t();
2221 last_t = last_t.max(t);
2222 let datoms = match codec::encode_datoms(&report.tx.datoms, &interner) {
2223 Ok(datoms) => datoms,
2224 Err(error) => {
2225 let _ = resp.send(Err(NodeError::Codec(error)));
2226 continue;
2227 }
2228 };
2229 let tempids = codec::encode_edn(&Edn::Map(
2230 report
2231 .tx
2232 .tempids
2233 .iter()
2234 .map(|(tempid, eid)| {
2235 (
2236 Edn::Str(tempid.clone()),
2237 Edn::Long(i64::try_from(eid.raw()).unwrap_or(i64::MAX)),
2238 )
2239 })
2240 .collect(),
2241 ));
2242 let _ = state
2243 .broadcast
2244 .send(pb::subscribe_item::Item::Report(pb::TxReport {
2245 t,
2246 tx_instant: report.tx_instant,
2247 datoms: datoms.clone(),
2248 }));
2249 let _ = resp.send(Ok(pb::TransactResponse {
2250 basis_before: report.db_before.basis_t(),
2251 basis_t: t,
2252 tx_instant: report.tx_instant,
2253 tempids,
2254 tx_data: datoms,
2255 }));
2256 }
2257 if last_t > 0 {
2258 let _ = state.basis.send(last_t);
2259 }
2260 }
2261
2262 pub async fn status(&self, name: &str) -> Result<pb::StatusResponse, NodeError> {
2267 let state = self.db_state(name).await?;
2268 let db = state.db();
2269 let counts = db.stats();
2270 let held = state.lease();
2271 let metrics = self.metrics.snapshot();
2272 Ok(pb::StatusResponse {
2273 basis_t: db.basis_t(),
2274 index_basis_t: state.index_basis(),
2275 lease_owner: held.owner,
2276 lease_version: held.version,
2277 lease_expires_unix_ms: held.expires_unix_ms,
2278 datom_count: counts.datoms as u64,
2279 entity_count: counts.entities as u64,
2280 attribute_count: counts.attributes as u64,
2281 transaction_count: metrics.tx_total,
2282 transaction_failure_count: metrics.tx_failed,
2283 transaction_queue_depth: metrics.queue_depth,
2284 index_lag: db.basis_t().saturating_sub(state.index_basis()),
2285 indexing_runs: metrics.index_runs,
2286 gc_runs: metrics.gc_runs,
2287 gc_swept_blobs: metrics.gc_swept,
2288 lease_owner_endpoint: held.endpoint,
2289 })
2290 }
2291
2292 pub async fn backup_info(&self, name: &str) -> Result<pb::GetStorageInfoResponse, NodeError> {
2300 let state = self.db_state(name).await?;
2301 let basis_t = {
2305 let _commit = state.commit.lock().await;
2306 state.check_lease(self.store.as_ref()).await?;
2307 state.db().basis_t()
2308 };
2309 let storage = self
2313 .config
2314 .store
2315 .connection_info(&self.config.data_dir, &self.config.storage_info)
2316 .await
2317 .map_err(NodeError::BadRequest)?;
2318 Ok(pb::GetStorageInfoResponse {
2319 basis_t,
2320 storage: Some(storage),
2321 })
2322 }
2323
2324 pub async fn release_leases(&self) {
2329 let states: Vec<Arc<DbState>> = self
2330 .dbs
2331 .write()
2332 .unwrap_or_else(std::sync::PoisonError::into_inner)
2333 .drain()
2334 .map(|(_, state)| state)
2335 .collect();
2336 for state in states {
2337 state.deposed.store(true, Ordering::Release);
2338 if let Err(error) =
2339 lease::release(self.store.as_ref(), &state.name, &state.lease()).await
2340 {
2341 tracing::warn!(db = %state.name, %error, "lease release failed at shutdown");
2342 }
2343 }
2344 }
2345
2346 pub async fn sync(&self, name: &str, t: u64) -> Result<u64, NodeError> {
2351 let state = self.db_state(name).await?;
2352 let mut basis = state.basis_watch();
2353 let target = if t == 0 { *basis.borrow() } else { t };
2354 loop {
2355 let current = *basis.borrow();
2356 if current >= target {
2357 return Ok(current);
2358 }
2359 if basis.changed().await.is_err() {
2360 return Ok(*basis.borrow());
2361 }
2362 }
2363 }
2364}
2365
2366#[cfg(test)]
2367mod tests {
2368 use super::IndexPolicy;
2369 use std::time::Duration;
2370
2371 fn pacing(interval_ms: u64, backoff: u32, threshold: u64, deadline_ms: u64) -> IndexPolicy {
2372 IndexPolicy {
2373 interval: Duration::from_millis(interval_ms),
2374 backoff,
2375 tail_threshold: threshold,
2376 tail_deadline: Duration::from_millis(deadline_ms),
2377 }
2378 }
2379
2380 #[test]
2381 fn base_interval_gates_publication() {
2382 let pacing = pacing(100, 4, 0, 60_000);
2383 assert!(!pacing.due(Duration::from_millis(99), Duration::ZERO, None));
2384 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, None));
2385 }
2386
2387 #[test]
2388 fn backoff_stretches_the_floor_past_the_interval() {
2389 let pacing = pacing(100, 4, 0, 60_000);
2390 let last = Duration::from_millis(300);
2391 assert!(!pacing.due(Duration::from_millis(1_199), last, Some(10)));
2392 assert!(pacing.due(Duration::from_millis(1_200), last, Some(10)));
2393 }
2394
2395 #[test]
2396 fn zero_backoff_keeps_the_base_interval() {
2397 let pacing = pacing(100, 0, 0, 60_000);
2398 assert!(pacing.due(
2399 Duration::from_millis(100),
2400 Duration::from_secs(30),
2401 Some(10)
2402 ));
2403 }
2404
2405 #[test]
2406 fn fast_publications_leave_the_interval_untouched() {
2407 let pacing = pacing(5_000, 4, 0, 60_000);
2408 assert!(pacing.due(Duration::from_secs(5), Duration::from_millis(3), Some(1)));
2409 }
2410
2411 #[test]
2412 fn small_tail_defers_until_the_deadline() {
2413 let pacing = pacing(100, 4, 1_000, 60_000);
2414 assert!(!pacing.due(Duration::from_secs(30), Duration::ZERO, Some(999)));
2415 assert!(pacing.due(Duration::from_secs(60), Duration::ZERO, Some(999)));
2416 }
2417
2418 #[test]
2419 fn tail_at_threshold_publishes_at_base_pacing() {
2420 let pacing = pacing(100, 4, 1_000, 60_000);
2421 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, Some(1_000)));
2422 }
2423
2424 #[test]
2425 fn unknown_tail_publishes_at_base_pacing() {
2426 let pacing = pacing(100, 4, 1_000, 60_000);
2427 assert!(pacing.due(Duration::from_millis(100), Duration::ZERO, None));
2428 }
2429
2430 #[test]
2431 fn deadline_never_overrides_the_backoff_floor() {
2432 let pacing = pacing(100, 4, 1_000, 200);
2433 let last = Duration::from_millis(300);
2434 assert!(!pacing.due(Duration::from_millis(400), last, Some(1)));
2435 assert!(pacing.due(Duration::from_millis(1_200), last, Some(1)));
2436 }
2437}