Skip to main content

haematite_cli/serve/
storage.rs

1//! Durable collections and documents over the engine's branch machinery.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5use std::sync::Arc;
6use std::time::Duration;
7
8use haematite::branch::history::SERVICE_BRANCH as SERVICE;
9use haematite::branch::{
10    KeptLimit, Kind as BranchKind, RefRecord as BranchRefRecord, RefStore as BranchRefStore,
11    Registry as BranchRegistry, ShardRef as BranchShardRef, create_branch_with_kind,
12    current_timestamp, fork_from_with_kind, open as open_branch,
13};
14use haematite::db::vacuum::CANONICAL_REFS_DIR;
15use haematite::db::{
16    Database, DatabaseConfig, DatabaseError, ReadOnlyDatabase, branches::BranchDatabase,
17};
18use haematite::store::NodeCacheBudget;
19use haematite::{Retention, RuntimeOptions};
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22
23use super::protocol::{Selection, Version};
24
25mod admission;
26pub use admission::Admission;
27
28pub mod attribution;
29pub mod embed_inherit;
30pub mod embed_job;
31pub mod embed_pending;
32pub mod embed_prune;
33pub mod embed_rewalk;
34pub mod embed_search;
35pub mod embedded;
36
37#[cfg(test)]
38#[path = "storage/envelope_tests.rs"]
39mod envelope_tests;
40#[cfg(test)]
41#[path = "storage/retention_tests.rs"]
42mod retention_tests;
43#[cfg(test)]
44#[path = "storage/span_measure_tests.rs"]
45mod span_measure_tests;
46#[cfg(test)]
47#[path = "storage/write_cost_tests.rs"]
48mod write_cost_tests;
49
50#[cfg(test)]
51#[path = "storage/batch_test_support.rs"]
52mod batch_test_support;
53#[cfg(test)]
54#[path = "storage/documents_delete_many_tests.rs"]
55mod documents_delete_many_tests;
56#[cfg(test)]
57#[path = "storage/edges_batch_cost_tests.rs"]
58mod edges_batch_cost_tests;
59#[cfg(test)]
60#[path = "storage/vectors_many_tests.rs"]
61mod vectors_many_tests;
62
63mod branches;
64mod catalogue;
65mod discards;
66mod document_delete_many;
67pub use discards::DiscardPasses;
68pub(super) mod edges;
69pub(super) mod field_index;
70pub(super) mod graph;
71mod history;
72pub use history::{KEPT_VERSIONS_DEFAULT, KEPT_VERSIONS_MOST};
73mod counts;
74mod dead;
75mod merge_holders;
76mod merge_journal;
77mod merges;
78mod mutations;
79mod retention;
80pub use retention::AppendPruneLimits;
81pub(in crate::serve) use retention::append_prune_address;
82pub mod reads;
83pub(super) mod relations;
84mod release;
85pub(super) mod review;
86mod vector_import;
87pub(super) mod vectors;
88pub use branches::ForkProvenance;
89#[cfg(test)]
90pub use catalogue::after_held;
91pub use catalogue::{Catalogue, Held};
92pub use merge_holders::{MergePin, MergePlan, MergeState};
93pub use merge_journal::{
94    CandidateAction, CandidateAdvance, MergeChoice, MergeChoiceRow, MergeChoices, MergeChoose,
95    MergeCoverage, MergeOwner, MissingChoice, RecordedChoice,
96};
97pub use release::{
98    Answer as ReleaseAnswer, HeldBy, IntentListing, Keep, Rules as ReleaseRules,
99    Source as ReleaseSource, UndatedGeneration,
100};
101#[cfg(test)]
102pub use release::{Crash, crash_at};
103
104const BRANCH_PREFIX: &str = haematite::branch::history::DATA_BRANCH_PREFIX;
105
106mod failure;
107pub use failure::Failure;
108#[cfg(test)]
109#[path = "storage/failure_tests.rs"]
110mod failure_tests;
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(deny_unknown_fields)]
114pub struct Receipt {
115    pub request_id: String,
116    pub fingerprint: String,
117    pub branch: String,
118    pub version: Version,
119    #[serde(default, skip_serializing_if = "Option::is_none")]
120    pub retention: Option<super::protocol::RetentionResult>,
121}
122
123#[derive(Debug, Serialize, Deserialize)]
124#[serde(deny_unknown_fields)]
125pub struct Collection {
126    pub name: String,
127    pub schema: Option<Value>,
128    pub changed_in: String,
129    pub changed_at: Version,
130}
131
132#[derive(Debug, Serialize, Deserialize)]
133#[serde(deny_unknown_fields)]
134pub struct DocumentRevision {
135    pub collection_branch: String,
136    pub collection_version: Version,
137    pub written_in: String,
138    pub written_at: Version,
139}
140
141/// A captured read selection contains every physical shard root.
142#[derive(Debug, Clone)]
143pub struct Captured {
144    pub branch: String,
145    pub version: Version,
146    pub shards: Vec<BranchShardRef>,
147}
148
149/// Mutation admission is serialized by the caller. Read-only work uses a
150/// captured selection and independent reader after releasing that admission.
151pub struct Storage {
152    refs: BranchRefStore,
153    visible: Catalogue,
154    registry: BranchRegistry,
155    reader: Arc<ReadOnlyDatabase>,
156    writer: BranchDatabase,
157    pending_discards: BTreeSet<String>,
158    confirmed_candidates: BTreeMap<String, Version>,
159    /// STOR-003 `MD5a`: the kept window's limit and the rows known in it.
160    kept_rows: history::KeptRows,
161    /// What came of the last discard's ask for a reclaim pass.
162    discard_passes: DiscardPasses,
163}
164
165pub(super) const fn runtime_options(
166    long_wait_interval: Duration,
167) -> Result<RuntimeOptions, DatabaseError> {
168    if long_wait_interval.is_zero() {
169        return Err(DatabaseError::InvalidLongWaitInterval {
170            given: long_wait_interval,
171        });
172    }
173    Ok(RuntimeOptions {
174        long_wait_interval,
175        reclaim_start: haematite::ReclaimStart::AtOpen,
176    })
177}
178
179impl Storage {
180    /// Create only in a previously absent directory. Never adopt application data.
181    ///
182    /// # Errors
183    ///
184    /// Refuses unsupported writer policies before branch references are created.
185    /// Propagates any error from `try_exists`, `open` or `create_branch_with_kind`.
186    pub fn create(
187        config: DatabaseConfig,
188        branch_cache: NodeCacheBudget,
189        long_wait_interval: Duration,
190    ) -> Result<Self, Failure> {
191        Self::create_kept(
192            config,
193            branch_cache,
194            history::default_kept()?,
195            long_wait_interval,
196        )
197    }
198
199    /// [`Self::create`], each branch record keeping `kept` previous versions
200    /// (STOR-003 `MD5a`).
201    ///
202    /// # Errors
203    ///
204    /// As [`Self::create`].
205    pub fn create_kept(
206        config: DatabaseConfig,
207        branch_cache: NodeCacheBudget,
208        kept: KeptLimit,
209        long_wait_interval: Duration,
210    ) -> Result<Self, Failure> {
211        let options = runtime_options(long_wait_interval).map_err(Failure::from)?;
212        if config.data_dir.try_exists().map_err(Failure::storage)? {
213            return Err(Failure::new(
214                "directory_exists",
215                "create requires a new directory",
216            ));
217        }
218        let database = Database::create(config, options).map_err(Failure::from)?;
219        database.require_path_writer().map_err(Failure::from)?;
220        let mut writer = BranchDatabase::new(database, branch_cache).map_err(Failure::database)?;
221        let mut refs = BranchRefStore::open_writing(
222            writer.data_dir().join(CANONICAL_REFS_DIR),
223            writer.writer_lock(),
224        )
225        .map_err(Failure::branch_ref)?;
226        refs.keep_versions(kept);
227        let registry = BranchRegistry::new();
228        create_branch_with_kind(
229            SERVICE,
230            writer.base_roots().map_err(Failure::database)?,
231            BranchKind::Namespace,
232            &mut refs,
233            &registry,
234            current_timestamp(),
235        )
236        .map_err(Failure::branch_commit)?;
237        counts::initialize_new_store(&mut writer, &mut refs, &registry)?;
238        Self::finish_open(writer, refs, registry, kept)
239    }
240
241    /// Reopen a served directory under its existing exclusive writer lock.
242    ///
243    /// # Errors
244    ///
245    /// Refuses unsupported writer policies before engine startup.
246    /// Propagates any error from `open`.
247    pub fn open(
248        path: &Path,
249        branch_cache: NodeCacheBudget,
250        long_wait_interval: Duration,
251    ) -> Result<Self, Failure> {
252        Self::open_kept(
253            path,
254            branch_cache,
255            history::default_kept()?,
256            long_wait_interval,
257        )
258    }
259
260    /// [`Self::open`], each branch record keeping `kept` previous versions
261    /// (STOR-003 `MD5a`).
262    ///
263    /// # Errors
264    ///
265    /// As [`Self::open`].
266    pub fn open_kept(
267        path: &Path,
268        branch_cache: NodeCacheBudget,
269        kept: KeptLimit,
270        long_wait_interval: Duration,
271    ) -> Result<Self, Failure> {
272        Self::open_kept_starting(
273            path,
274            branch_cache,
275            kept,
276            long_wait_interval,
277            haematite::ReclaimStart::AtOpen,
278        )
279    }
280
281    /// [`Self::open`] with reclaim held for the life of the handle, for a test
282    /// that counts what the open itself reads: a pass started at open reads
283    /// on the shards' threads while the open goes on, so its reads would land
284    /// in a process-wide count only when they happen to fall inside it.
285    ///
286    /// # Errors
287    ///
288    /// As [`Self::open`].
289    #[doc(hidden)]
290    pub fn open_reclaim_held(
291        path: &Path,
292        branch_cache: NodeCacheBudget,
293        long_wait_interval: Duration,
294    ) -> Result<Self, Failure> {
295        Self::open_kept_starting(
296            path,
297            branch_cache,
298            history::default_kept()?,
299            long_wait_interval,
300            haematite::ReclaimStart::Held,
301        )
302    }
303
304    fn open_kept_starting(
305        path: &Path,
306        branch_cache: NodeCacheBudget,
307        kept: KeptLimit,
308        long_wait_interval: Duration,
309        reclaim_start: haematite::ReclaimStart,
310    ) -> Result<Self, Failure> {
311        let options = RuntimeOptions {
312            reclaim_start,
313            ..runtime_options(long_wait_interval).map_err(Failure::from)?
314        };
315        if !path.join(CANONICAL_REFS_DIR).is_dir() {
316            return Err(Failure::new(
317                "not_served",
318                "directory has no served branch references",
319            ));
320        }
321        // The service's own record, read from disk with no ref store opened
322        // and nothing attached: only a store the service made is moved below.
323        match BranchRefStore::read_record(path.join(CANONICAL_REFS_DIR), SERVICE) {
324            Ok(Some(_record)) => {}
325            Ok(None) => {
326                return Err(Failure::new(
327                    "not_served",
328                    "directory is not a haem/1 service database",
329                ));
330            }
331            Err(error) => {
332                return Err(Failure::new(
333                    "service_record_unreadable",
334                    format!("the service's branch record cannot be read: {error}"),
335                ));
336            }
337        }
338        let database = Database::open_path_writer(path, options).map_err(Failure::from)?;
339        let database = if database.reclaim_mark_bytes().is_some() {
340            database
341        } else {
342            // Moved once, from KeepAll to reclaiming under the default bound
343            // (STOR-003 H1a): the store's config records it, so every later
344            // open finds Reclaim and moves nothing.
345            drop(database);
346            let moved = Database::open_with_retention(path, options, Retention::reclaim(0))
347                .map_err(Failure::from)?;
348            moved.require_path_writer().map_err(Failure::from)?;
349            moved
350        };
351        let writer = BranchDatabase::new(database, branch_cache).map_err(Failure::database)?;
352        let mut refs =
353            BranchRefStore::open_writing(path.join(CANONICAL_REFS_DIR), writer.writer_lock())
354                .map_err(Failure::branch_ref)?;
355        refs.keep_versions(kept);
356        if refs.get(SERVICE).is_none() {
357            return Err(Failure::new(
358                "not_served",
359                "directory is not a haem/1 service database",
360            ));
361        }
362        Self::finish_open(writer, refs, BranchRegistry::new(), kept)
363    }
364
365    fn finish_open(
366        writer: BranchDatabase,
367        mut refs: BranchRefStore,
368        registry: BranchRegistry,
369        kept: KeptLimit,
370    ) -> Result<Self, Failure> {
371        if refs.get(SERVICE).is_none() {
372            return Err(Failure::new("not_served", "missing service namespace"));
373        }
374        let reader =
375            Arc::new(ReadOnlyDatabase::open(writer.data_dir()).map_err(Failure::observer)?);
376        history::require_file_rows(&reader, &refs)?;
377        discards::require_file_rows(&reader, &refs)?;
378        let main = branch_name("main");
379        if refs.get(&main).is_none() {
380            let parent = open_branch(SERVICE, &refs, &registry).map_err(Failure::branch_commit)?;
381            fork_from_with_kind(
382                &parent,
383                &main,
384                BranchKind::Work,
385                &mut refs,
386                &registry,
387                current_timestamp(),
388            )
389            .map_err(Failure::branch_commit)?;
390        }
391        // Recovered bytes are not, by themselves, an acknowledged fence.
392        // The fence changes no record, and this open owns the store, so the
393        // listing taken after it is the listing it fenced.
394        refs.confirm_directory().map_err(Failure::branch_ref)?;
395        let visible: BTreeMap<_, _> = refs
396            .list()
397            .map(|record| {
398                let kept = refs.kept(&record.name).to_vec();
399                (record.name.clone(), (record.clone(), kept))
400            })
401            .collect();
402        // The holder of `writer`, the database this storage keeps: after a
403        // move at open it is the reopened one's, never the first's (H1b).
404        let holder = writer.read_holder();
405        let mut storage = Self {
406            refs,
407            visible: Catalogue::new(visible, Arc::clone(&reader), holder),
408            registry,
409            reader,
410            writer,
411            pending_discards: BTreeSet::new(),
412            confirmed_candidates: BTreeMap::new(),
413            kept_rows: history::KeptRows::new(kept),
414            discard_passes: DiscardPasses::default(),
415        };
416        storage.reconcile_discards()?;
417        Ok(storage)
418    }
419
420    /// # Errors
421    ///
422    /// Returns `Failure` if `capture` fails.
423    pub fn capture(&self, selection: &Selection) -> Result<Captured, Failure> {
424        self.visible.capture(selection)
425    }
426
427    pub(in crate::serve) fn activate_reclaim(&self) -> Result<(), Failure> {
428        self.writer.activate_reclaim().map_err(Failure::from)
429    }
430
431    /// Run each shard's reclaim to rest (STOR-003 H1c).
432    ///
433    /// # Errors
434    /// Propagates a failure while draining shard reclaim work.
435    #[cfg(test)]
436    pub fn quiesce_reclaim(&self) -> Result<(), Failure> {
437        self.writer.quiesce_reclaim().map_err(Failure::from)
438    }
439
440    /// Each shard's reclaim, as the database answers it (SL2: a test's
441    /// diagnosis).
442    #[cfg(test)]
443    pub fn reclaim_status(&self) -> haematite::db::ReclaimStatus {
444        self.writer.reclaim_status()
445    }
446
447    /// How many read holds live on this storage's database (STOR-003 H1c).
448    #[cfg(test)]
449    pub fn live_read_holds(&self) -> usize {
450        self.writer.read_holder().status().live
451    }
452
453    pub fn catalogue(&self) -> Catalogue {
454        self.visible.clone()
455    }
456
457    /// What came of the last discard's ask for a reclaim pass, shared with
458    /// whoever answers `store.stats`.
459    #[must_use]
460    pub fn discard_passes(&self) -> DiscardPasses {
461        self.discard_passes.clone()
462    }
463
464    /// A separate immutable reader can perform captured reads concurrently.
465    ///
466    /// # Errors
467    ///
468    /// Returns `Failure` if `open` or `data_dir` fails.
469    pub fn reader(&self) -> Result<ReadOnlyDatabase, Failure> {
470        ReadOnlyDatabase::open(self.writer.data_dir()).map_err(Failure::observer)
471    }
472
473    fn retain_current(&mut self, branch: &str, record: &BranchRefRecord) -> Result<(), Failure> {
474        self.retain_history_row(branch, record)
475    }
476
477    fn publish_record(&self, name: &str) -> Result<(), Failure> {
478        let record = self
479            .refs
480            .get(name)
481            .ok_or_else(|| Failure::new("reference_missing", "committed reference disappeared"))?;
482        self.visible
483            .publish(record.clone(), self.refs.kept(name).to_vec())
484    }
485}
486
487const fn version(record: &BranchRefRecord) -> Version {
488    Version {
489        generation: record.created,
490        sequence: record.seq,
491    }
492}
493
494fn branch_name(name: &str) -> String {
495    format!("{BRANCH_PREFIX}{name}")
496}
497
498fn identifier(value: &str) -> Result<(), Failure> {
499    if value.is_empty() || value.len() > 512 || value.contains('\0') {
500        return Err(Failure::new(
501            "invalid_identifier",
502            "names must contain 1 to 512 UTF-8 bytes and no NUL",
503        ));
504    }
505    Ok(())
506}
507
508fn collection_key(name: &str) -> Vec<u8> {
509    format!("c\0{name}").into_bytes()
510}
511pub(in crate::serve) fn document_key(collection: &str, id: &str) -> Vec<u8> {
512    format!("d\0{collection}\0{id}").into_bytes()
513}
514
515pub(in crate::serve) fn revision_key(collection: &str, id: &str) -> Vec<u8> {
516    format!("m\0{collection}\0{id}").into_bytes()
517}
518fn receipt_key(branch: &str, generation: u64, id: &str) -> Vec<u8> {
519    format!("r\0{branch}\0{generation}\0{id}").into_bytes()
520}
521
522#[cfg(test)]
523mod long_wait_tests {
524    use std::error::Error;
525    use std::time::Duration;
526
527    use haematite::DatabaseError;
528    use haematite::db::DatabaseConfig;
529    use haematite::store::NodeCacheBudget;
530
531    use super::{Storage, runtime_options};
532
533    #[test]
534    fn zero_is_the_typed_engine_refusal() {
535        assert!(matches!(
536            runtime_options(Duration::ZERO),
537            Err(DatabaseError::InvalidLongWaitInterval { given }) if given == Duration::ZERO
538        ));
539    }
540
541    #[test]
542    fn zero_refuses_create_and_open_before_directory_checks() -> Result<(), Box<dyn Error>> {
543        let directory = tempfile::tempdir()?;
544        let path = directory.path().join("absent");
545        let config = DatabaseConfig {
546            data_dir: path.clone(),
547            shard_count: 1,
548            distributed: None,
549            executor_threads: Some(1),
550            node_cache_budget: Some(NodeCacheBudget::Unlimited),
551            retention: haematite::Retention::reclaim(0),
552        };
553        for refused in [
554            Storage::create(config, NodeCacheBudget::Unlimited, Duration::ZERO),
555            Storage::open(&path, NodeCacheBudget::Unlimited, Duration::ZERO),
556        ] {
557            let failure = refused.err().ok_or("zero interval was accepted")?;
558            assert_eq!(failure.code, "storage_unavailable");
559            assert_eq!(
560                failure.message,
561                DatabaseError::InvalidLongWaitInterval {
562                    given: Duration::ZERO
563                }
564                .to_string()
565            );
566            assert!(!path.try_exists()?);
567        }
568        Ok(())
569    }
570}