zerodds_durability_store/store.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! The [`DurabilityStore`] trait — the storage abstraction every cold adapter
5//! implements (ADR 0009, layer ②b).
6
7use alloc::vec::Vec;
8use std::time::SystemTime;
9
10use crate::contract::Contract;
11use crate::error::Result;
12use crate::model::{DurabilitySample, Page, Selector, StoreStats};
13use crate::topic_meta::TopicMeta;
14
15/// Default page size for the [`DurabilityStore::replay_for_topic`] convenience
16/// drain when a selector does not set its own `limit`.
17pub const DEFAULT_PAGE: usize = 1024;
18
19/// A durable, contract-bounded sample store. Implementations are the cold
20/// adapters (sqlite / file / lakehouse / in-memory). All methods are
21/// `&self` + `Send + Sync`: the daemon shares one store across threads.
22///
23/// Retention is bounded ONLY by the topic's `Contract` (ADR 0009 inv. 1):
24/// implementations enforce it on [`store`](DurabilityStore::store) (drop-oldest
25/// under `KEEP_LAST`, [`StoreError::OutOfResources`](crate::StoreError) under
26/// `KEEP_ALL` caps).
27pub trait DurabilityStore: Send + Sync {
28 /// Registers (or replaces) the retention [`Contract`] for `topic`. The
29 /// daemon calls this when it starts serving a topic. Topics without an
30 /// explicit contract use the store's default ([`Contract::default`]).
31 ///
32 /// Contract-only: it does not persist a type name or keyedness, so a
33 /// topic that only ever saw `set_contract` has no [`TopicMeta`]
34 /// ([`topic_meta`](Self::topic_meta) returns `None` for it) — use
35 /// [`set_topic_meta`](Self::set_topic_meta) when serving a topic needs to
36 /// survive a restart without a writer/reader being re-discovered first.
37 /// When both were ever called for the same topic, `set_topic_meta`'s
38 /// contract wins (it is the more recent, more complete registration).
39 ///
40 /// # Errors
41 /// Backend/I/O failure.
42 fn set_contract(&self, topic: &str, contract: Contract) -> Result<()>;
43
44 /// Persists what the durability service (or any other caller) needs to
45 /// resume serving `topic` after a restart, with no writer or reader
46 /// re-discovered first: its type name, keyedness, and the retention
47 /// contract in force (see [`TopicMeta`]). Called once, when a topic is
48 /// first served. Also becomes the contract
49 /// [`store`](Self::store)/[`cleanup`](Self::cleanup) enforce for `topic`
50 /// (superseding a plain [`set_contract`](Self::set_contract) for the same
51 /// topic — see its doc).
52 ///
53 /// # Errors
54 /// Backend/I/O failure.
55 fn set_topic_meta(&self, topic: &str, meta: TopicMeta) -> Result<()>;
56
57 /// The persisted [`TopicMeta`] for `topic`, if
58 /// [`set_topic_meta`](Self::set_topic_meta) was ever called for it.
59 /// `None` for a topic that only ever saw
60 /// [`set_contract`](Self::set_contract) (contract-only, no served
61 /// type/keyedness known) or was never registered at all.
62 ///
63 /// # Errors
64 /// Backend/I/O failure.
65 fn topic_meta(&self, topic: &str) -> Result<Option<TopicMeta>>;
66
67 /// Persists `sample` durably, enforcing the topic contract. Returns
68 /// [`StoreError::OutOfResources`](crate::StoreError) when a `KEEP_ALL` cap
69 /// is hit.
70 ///
71 /// # Errors
72 /// Contract cap exceeded, or a backend/I/O failure.
73 fn store(&self, sample: DurabilitySample) -> Result<()>;
74
75 /// Returns one ordered [`Page`] of samples for `topic` matching `selector`.
76 /// Ordering is `(instance_key, sequence)`. Drive pagination via
77 /// [`Page::next`] + [`Selector::after_cursor`].
78 ///
79 /// # Errors
80 /// Backend/I/O failure.
81 fn query(&self, topic: &str, selector: &Selector) -> Result<Page>;
82
83 /// Marks an instance as unregistered at `now`; its samples become eligible
84 /// for purge after `now + cleanup_delay` (applied by [`cleanup`]).
85 ///
86 /// [`cleanup`]: DurabilityStore::cleanup
87 ///
88 /// # Errors
89 /// Backend/I/O failure.
90 fn unregister(&self, topic: &str, instance_key: &[u8; 16], now: SystemTime) -> Result<()>;
91
92 /// Purges instances whose `unregister` time + cleanup-delay is `<= now`.
93 /// Returns the number of purged instances.
94 ///
95 /// # Errors
96 /// Backend/I/O failure.
97 fn cleanup(&self, now: SystemTime) -> Result<usize>;
98
99 /// Aggregate counters for `topic`.
100 ///
101 /// # Errors
102 /// Backend/I/O failure.
103 fn stats(&self, topic: &str) -> Result<StoreStats>;
104
105 /// Convenience: drains the full contracted history for `topic` by paging
106 /// through [`query`](DurabilityStore::query). Prefer paged `query` for
107 /// large contracts; this materializes everything into a `Vec`.
108 ///
109 /// # Errors
110 /// Backend/I/O failure from the underlying `query`.
111 fn replay_for_topic(&self, topic: &str) -> Result<Vec<DurabilitySample>> {
112 let mut out = Vec::new();
113 let mut sel = Selector {
114 limit: Some(DEFAULT_PAGE),
115 ..Selector::default()
116 };
117 loop {
118 let page = self.query(topic, &sel)?;
119 let got = page.samples.len();
120 out.extend(page.samples);
121 match page.next {
122 Some(cursor) if got > 0 => sel = sel.after_cursor(cursor),
123 _ => break,
124 }
125 }
126 Ok(out)
127 }
128}