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, DiscoveredStore, FORMAT_VERSION, RootStore, SegmentCache,
16 SegmentCacheConfig, SegmentCacheMetrics, SegmentReader, StoreError, db_root_name,
17 decode_index_manifest, decode_segment_keys, is_index_manifest, meta_root_name,
18};
19use thiserror::Error;
20
21#[async_trait]
26pub trait PeerStorage: Send + Sync {
27 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
29 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
31}
32
33pub struct CachedPeerStorage {
35 storage: Arc<dyn PeerStorage>,
36 cache: SegmentCache,
37}
38
39impl CachedPeerStorage {
40 pub fn open(
46 storage: Arc<dyn PeerStorage>,
47 config: &SegmentCacheConfig,
48 ) -> std::io::Result<Self> {
49 Ok(Self {
50 storage,
51 cache: SegmentCache::open(config)?,
52 })
53 }
54
55 #[must_use]
57 pub fn metrics(&self) -> Arc<SegmentCacheMetrics> {
58 self.cache.metrics()
59 }
60}
61
62#[async_trait]
63impl SegmentReader for CachedPeerStorage {
64 async fn read_segment(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
65 self.storage.get_blob(id).await
66 }
67}
68
69#[async_trait]
70impl PeerStorage for CachedPeerStorage {
71 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
72 Ok(self
73 .cache
74 .get_or_load(self, id)
75 .await?
76 .map(|bytes| bytes.to_vec()))
77 }
78 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
79 self.storage.get_peer_root(name).await
80 }
81}
82
83#[async_trait]
84impl<S> PeerStorage for S
85where
86 S: BlobStore + RootStore + Send + Sync,
87{
88 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
89 BlobStore::get(self, id).await
90 }
91
92 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
93 RootStore::get_root(self, name).await
94 }
95}
96
97pub struct DiscoveredPeerStorage(DiscoveredStore);
99
100impl DiscoveredPeerStorage {
101 #[must_use]
103 pub const fn new(store: DiscoveredStore) -> Self {
104 Self(store)
105 }
106}
107
108#[async_trait]
109impl PeerStorage for DiscoveredPeerStorage {
110 async fn get_blob(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
111 self.0.get(id).await
112 }
113
114 async fn get_peer_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
115 self.0.get_root(name).await
116 }
117}
118
119#[derive(Debug, Error)]
121pub enum SnapshotError {
122 #[error(transparent)]
124 Store(#[from] StoreError),
125 #[error(transparent)]
127 Codec(#[from] CodecError),
128 #[error(transparent)]
130 Key(#[from] DecodeError),
131 #[error("malformed published root for database {0:?}")]
133 MalformedRoot(String),
134 #[error("storage format {found} is newer than supported format {supported}")]
136 UnsupportedFormat {
137 found: u32,
139 supported: u32,
141 },
142 #[error("published snapshot for database {0:?} has no metadata root")]
144 MissingMetadata(String),
145}
146
147pub async fn load_current_snapshot(
156 store: &dyn PeerStorage,
157 db: &str,
158) -> Result<Option<Db>, SnapshotError> {
159 let Some(root_bytes) = store.get_peer_root(&db_root_name(db)).await? else {
160 return Ok(None);
161 };
162 let root =
163 DbRoot::decode(&root_bytes).ok_or_else(|| SnapshotError::MalformedRoot(db.into()))?;
164 if root.format_version > FORMAT_VERSION {
165 return Err(SnapshotError::UnsupportedFormat {
166 found: root.format_version,
167 supported: FORMAT_VERSION,
168 });
169 }
170 let Some(roots) = root.roots else {
171 return Ok(None);
172 };
173 let Some(metadata) = store.get_peer_root(&meta_root_name(db)).await? else {
174 return Err(SnapshotError::MissingMetadata(db.into()));
175 };
176 let (schema, idents, interner) = codec::decode_metadata(&metadata)?;
177 let datoms = load_index_keys(store, &roots[0])
178 .await?
179 .into_iter()
180 .map(|key| Datom::from_key(IndexOrder::Eavt, &key))
181 .collect::<Result<Vec<_>, _>>()?;
182 Ok(Some(Db::from_current_snapshot(
183 root.index_basis_t,
184 schema,
185 idents,
186 interner,
187 datoms,
188 )))
189}
190
191async fn load_index_keys(store: &dyn PeerStorage, id: &BlobId) -> Result<Vec<Vec<u8>>, StoreError> {
194 let blob = store
195 .get_blob(id)
196 .await?
197 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
198 if !is_index_manifest(&blob) {
199 return decode_segment_keys(&blob);
200 }
201 let mut keys = Vec::new();
202 for child in decode_index_manifest(&blob)? {
203 let chunk = store
204 .get_blob(&child)
205 .await?
206 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
207 keys.extend(decode_segment_keys(&chunk)?);
208 }
209 Ok(keys)
210}
211
212pub struct SegmentSource<S> {
214 store: Arc<S>,
215 cache: SegmentCache,
216}
217
218impl<S: BlobStore + RootStore> SegmentSource<S> {
219 #[must_use]
221 pub fn new(store: Arc<S>) -> Self {
222 Self {
223 store,
224 cache: SegmentCache::default(),
225 }
226 }
227
228 pub async fn index_root(&self, db: &str) -> Result<Option<DbRoot>, StoreError> {
233 Ok(self
234 .store
235 .get_root(&db_root_name(db))
236 .await?
237 .as_deref()
238 .and_then(DbRoot::decode))
239 }
240
241 pub async fn lease_holder_endpoint(&self, db: &str) -> Result<Option<String>, StoreError> {
249 Ok(self
250 .index_root(db)
251 .await?
252 .and_then(|root| (!root.owner_endpoint.is_empty()).then_some(root.owner_endpoint)))
253 }
254
255 pub async fn segment(
262 &self,
263 root: &DbRoot,
264 order: IndexOrder,
265 ) -> Result<Option<Arc<[u8]>>, StoreError> {
266 let Some(roots) = &root.roots else {
267 return Ok(None);
268 };
269 let slot = match order {
270 IndexOrder::Eavt => 0,
271 IndexOrder::Aevt => 1,
272 IndexOrder::Avet => 2,
273 IndexOrder::Vaet => 3,
274 };
275 let Some(blob) = self
276 .cache
277 .get_or_load(self.store.as_ref(), &roots[slot])
278 .await?
279 else {
280 return Ok(None);
281 };
282 if !is_index_manifest(&blob) {
283 return Ok(Some(blob));
284 }
285 let mut bytes = Vec::new();
286 for child in decode_index_manifest(&blob)? {
287 let chunk = self
288 .cache
289 .get_or_load(self.store.as_ref(), &child)
290 .await?
291 .ok_or_else(|| StoreError::MissingBlob(child.clone()))?;
292 bytes.extend_from_slice(&chunk);
293 }
294 Ok(Some(bytes.into()))
295 }
296
297 pub fn segment_keys(bytes: &[u8]) -> Result<Vec<Vec<u8>>, StoreError> {
303 decode_segment_keys(bytes)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use corium_core::{EntityId, KeywordInterner, Value};
310 use corium_db::Idents;
311 use corium_store::{MemoryStore, RootStore};
312
313 use super::*;
314
315 #[tokio::test]
316 async fn loads_published_eavt_snapshot() {
317 let store = MemoryStore::default();
318 let datom = Datom {
319 e: EntityId::from_raw(1_001),
320 a: EntityId::from_raw(101),
321 v: Value::Str("snapshot".into()),
322 tx: EntityId::from_raw(37),
323 added: true,
324 };
325 let key = datom.key(IndexOrder::Eavt);
326 let mut segment = Vec::new();
327 segment.extend_from_slice(&(key.len() as u64).to_be_bytes());
328 segment.extend_from_slice(&key);
329 let id = store.put(&segment).await.expect("put segment");
330 let root = DbRoot {
331 format_version: FORMAT_VERSION,
332 lease_version: 1,
333 owner: "test".into(),
334 lease_expires_unix_ms: 0,
335 owner_endpoint: String::new(),
336 index_basis_t: 37,
337 roots: Some([id.clone(), id.clone(), id.clone(), id]),
338 next_entity_id: 1_005,
339 last_tx_instant: 0,
340 key_manifest_version: 0,
341 };
342 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
343 .await
344 .expect("put root");
345 let metadata = codec::encode_metadata(
346 &corium_core::Schema::default(),
347 &Idents::default(),
348 &KeywordInterner::default(),
349 );
350 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
351 .await
352 .expect("put metadata");
353
354 let db = load_current_snapshot(&store, "music")
355 .await
356 .expect("load snapshot")
357 .expect("published snapshot");
358 assert_eq!(db.basis_t(), 37);
359 assert_eq!(db.datoms(), vec![datom]);
360 }
361
362 #[tokio::test]
363 async fn loads_chunked_manifest_snapshot() {
364 let store = MemoryStore::default();
365 let datoms: Vec<Datom> = (0..4u64)
366 .map(|n| Datom {
367 e: EntityId::from_raw(1_001 + n),
368 a: EntityId::from_raw(101),
369 v: Value::Long(i64::try_from(n).unwrap()),
370 tx: EntityId::from_raw(37),
371 added: true,
372 })
373 .collect();
374 let mut chunk_ids = Vec::new();
376 for pair in datoms.chunks(2) {
377 let mut chunk = Vec::new();
378 for datom in pair {
379 let key = datom.key(IndexOrder::Eavt);
380 chunk.extend_from_slice(&(key.len() as u64).to_be_bytes());
381 chunk.extend_from_slice(&key);
382 }
383 chunk_ids.push(store.put(&chunk).await.expect("put chunk"));
384 }
385 let manifest = corium_store::encode_index_manifest(&chunk_ids);
386 let id = store.put(&manifest).await.expect("put manifest");
387 let root = DbRoot {
388 format_version: FORMAT_VERSION,
389 lease_version: 1,
390 owner: "test".into(),
391 lease_expires_unix_ms: 0,
392 owner_endpoint: String::new(),
393 index_basis_t: 37,
394 roots: Some([id.clone(), id.clone(), id.clone(), id]),
395 next_entity_id: 1_005,
396 last_tx_instant: 0,
397 key_manifest_version: 0,
398 };
399 RootStore::cas_root(&store, &db_root_name("music"), None, &root.encode())
400 .await
401 .expect("put root");
402 let metadata = codec::encode_metadata(
403 &corium_core::Schema::default(),
404 &Idents::default(),
405 &KeywordInterner::default(),
406 );
407 RootStore::cas_root(&store, &meta_root_name("music"), None, &metadata)
408 .await
409 .expect("put metadata");
410
411 let db = load_current_snapshot(&store, "music")
412 .await
413 .expect("load snapshot")
414 .expect("published snapshot");
415 assert_eq!(db.basis_t(), 37);
416 assert_eq!(db.datoms(), datoms);
417 }
418
419 #[tokio::test]
420 async fn absent_publication_falls_back_to_log_replay() {
421 assert!(
422 load_current_snapshot(&MemoryStore::default(), "music")
423 .await
424 .expect("load snapshot")
425 .is_none()
426 );
427 }
428}