1use std::sync::Arc;
9
10use async_trait::async_trait;
11use corium_core::{Datom, IndexOrder, encoding::DecodeError};
12use corium_db::Db;
13use corium_protocol::codec::{self, CodecError};
14use corium_store::{
15 BlobId, BlobStore, DbRoot, FORMAT_VERSION, RootStore, SegmentCache, StoreError, db_root_name,
16 decode_index_manifest, decode_segment_keys, is_index_manifest, meta_root_name,
17};
18use thiserror::Error;
19
20#[async_trait]
25pub trait PeerStorage: Send + Sync {
26 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
28 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
30}
31
32#[async_trait]
33impl<S> PeerStorage for S
34where
35 S: BlobStore + RootStore + Send + Sync,
36{
37 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
38 BlobStore::get(self, id).await
39 }
40
41 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
42 RootStore::get_root(self, name).await
43 }
44}
45
46#[derive(Debug, Error)]
48pub enum SnapshotError {
49 #[error(transparent)]
51 Store(#[from] StoreError),
52 #[error(transparent)]
54 Codec(#[from] CodecError),
55 #[error(transparent)]
57 Key(#[from] DecodeError),
58 #[error("malformed published root for database {0:?}")]
60 MalformedRoot(String),
61 #[error("storage format {found} is newer than supported format {supported}")]
63 UnsupportedFormat {
64 found: u32,
66 supported: u32,
68 },
69 #[error("published snapshot for database {0:?} has no metadata root")]
71 MissingMetadata(String),
72}
73
74pub async fn load_current_snapshot(
83 store: &dyn PeerStorage,
84 db: &str,
85) -> Result<Option<Db>, SnapshotError> {
86 let Some(root_bytes) = store.get_peer_root(&db_root_name(db)).await? else {
87 return Ok(None);
88 };
89 let root =
90 DbRoot::decode(&root_bytes).ok_or_else(|| SnapshotError::MalformedRoot(db.into()))?;
91 if root.format_version > FORMAT_VERSION {
92 return Err(SnapshotError::UnsupportedFormat {
93 found: root.format_version,
94 supported: FORMAT_VERSION,
95 });
96 }
97 let Some(roots) = root.roots else {
98 return Ok(None);
99 };
100 let Some(metadata) = store.get_peer_root(&meta_root_name(db)).await? else {
101 return Err(SnapshotError::MissingMetadata(db.into()));
102 };
103 let (schema, idents, interner) = codec::decode_metadata(&metadata)?;
104 let datoms = load_index_keys(store, &roots[0])
105 .await?
106 .into_iter()
107 .map(|key| Datom::from_key(IndexOrder::Eavt, &key))
108 .collect::<Result<Vec<_>, _>>()?;
109 Ok(Some(Db::from_current_snapshot(
110 root.index_basis_t,
111 schema,
112 idents,
113 interner,
114 datoms,
115 )))
116}
117
118async fn load_index_keys(store: &dyn PeerStorage, id: &BlobId) -> Result<Vec<Vec<u8>>, StoreError> {
121 let blob = store
122 .get_blob(id)
123 .await?
124 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
125 if !is_index_manifest(&blob) {
126 return decode_segment_keys(&blob);
127 }
128 let mut keys = Vec::new();
129 for child in decode_index_manifest(&blob)? {
130 let chunk = store
131 .get_blob(&child)
132 .await?
133 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
134 keys.extend(decode_segment_keys(&chunk)?);
135 }
136 Ok(keys)
137}
138
139pub struct SegmentSource<S> {
141 store: Arc<S>,
142 cache: SegmentCache,
143}
144
145impl<S: BlobStore + RootStore> SegmentSource<S> {
146 #[must_use]
148 pub fn new(store: Arc<S>) -> Self {
149 Self {
150 store,
151 cache: SegmentCache::default(),
152 }
153 }
154
155 pub async fn index_root(&self, db: &str) -> Result<Option<DbRoot>, StoreError> {
160 Ok(self
161 .store
162 .get_root(&db_root_name(db))
163 .await?
164 .as_deref()
165 .and_then(DbRoot::decode))
166 }
167
168 pub async fn lease_holder_endpoint(&self, db: &str) -> Result<Option<String>, StoreError> {
176 Ok(self
177 .index_root(db)
178 .await?
179 .and_then(|root| (!root.owner_endpoint.is_empty()).then_some(root.owner_endpoint)))
180 }
181
182 pub async fn segment(
189 &self,
190 root: &DbRoot,
191 order: IndexOrder,
192 ) -> Result<Option<Arc<[u8]>>, StoreError> {
193 let Some(roots) = &root.roots else {
194 return Ok(None);
195 };
196 let slot = match order {
197 IndexOrder::Eavt => 0,
198 IndexOrder::Aevt => 1,
199 IndexOrder::Avet => 2,
200 IndexOrder::Vaet => 3,
201 };
202 let Some(blob) = self
203 .cache
204 .get_or_load(self.store.as_ref(), &roots[slot])
205 .await?
206 else {
207 return Ok(None);
208 };
209 if !is_index_manifest(&blob) {
210 return Ok(Some(blob));
211 }
212 let mut bytes = Vec::new();
213 for child in decode_index_manifest(&blob)? {
214 let chunk = self
215 .cache
216 .get_or_load(self.store.as_ref(), &child)
217 .await?
218 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
219 bytes.extend_from_slice(&chunk);
220 }
221 Ok(Some(bytes.into()))
222 }
223
224 pub fn segment_keys(bytes: &[u8]) -> Result<Vec<Vec<u8>>, StoreError> {
230 decode_segment_keys(bytes)
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use corium_core::{EntityId, KeywordInterner, Value};
237 use corium_db::Idents;
238 use corium_store::{MemoryStore, RootStore};
239
240 use super::*;
241
242 #[tokio::test]
243 async fn loads_published_eavt_snapshot() {
244 let store = MemoryStore::default();
245 let datom = Datom {
246 e: EntityId::from_raw(1_001),
247 a: EntityId::from_raw(101),
248 v: Value::Str("snapshot".into()),
249 tx: EntityId::from_raw(37),
250 added: true,
251 };
252 let key = datom.key(IndexOrder::Eavt);
253 let mut segment = Vec::new();
254 segment.extend_from_slice(&(key.len() as u64).to_be_bytes());
255 segment.extend_from_slice(&key);
256 let id = store.put(&segment).await.expect("put segment");
257 let root = DbRoot {
258 format_version: FORMAT_VERSION,
259 lease_version: 1,
260 owner: "test".into(),
261 lease_expires_unix_ms: 0,
262 owner_endpoint: String::new(),
263 index_basis_t: 37,
264 roots: Some([id.clone(), id.clone(), id.clone(), id]),
265 next_entity_id: 1_005,
266 last_tx_instant: 0,
267 };
268 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
269 .await
270 .expect("put root");
271 let metadata = codec::encode_metadata(
272 &corium_core::Schema::default(),
273 &Idents::default(),
274 &KeywordInterner::default(),
275 );
276 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
277 .await
278 .expect("put metadata");
279
280 let db = load_current_snapshot(&store, "music")
281 .await
282 .expect("load snapshot")
283 .expect("published snapshot");
284 assert_eq!(db.basis_t(), 37);
285 assert_eq!(db.datoms(), vec![datom]);
286 }
287
288 #[tokio::test]
289 async fn loads_chunked_manifest_snapshot() {
290 let store = MemoryStore::default();
291 let datoms: Vec<Datom> = (0..4u64)
292 .map(|n| Datom {
293 e: EntityId::from_raw(1_001 + n),
294 a: EntityId::from_raw(101),
295 v: Value::Long(i64::try_from(n).unwrap()),
296 tx: EntityId::from_raw(37),
297 added: true,
298 })
299 .collect();
300 let mut chunk_ids = Vec::new();
302 for pair in datoms.chunks(2) {
303 let mut chunk = Vec::new();
304 for datom in pair {
305 let key = datom.key(IndexOrder::Eavt);
306 chunk.extend_from_slice(&(key.len() as u64).to_be_bytes());
307 chunk.extend_from_slice(&key);
308 }
309 chunk_ids.push(store.put(&chunk).await.expect("put chunk"));
310 }
311 let manifest = corium_store::encode_index_manifest(&chunk_ids);
312 let id = store.put(&manifest).await.expect("put manifest");
313 let root = DbRoot {
314 format_version: FORMAT_VERSION,
315 lease_version: 1,
316 owner: "test".into(),
317 lease_expires_unix_ms: 0,
318 owner_endpoint: String::new(),
319 index_basis_t: 37,
320 roots: Some([id.clone(), id.clone(), id.clone(), id]),
321 next_entity_id: 1_005,
322 last_tx_instant: 0,
323 };
324 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
325 .await
326 .expect("put root");
327 let metadata = codec::encode_metadata(
328 &corium_core::Schema::default(),
329 &Idents::default(),
330 &KeywordInterner::default(),
331 );
332 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
333 .await
334 .expect("put metadata");
335
336 let db = load_current_snapshot(&store, "music")
337 .await
338 .expect("load snapshot")
339 .expect("published snapshot");
340 assert_eq!(db.basis_t(), 37);
341 assert_eq!(db.datoms(), datoms);
342 }
343
344 #[tokio::test]
345 async fn absent_publication_falls_back_to_log_replay() {
346 assert!(
347 load_current_snapshot(&MemoryStore::default(), "music")
348 .await
349 .expect("load snapshot")
350 .is_none()
351 );
352 }
353}