Skip to main content

prolly/prolly/
snapshot.rs

1//! Snapshot and branch-style helpers built on named roots.
2//!
3//! Named roots are intentionally byte-oriented and low level. This module adds a
4//! small convention layer for common embedded-library workflows: branch heads,
5//! tags, checkpoints, and custom root namespaces. The helpers do not add new
6//! storage semantics; they produce deterministic root names and delegate all
7//! durability and compare-and-swap behavior to the manifest store.
8
9use super::error::Error;
10use super::manifest::{
11    ManifestStore, ManifestStoreScan, NamedRootSelection, NamedRootUpdate, RootManifest,
12};
13use super::store::Store;
14use super::tree::Tree;
15use super::Prolly;
16
17/// Root-name prefix used for branch snapshots.
18pub const SNAPSHOT_BRANCH_PREFIX: &[u8] = b"refs/heads/";
19/// Root-name prefix used for immutable release/tag snapshots.
20pub const SNAPSHOT_TAG_PREFIX: &[u8] = b"refs/tags/";
21/// Root-name prefix used for checkpoint snapshots.
22pub const SNAPSHOT_CHECKPOINT_PREFIX: &[u8] = b"refs/checkpoints/";
23
24/// Namespace used to derive stable named-root keys for snapshots.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum SnapshotNamespace {
27    /// Mutable branch-style snapshots, stored under `refs/heads/`.
28    Branch,
29    /// Tag-style snapshots, stored under `refs/tags/`.
30    Tag,
31    /// Checkpoint snapshots, stored under `refs/checkpoints/`.
32    Checkpoint,
33    /// Application-defined root namespace.
34    Custom(Vec<u8>),
35}
36
37impl SnapshotNamespace {
38    /// Branch snapshot namespace.
39    pub fn branch() -> Self {
40        Self::Branch
41    }
42
43    /// Tag snapshot namespace.
44    pub fn tag() -> Self {
45        Self::Tag
46    }
47
48    /// Checkpoint snapshot namespace.
49    pub fn checkpoint() -> Self {
50        Self::Checkpoint
51    }
52
53    /// Custom snapshot namespace.
54    pub fn custom(prefix: impl Into<Vec<u8>>) -> Self {
55        Self::Custom(prefix.into())
56    }
57
58    /// Return the raw named-root prefix for this namespace.
59    pub fn prefix(&self) -> &[u8] {
60        match self {
61            Self::Branch => SNAPSHOT_BRANCH_PREFIX,
62            Self::Tag => SNAPSHOT_TAG_PREFIX,
63            Self::Checkpoint => SNAPSHOT_CHECKPOINT_PREFIX,
64            Self::Custom(prefix) => prefix,
65        }
66    }
67
68    /// Build the durable named-root key for `id` in this namespace.
69    pub fn root_name(&self, id: impl AsRef<[u8]>) -> Vec<u8> {
70        snapshot_root_name(self, id)
71    }
72
73    /// Return the snapshot id if `name` belongs to this namespace.
74    pub fn id_from_name(&self, name: impl AsRef<[u8]>) -> Option<Vec<u8>> {
75        snapshot_id_from_name(self, name)
76    }
77}
78
79/// Build the durable named-root key for `id` in `namespace`.
80pub fn snapshot_root_name(namespace: &SnapshotNamespace, id: impl AsRef<[u8]>) -> Vec<u8> {
81    let prefix = namespace.prefix();
82    let id = id.as_ref();
83    let mut name = Vec::with_capacity(prefix.len() + id.len());
84    name.extend_from_slice(prefix);
85    name.extend_from_slice(id);
86    name
87}
88
89/// Return the snapshot id if `name` belongs to `namespace`.
90pub fn snapshot_id_from_name(
91    namespace: &SnapshotNamespace,
92    name: impl AsRef<[u8]>,
93) -> Option<Vec<u8>> {
94    let prefix = namespace.prefix();
95    let name = name.as_ref();
96    name.strip_prefix(prefix).map(<[u8]>::to_vec)
97}
98
99/// A snapshot root with namespace-local id and manifest metadata.
100#[derive(Clone, Debug, PartialEq)]
101pub struct SnapshotRoot {
102    /// Namespace-local snapshot id.
103    pub id: Vec<u8>,
104    /// Full durable named-root key.
105    pub name: Vec<u8>,
106    /// Tree stored under this snapshot.
107    pub tree: Tree,
108    /// Optional creation timestamp from the root manifest.
109    pub created_at_millis: Option<u64>,
110    /// Optional update timestamp from the root manifest.
111    pub updated_at_millis: Option<u64>,
112}
113
114impl SnapshotRoot {
115    fn from_manifest(
116        namespace: &SnapshotNamespace,
117        name: Vec<u8>,
118        manifest: RootManifest,
119    ) -> Option<Self> {
120        let id = snapshot_id_from_name(namespace, &name)?;
121        let created_at_millis = manifest.created_at_millis;
122        let updated_at_millis = manifest.updated_at_millis;
123        Some(Self {
124            id,
125            name,
126            tree: manifest.into_tree(),
127            created_at_millis,
128            updated_at_millis,
129        })
130    }
131}
132
133/// Result of loading several snapshots by namespace-local id.
134#[derive(Clone, Debug, Default, PartialEq)]
135pub struct SnapshotSelection {
136    /// Snapshots that were present.
137    pub snapshots: Vec<SnapshotRoot>,
138    /// Namespace-local ids that were requested but absent.
139    pub missing_ids: Vec<Vec<u8>>,
140}
141
142/// Convenience API for branch, tag, checkpoint, or custom snapshot roots.
143pub struct SnapshotManager<'a, S: Store> {
144    prolly: &'a Prolly<S>,
145    namespace: SnapshotNamespace,
146}
147
148impl<'a, S: Store> SnapshotManager<'a, S> {
149    /// Create a manager for `namespace` using an existing [`Prolly`] manager.
150    pub fn new(prolly: &'a Prolly<S>, namespace: SnapshotNamespace) -> Self {
151        Self { prolly, namespace }
152    }
153
154    /// Return the namespace used by this manager.
155    pub fn namespace(&self) -> &SnapshotNamespace {
156        &self.namespace
157    }
158
159    /// Build a durable named-root key from a namespace-local id.
160    pub fn root_name(&self, id: impl AsRef<[u8]>) -> Vec<u8> {
161        self.namespace.root_name(id)
162    }
163
164    /// Load one snapshot by namespace-local id.
165    pub fn load(&self, id: impl AsRef<[u8]>) -> Result<Option<Tree>, Error>
166    where
167        S: ManifestStore,
168    {
169        self.prolly.load_named_root(&self.root_name(id))
170    }
171
172    /// Load several snapshots by namespace-local id.
173    pub fn load_many<I, Id>(&self, ids: I) -> Result<SnapshotSelection, Error>
174    where
175        S: ManifestStore,
176        I: IntoIterator<Item = Id>,
177        Id: AsRef<[u8]>,
178    {
179        let ids = ids
180            .into_iter()
181            .map(|id| id.as_ref().to_vec())
182            .collect::<Vec<_>>();
183        let names = ids.iter().map(|id| self.root_name(id)).collect::<Vec<_>>();
184        let selection = self.prolly.load_named_roots(names)?;
185        Ok(self.selection_from_named_roots(selection))
186    }
187
188    /// Publish or replace a snapshot.
189    pub fn publish(&self, id: impl AsRef<[u8]>, tree: &Tree) -> Result<(), Error>
190    where
191        S: ManifestStore,
192    {
193        self.prolly.publish_named_root(&self.root_name(id), tree)
194    }
195
196    /// Publish or replace a snapshot with an explicit timestamp.
197    pub fn publish_at_millis(
198        &self,
199        id: impl AsRef<[u8]>,
200        tree: &Tree,
201        timestamp_millis: u64,
202    ) -> Result<(), Error>
203    where
204        S: ManifestStore,
205    {
206        self.prolly
207            .publish_named_root_at_millis(&self.root_name(id), tree, timestamp_millis)
208    }
209
210    /// Delete a snapshot. Deleting a missing snapshot is not an error.
211    pub fn delete(&self, id: impl AsRef<[u8]>) -> Result<(), Error>
212    where
213        S: ManifestStore,
214    {
215        self.prolly.delete_named_root(&self.root_name(id))
216    }
217
218    /// Atomically update a snapshot when the current tree matches `expected`.
219    pub fn compare_and_swap(
220        &self,
221        id: impl AsRef<[u8]>,
222        expected: Option<&Tree>,
223        replacement: Option<&Tree>,
224    ) -> Result<NamedRootUpdate, Error>
225    where
226        S: ManifestStore,
227    {
228        self.prolly
229            .compare_and_swap_named_root(&self.root_name(id), expected, replacement)
230    }
231
232    /// Atomically update a snapshot with an explicit timestamp.
233    pub fn compare_and_swap_at_millis(
234        &self,
235        id: impl AsRef<[u8]>,
236        expected: Option<&Tree>,
237        replacement: Option<&Tree>,
238        timestamp_millis: u64,
239    ) -> Result<NamedRootUpdate, Error>
240    where
241        S: ManifestStore,
242    {
243        self.prolly.compare_and_swap_named_root_at_millis(
244            &self.root_name(id),
245            expected,
246            replacement,
247            timestamp_millis,
248        )
249    }
250
251    /// List all snapshots in this namespace.
252    pub fn list(&self) -> Result<Vec<SnapshotRoot>, Error>
253    where
254        S: ManifestStoreScan,
255    {
256        let mut snapshots = self
257            .prolly
258            .list_named_root_manifests()?
259            .into_iter()
260            .filter_map(|root| {
261                SnapshotRoot::from_manifest(&self.namespace, root.name, root.manifest)
262            })
263            .collect::<Vec<_>>();
264        snapshots.sort_by(|left, right| left.name.cmp(&right.name));
265        Ok(snapshots)
266    }
267
268    fn selection_from_named_roots(&self, selection: NamedRootSelection) -> SnapshotSelection {
269        let snapshots = selection
270            .roots
271            .into_iter()
272            .filter_map(|root| {
273                let id = self.namespace.id_from_name(&root.name)?;
274                Some(SnapshotRoot {
275                    id,
276                    name: root.name,
277                    tree: root.tree,
278                    created_at_millis: None,
279                    updated_at_millis: None,
280                })
281            })
282            .collect::<Vec<_>>();
283        let missing_ids = selection
284            .missing_names
285            .into_iter()
286            .filter_map(|name| self.namespace.id_from_name(name))
287            .collect::<Vec<_>>();
288        SnapshotSelection {
289            snapshots,
290            missing_ids,
291        }
292    }
293}
294
295impl<S: Store> Prolly<S> {
296    /// Return a snapshot manager for the provided namespace.
297    pub fn snapshots(&self, namespace: SnapshotNamespace) -> SnapshotManager<'_, S> {
298        SnapshotManager::new(self, namespace)
299    }
300
301    /// Return a branch snapshot manager using `refs/heads/` names.
302    pub fn branch_snapshots(&self) -> SnapshotManager<'_, S> {
303        self.snapshots(SnapshotNamespace::Branch)
304    }
305
306    /// Return a tag snapshot manager using `refs/tags/` names.
307    pub fn tag_snapshots(&self) -> SnapshotManager<'_, S> {
308        self.snapshots(SnapshotNamespace::Tag)
309    }
310
311    /// Return a checkpoint snapshot manager using `refs/checkpoints/` names.
312    pub fn checkpoint_snapshots(&self) -> SnapshotManager<'_, S> {
313        self.snapshots(SnapshotNamespace::Checkpoint)
314    }
315}