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, 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
139fn decode_segment_keys(bytes: &[u8]) -> Result<Vec<Vec<u8>>, StoreError> {
140 let mut keys = Vec::new();
141 let mut input = bytes;
142 while !input.is_empty() {
143 let (len_bytes, rest) = input
144 .split_at_checked(8)
145 .ok_or_else(|| StoreError::Io(std::io::Error::other("truncated segment")))?;
146 let len = usize::try_from(u64::from_be_bytes(len_bytes.try_into().unwrap_or_default()))
147 .map_err(|_| StoreError::Io(std::io::Error::other("segment key too large")))?;
148 let (key, rest) = rest
149 .split_at_checked(len)
150 .ok_or_else(|| StoreError::Io(std::io::Error::other("truncated segment key")))?;
151 keys.push(key.to_vec());
152 input = rest;
153 }
154 Ok(keys)
155}
156
157pub struct SegmentSource<S> {
159 store: Arc<S>,
160 cache: SegmentCache,
161}
162
163impl<S: BlobStore + RootStore> SegmentSource<S> {
164 #[must_use]
166 pub fn new(store: Arc<S>) -> Self {
167 Self {
168 store,
169 cache: SegmentCache::default(),
170 }
171 }
172
173 pub async fn index_root(&self, db: &str) -> Result<Option<DbRoot>, StoreError> {
178 Ok(self
179 .store
180 .get_root(&db_root_name(db))
181 .await?
182 .as_deref()
183 .and_then(DbRoot::decode))
184 }
185
186 pub async fn lease_holder_endpoint(&self, db: &str) -> Result<Option<String>, StoreError> {
194 Ok(self
195 .index_root(db)
196 .await?
197 .and_then(|root| (!root.owner_endpoint.is_empty()).then_some(root.owner_endpoint)))
198 }
199
200 pub async fn segment(
207 &self,
208 root: &DbRoot,
209 order: IndexOrder,
210 ) -> Result<Option<Arc<[u8]>>, StoreError> {
211 let Some(roots) = &root.roots else {
212 return Ok(None);
213 };
214 let slot = match order {
215 IndexOrder::Eavt => 0,
216 IndexOrder::Aevt => 1,
217 IndexOrder::Avet => 2,
218 IndexOrder::Vaet => 3,
219 };
220 let Some(blob) = self
221 .cache
222 .get_or_load(self.store.as_ref(), &roots[slot])
223 .await?
224 else {
225 return Ok(None);
226 };
227 if !is_index_manifest(&blob) {
228 return Ok(Some(blob));
229 }
230 let mut bytes = Vec::new();
231 for child in decode_index_manifest(&blob)? {
232 let chunk = self
233 .cache
234 .get_or_load(self.store.as_ref(), &child)
235 .await?
236 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
237 bytes.extend_from_slice(&chunk);
238 }
239 Ok(Some(bytes.into()))
240 }
241
242 pub fn segment_keys(bytes: &[u8]) -> Result<Vec<Vec<u8>>, StoreError> {
248 decode_segment_keys(bytes)
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use corium_core::{EntityId, KeywordInterner, Value};
255 use corium_db::Idents;
256 use corium_store::{MemoryStore, RootStore};
257
258 use super::*;
259
260 #[tokio::test]
261 async fn loads_published_eavt_snapshot() {
262 let store = MemoryStore::default();
263 let datom = Datom {
264 e: EntityId::from_raw(1_001),
265 a: EntityId::from_raw(101),
266 v: Value::Str("snapshot".into()),
267 tx: EntityId::from_raw(37),
268 added: true,
269 };
270 let key = datom.key(IndexOrder::Eavt);
271 let mut segment = Vec::new();
272 segment.extend_from_slice(&(key.len() as u64).to_be_bytes());
273 segment.extend_from_slice(&key);
274 let id = store.put(&segment).await.expect("put segment");
275 let root = DbRoot {
276 format_version: FORMAT_VERSION,
277 lease_version: 1,
278 owner: "test".into(),
279 lease_expires_unix_ms: 0,
280 owner_endpoint: String::new(),
281 index_basis_t: 37,
282 roots: Some([id.clone(), id.clone(), id.clone(), id]),
283 };
284 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
285 .await
286 .expect("put root");
287 let metadata = codec::encode_metadata(
288 &corium_core::Schema::default(),
289 &Idents::default(),
290 &KeywordInterner::default(),
291 );
292 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
293 .await
294 .expect("put metadata");
295
296 let db = load_current_snapshot(&store, "music")
297 .await
298 .expect("load snapshot")
299 .expect("published snapshot");
300 assert_eq!(db.basis_t(), 37);
301 assert_eq!(db.datoms(), vec![datom]);
302 }
303
304 #[tokio::test]
305 async fn loads_chunked_manifest_snapshot() {
306 let store = MemoryStore::default();
307 let datoms: Vec<Datom> = (0..4u64)
308 .map(|n| Datom {
309 e: EntityId::from_raw(1_001 + n),
310 a: EntityId::from_raw(101),
311 v: Value::Long(i64::try_from(n).unwrap()),
312 tx: EntityId::from_raw(37),
313 added: true,
314 })
315 .collect();
316 let mut chunk_ids = Vec::new();
318 for pair in datoms.chunks(2) {
319 let mut chunk = Vec::new();
320 for datom in pair {
321 let key = datom.key(IndexOrder::Eavt);
322 chunk.extend_from_slice(&(key.len() as u64).to_be_bytes());
323 chunk.extend_from_slice(&key);
324 }
325 chunk_ids.push(store.put(&chunk).await.expect("put chunk"));
326 }
327 let manifest = corium_store::encode_index_manifest(&chunk_ids);
328 let id = store.put(&manifest).await.expect("put manifest");
329 let root = DbRoot {
330 format_version: FORMAT_VERSION,
331 lease_version: 1,
332 owner: "test".into(),
333 lease_expires_unix_ms: 0,
334 owner_endpoint: String::new(),
335 index_basis_t: 37,
336 roots: Some([id.clone(), id.clone(), id.clone(), id]),
337 };
338 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
339 .await
340 .expect("put root");
341 let metadata = codec::encode_metadata(
342 &corium_core::Schema::default(),
343 &Idents::default(),
344 &KeywordInterner::default(),
345 );
346 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
347 .await
348 .expect("put metadata");
349
350 let db = load_current_snapshot(&store, "music")
351 .await
352 .expect("load snapshot")
353 .expect("published snapshot");
354 assert_eq!(db.basis_t(), 37);
355 assert_eq!(db.datoms(), datoms);
356 }
357
358 #[tokio::test]
359 async fn absent_publication_falls_back_to_log_replay() {
360 assert!(
361 load_current_snapshot(&MemoryStore::default(), "music")
362 .await
363 .expect("load snapshot")
364 .is_none()
365 );
366 }
367}