uni_store/snapshot/
manager.rs1use crate::store_utils::{
5 DEFAULT_TIMEOUT, get_with_timeout, is_not_found, list_with_timeout, put_with_timeout,
6};
7use anyhow::Result;
8use bytes::Bytes;
9use chrono::{DateTime, Utc};
10use object_store::ObjectStore;
11use object_store::path::Path as ObjectStorePath;
12use std::collections::HashMap;
13use std::sync::Arc;
14use tracing::instrument;
15use uni_common::core::fork::ForkId;
16use uni_common::core::snapshot::SnapshotManifest;
17
18pub struct SnapshotManager {
26 store: Arc<dyn ObjectStore>,
27 fork_id: Option<ForkId>,
29}
30
31impl SnapshotManager {
32 pub fn new(store: Arc<dyn ObjectStore>) -> Self {
34 Self {
35 store,
36 fork_id: None,
37 }
38 }
39
40 pub fn new_for_fork(store: Arc<dyn ObjectStore>, fork_id: ForkId) -> Self {
46 Self {
47 store,
48 fork_id: Some(fork_id),
49 }
50 }
51
52 pub fn is_fork_scoped(&self) -> bool {
55 self.fork_id.is_some()
56 }
57
58 fn catalog_prefix(&self) -> String {
61 match &self.fork_id {
62 Some(id) => format!("catalog/forks/{id}"),
63 None => "catalog".to_string(),
64 }
65 }
66
67 fn manifest_path(&self, snapshot_id: &str) -> ObjectStorePath {
68 ObjectStorePath::from(format!(
69 "{}/manifests/{}.json",
70 self.catalog_prefix(),
71 snapshot_id
72 ))
73 }
74
75 fn latest_ptr_path(&self) -> ObjectStorePath {
76 ObjectStorePath::from(format!("{}/latest", self.catalog_prefix()))
77 }
78
79 fn named_snapshots_path(&self) -> ObjectStorePath {
80 ObjectStorePath::from(format!("{}/named_snapshots.json", self.catalog_prefix()))
81 }
82
83 #[instrument(skip(self, manifest), fields(snapshot_id = %manifest.snapshot_id, size_bytes), level = "info")]
84 pub async fn save_snapshot(&self, manifest: &SnapshotManifest) -> Result<()> {
85 let path = self.manifest_path(&manifest.snapshot_id);
86 let json = serde_json::to_string_pretty(manifest)?;
87 tracing::Span::current().record("size_bytes", json.len());
88 put_with_timeout(&self.store, &path, Bytes::from(json), DEFAULT_TIMEOUT).await?;
89 Ok(())
90 }
91
92 #[instrument(skip(self), level = "info")]
93 pub async fn load_snapshot(&self, snapshot_id: &str) -> Result<SnapshotManifest> {
94 match self
96 .load_snapshot_at(&self.manifest_path(snapshot_id))
97 .await
98 {
99 Ok(m) => Ok(m),
100 Err(e) if self.fork_id.is_some() => {
101 let primary_path =
106 ObjectStorePath::from(format!("catalog/manifests/{snapshot_id}.json"));
107 self.load_snapshot_at(&primary_path).await.map_err(|_| e)
108 }
109 Err(e) => Err(e),
110 }
111 }
112
113 async fn load_snapshot_at(&self, path: &ObjectStorePath) -> Result<SnapshotManifest> {
114 let result = get_with_timeout(&self.store, path, DEFAULT_TIMEOUT).await?;
115 let bytes = result.bytes().await?;
116 let content = String::from_utf8(bytes.to_vec())?;
117 let manifest: SnapshotManifest = serde_json::from_str(&content)?;
118 Ok(manifest)
119 }
120
121 pub async fn list_snapshots(&self) -> Result<Vec<String>> {
122 let prefix = ObjectStorePath::from(format!("{}/manifests", self.catalog_prefix()));
123 let metas = list_with_timeout(&self.store, Some(&prefix), DEFAULT_TIMEOUT).await?;
124 let mut ids = Vec::new();
125
126 for meta in metas {
127 if let Some(filename) = meta.location.filename()
128 && filename.ends_with(".json")
129 {
130 ids.push(filename.trim_end_matches(".json").to_string());
131 }
132 }
133 Ok(ids)
134 }
135
136 pub async fn has_any_manifests(&self) -> Result<bool> {
138 let ids = self.list_snapshots().await?;
139 Ok(!ids.is_empty())
140 }
141
142 pub async fn load_latest_snapshot(&self) -> Result<Option<SnapshotManifest>> {
143 let latest_path = self.latest_ptr_path();
144 match get_with_timeout(&self.store, &latest_path, DEFAULT_TIMEOUT).await {
145 Ok(result) => {
146 let bytes = result.bytes().await.map_err(anyhow::Error::from)?;
147 let snapshot_id = String::from_utf8(bytes.to_vec())?;
148 let snapshot_id = snapshot_id.trim();
149 if snapshot_id.is_empty() {
150 return Ok(None);
151 }
152 Ok(Some(self.load_snapshot(snapshot_id).await?))
153 }
154 Err(e) if is_not_found(&e) => Ok(None),
155 Err(e) => Err(e),
156 }
157 }
158
159 #[instrument(skip(self), level = "info")]
160 pub async fn set_latest_snapshot(&self, snapshot_id: &str) -> Result<()> {
161 let path = self.latest_ptr_path();
162 put_with_timeout(
163 &self.store,
164 &path,
165 Bytes::from(snapshot_id.to_string()),
166 DEFAULT_TIMEOUT,
167 )
168 .await?;
169 Ok(())
170 }
171
172 pub async fn load_named_snapshots(&self) -> Result<HashMap<String, String>> {
173 let path = self.named_snapshots_path();
174 match get_with_timeout(&self.store, &path, DEFAULT_TIMEOUT).await {
175 Ok(result) => {
176 let bytes = result.bytes().await?;
177 let content = String::from_utf8(bytes.to_vec())?;
178 Ok(serde_json::from_str(&content)?)
179 }
180 Err(e) if is_not_found(&e) => Ok(HashMap::new()),
186 Err(e) => Err(e),
187 }
188 }
189
190 pub async fn save_named_snapshot(&self, name: &str, snapshot_id: &str) -> Result<()> {
191 let mut map = self.load_named_snapshots().await?;
192 map.insert(name.to_string(), snapshot_id.to_string());
193
194 let json = serde_json::to_string_pretty(&map)?;
195 put_with_timeout(
196 &self.store,
197 &self.named_snapshots_path(),
198 Bytes::from(json),
199 DEFAULT_TIMEOUT,
200 )
201 .await?;
202 Ok(())
203 }
204
205 pub async fn get_named_snapshot(&self, name: &str) -> Result<Option<String>> {
206 let map = self.load_named_snapshots().await?;
207 Ok(map.get(name).cloned())
208 }
209
210 pub async fn find_snapshot_at_time(
212 &self,
213 target: DateTime<Utc>,
214 ) -> Result<Option<SnapshotManifest>> {
215 let ids = self.list_snapshots().await?;
216 let mut best: Option<SnapshotManifest> = None;
217
218 for id in ids {
219 let m = self.load_snapshot(&id).await?;
225 if m.created_at <= target && best.as_ref().is_none_or(|b| m.created_at > b.created_at) {
226 best = Some(m);
227 }
228 }
229 Ok(best)
230 }
231}
232
233pub fn fsync_snapshot_pointer(
249 local_root: Option<&std::path::Path>,
250 fork_id: Option<&ForkId>,
251 snapshot_id: &str,
252) -> std::io::Result<()> {
253 let Some(root) = local_root else {
254 return Ok(());
255 };
256 let prefix = match fork_id {
260 Some(id) => root.join("catalog").join("forks").join(id.to_string()),
261 None => root.join("catalog"),
262 };
263 let manifest = prefix.join("manifests").join(format!("{snapshot_id}.json"));
264 let latest = prefix.join("latest");
265 crate::runtime::wal::sync_file_and_parent(&manifest)?;
266 crate::runtime::wal::sync_file_and_parent(&latest)?;
267 Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
277 fn test_fsync_snapshot_pointer_noop_for_remote() {
278 assert!(fsync_snapshot_pointer(None, None, "snap-1").is_ok());
279 }
280
281 #[test]
284 fn test_fsync_snapshot_pointer_syncs_local_artifacts() {
285 let dir = tempfile::TempDir::new().unwrap();
286 let root = dir.path();
287 let manifests = root.join("catalog").join("manifests");
288 std::fs::create_dir_all(&manifests).unwrap();
289 std::fs::write(manifests.join("snap-1.json"), b"{}").unwrap();
290 std::fs::write(root.join("catalog").join("latest"), b"snap-1").unwrap();
291
292 fsync_snapshot_pointer(Some(root), None, "snap-1").unwrap();
293 }
294}