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