1use std::{
7 collections::{BTreeMap, HashMap, HashSet},
8 fmt,
9 fs::{self, File, OpenOptions},
10 io,
11 path::{Path, PathBuf},
12 pin::Pin,
13 sync::{Arc, RwLock},
14 time::{Duration, SystemTime},
15};
16
17use async_trait::async_trait;
18use fs2::FileExt;
19use thiserror::Error;
20use tokio_stream::{Stream, StreamExt, wrappers::ReceiverStream};
21
22mod segment_cache;
23pub use segment_cache::{SegmentCache, SegmentCacheConfig, SegmentCacheMetrics, SegmentReader};
24
25mod discovery;
26pub use discovery::{DiscoveredStore, DiscoveredStoreSpec, StorageConnectionError};
27
28mod encrypted_store;
29pub use encrypted_store::EncryptedBlobStore;
30
31mod key_manifest;
32pub use key_manifest::{
33 KEY_MANIFEST_FORMAT_VERSION, KeyManifest, LOG_RECORDS_PER_EPOCH_LIMIT,
34 LOG_RECORDS_PER_EPOCH_WARN, ProtectionClassKey, StorageAlgorithm, StorageKey, StorageKeyState,
35 keys_root_name, load_key_manifest, publish_key_manifest,
36};
37
38mod snapshot;
39pub use snapshot::{
40 INDEX_MANIFEST_MAGIC, chunk_segment_keys, decode_index_manifest, decode_segment_keys,
41 encode_index_manifest, encode_segment_chunk, index_blob_children, is_index_manifest,
42};
43
44#[cfg(feature = "postgres")]
45mod postgres_store;
46#[cfg(feature = "postgres")]
47pub use postgres_store::PostgresBlobStore;
48#[cfg(feature = "turso")]
49mod turso_store;
50#[cfg(feature = "turso")]
51pub use turso_store::TursoBlobStore;
52#[cfg(feature = "s3")]
53mod s3_store;
54#[cfg(feature = "s3")]
55pub use s3_store::{S3BlobStore, S3ClientConfig, normalize_s3_prefix};
56
57#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
59pub struct BlobId(String);
60
61impl BlobId {
62 #[must_use]
64 pub fn as_str(&self) -> &str {
65 &self.0
66 }
67
68 #[must_use]
70 pub fn from_hex(text: &str) -> Option<Self> {
71 (text.len() == 64 && text.bytes().all(|byte| byte.is_ascii_hexdigit()))
72 .then(|| Self(text.to_owned()))
73 }
74}
75
76impl fmt::Display for BlobId {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 f.write_str(&self.0)
79 }
80}
81
82#[derive(Debug, Error)]
84pub enum StoreError {
85 #[error("store I/O failed: {0}")]
87 Io(#[from] io::Error),
88 #[error("root CAS failed: expected {expected:?}, actual {actual:?}")]
90 CasFailed {
91 expected: Option<Vec<u8>>,
93 actual: Option<Vec<u8>>,
95 },
96 #[error("blob content did not match digest {0}")]
98 CorruptBlob(BlobId),
99 #[error("reachable blob is missing: {0}")]
101 MissingBlob(BlobId),
102 #[error("encrypted blob requires unavailable storage-key epoch {0}")]
104 MissingEncryptionKey(u32),
105 #[error("blob store returned id {actual}, expected {expected}")]
107 BlobIdMismatch {
108 expected: BlobId,
110 actual: BlobId,
112 },
113 #[error("encrypted blob failed: {0}")]
115 Encryption(#[from] corium_crypt::CryptError),
116 #[error("storage key unavailable: {0}")]
118 Keyring(corium_crypt::KeyError),
119 #[error("database {db:?} is encrypted under key {kek}; no storage key is configured")]
121 EncryptedWithoutKey {
122 db: String,
124 kek: String,
126 },
127 #[error("invalid key manifest: {0}")]
129 InvalidKeyManifest(String),
130 #[error("key manifest format {found} is newer than supported format {supported}")]
132 UnsupportedKeyManifest {
133 found: u32,
135 supported: u32,
137 },
138 #[error("unsupported storage encryption algorithm {0:?}")]
140 UnsupportedKeyAlgorithm(String),
141 #[error("storage key epochs are exhausted")]
143 StorageEpochExhausted,
144 #[error("invalid root name {0:?}")]
146 InvalidRootName(String),
147 #[error("store blocking task failed: {0}")]
149 BlockingTask(String),
150 #[error("the transactor's local storage at {0} is not reachable from this process")]
153 UnreachableLocalStorage(PathBuf),
154 #[cfg(feature = "postgres")]
156 #[error("PostgreSQL store failed: {0}")]
157 Postgres(#[from] deadpool_postgres::tokio_postgres::Error),
158 #[cfg(feature = "postgres")]
160 #[error("PostgreSQL connection pool failed: {0}")]
161 PostgresPool(#[from] deadpool_postgres::PoolError),
162 #[cfg(feature = "postgres")]
164 #[error("PostgreSQL connection pool configuration failed: {0}")]
165 PostgresPoolCreate(#[from] deadpool_postgres::CreatePoolError),
166 #[cfg(feature = "postgres")]
168 #[error("cannot load native certificate roots for PostgreSQL TLS: {0}")]
169 PostgresTlsRoots(String),
170 #[cfg(feature = "postgres")]
172 #[error("PostgreSQL store contains invalid data: {0}")]
173 InvalidPostgresData(String),
174 #[cfg(feature = "turso")]
176 #[error("Turso blob store failed: {0}")]
177 Turso(#[from] turso::Error),
178 #[cfg(feature = "turso")]
180 #[error("Turso database path is not valid UTF-8: {0:?}")]
181 InvalidTursoPath(PathBuf),
182 #[cfg(feature = "turso")]
184 #[error("Turso blob store contains invalid data: {0}")]
185 InvalidTursoData(String),
186 #[cfg(feature = "s3")]
188 #[error("S3 store failed: {0}")]
189 S3(String),
190 #[cfg(feature = "s3")]
192 #[error("S3 store contains invalid data: {0}")]
193 InvalidS3Data(String),
194}
195
196#[cfg(feature = "s3")]
205impl<E> From<aws_sdk_s3::error::SdkError<E>> for StoreError
206where
207 E: std::error::Error + Send + Sync + 'static,
208{
209 fn from(error: aws_sdk_s3::error::SdkError<E>) -> Self {
210 StoreError::S3(aws_sdk_s3::error::DisplayErrorContext(error).to_string())
211 }
212}
213
214pub type BlobIdStream = Pin<Box<dyn Stream<Item = Result<BlobId, StoreError>> + Send + 'static>>;
216
217async fn run_blocking<T>(
218 operation: impl FnOnce() -> Result<T, StoreError> + Send + 'static,
219) -> Result<T, StoreError>
220where
221 T: Send + 'static,
222{
223 tokio::task::spawn_blocking(operation)
224 .await
225 .map_err(|error| StoreError::BlockingTask(error.to_string()))?
226}
227
228#[async_trait]
230pub trait BlobStore: Send + Sync {
231 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError>;
237 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
243 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
249 Ok(self.get(id).await?.is_some())
250 }
251 async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
258 let id = digest(bytes);
259 if self.contains(&id).await? {
260 Ok(id)
261 } else {
262 self.put(bytes).await
263 }
264 }
265 async fn delete(&self, id: &BlobId) -> Result<(), StoreError>;
271 async fn list(&self) -> Result<BlobIdStream, StoreError>;
277 async fn modified_at(&self, _id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
284 Ok(None)
285 }
286}
287
288#[async_trait]
292impl<S: BlobStore + ?Sized> BlobStore for Arc<S> {
293 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
294 self.as_ref().put(bytes).await
295 }
296
297 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
298 self.as_ref().get(id).await
299 }
300
301 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
302 self.as_ref().contains(id).await
303 }
304
305 async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
306 self.as_ref().put_if_absent(bytes).await
307 }
308
309 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
310 self.as_ref().delete(id).await
311 }
312
313 async fn list(&self) -> Result<BlobIdStream, StoreError> {
314 self.as_ref().list().await
315 }
316
317 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
318 self.as_ref().modified_at(id).await
319 }
320}
321
322#[async_trait]
324pub trait RootStore: Send + Sync {
325 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
331 async fn cas_root(
337 &self,
338 name: &str,
339 expected: Option<&[u8]>,
340 new: &[u8],
341 ) -> Result<(), StoreError>;
342 async fn delete_root(&self, name: &str) -> Result<(), StoreError>;
348 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError>;
354}
355
356#[async_trait]
357impl<S: RootStore + ?Sized> RootStore for Arc<S> {
358 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
359 self.as_ref().get_root(name).await
360 }
361
362 async fn cas_root(
363 &self,
364 name: &str,
365 expected: Option<&[u8]>,
366 new: &[u8],
367 ) -> Result<(), StoreError> {
368 self.as_ref().cas_root(name, expected, new).await
369 }
370
371 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
372 self.as_ref().delete_root(name).await
373 }
374
375 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
376 self.as_ref().list_roots(prefix).await
377 }
378}
379
380#[derive(Clone, Default)]
382pub struct MemoryStore {
383 inner: Arc<RwLock<MemoryInner>>,
384}
385#[derive(Default)]
386struct MemoryInner {
387 blobs: HashMap<BlobId, Vec<u8>>,
388 roots: BTreeMap<String, Vec<u8>>,
389}
390
391#[async_trait]
392impl BlobStore for MemoryStore {
393 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
394 let inner = Arc::clone(&self.inner);
395 let bytes = bytes.to_vec();
396 run_blocking(move || {
397 let id = digest(&bytes);
398 inner
399 .write()
400 .expect("poisoned store lock")
401 .blobs
402 .insert(id.clone(), bytes);
403 Ok(id)
404 })
405 .await
406 }
407 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
408 let inner = Arc::clone(&self.inner);
409 let id = id.clone();
410 run_blocking(move || {
411 Ok(inner
412 .read()
413 .expect("poisoned store lock")
414 .blobs
415 .get(&id)
416 .cloned())
417 })
418 .await
419 }
420 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
421 let inner = Arc::clone(&self.inner);
422 let id = id.clone();
423 run_blocking(move || {
424 inner
425 .write()
426 .expect("poisoned store lock")
427 .blobs
428 .remove(&id);
429 Ok(())
430 })
431 .await
432 }
433 async fn list(&self) -> Result<BlobIdStream, StoreError> {
434 let inner = Arc::clone(&self.inner);
435 let ids = run_blocking(move || {
436 Ok(inner
437 .read()
438 .expect("poisoned store lock")
439 .blobs
440 .keys()
441 .cloned()
442 .collect::<Vec<_>>())
443 })
444 .await?;
445 Ok(Box::pin(tokio_stream::iter(ids.into_iter().map(Ok))))
446 }
447}
448#[async_trait]
449impl RootStore for MemoryStore {
450 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
451 let inner = Arc::clone(&self.inner);
452 let name = name.to_owned();
453 run_blocking(move || {
454 Ok(inner
455 .read()
456 .expect("poisoned store lock")
457 .roots
458 .get(&name)
459 .cloned())
460 })
461 .await
462 }
463 async fn cas_root(
464 &self,
465 name: &str,
466 expected: Option<&[u8]>,
467 new: &[u8],
468 ) -> Result<(), StoreError> {
469 let inner = Arc::clone(&self.inner);
470 let name = name.to_owned();
471 let expected = expected.map(<[u8]>::to_vec);
472 let new = new.to_vec();
473 run_blocking(move || {
474 let mut inner = inner.write().expect("poisoned store lock");
475 let actual = inner.roots.get(&name).cloned();
476 if actual != expected {
477 return Err(StoreError::CasFailed { expected, actual });
478 }
479 inner.roots.insert(name, new);
480 Ok(())
481 })
482 .await
483 }
484 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
485 let inner = Arc::clone(&self.inner);
486 let name = name.to_owned();
487 run_blocking(move || {
488 inner
489 .write()
490 .expect("poisoned store lock")
491 .roots
492 .remove(&name);
493 Ok(())
494 })
495 .await
496 }
497 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
498 let inner = Arc::clone(&self.inner);
499 let prefix = prefix.to_owned();
500 run_blocking(move || {
501 Ok(inner
502 .read()
503 .expect("poisoned store lock")
504 .roots
505 .keys()
506 .filter(|name| name.starts_with(&prefix))
507 .cloned()
508 .collect())
509 })
510 .await
511 }
512}
513
514#[derive(Clone)]
516pub struct FsStore {
517 root: PathBuf,
518}
519impl FsStore {
520 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
526 let root = root.as_ref().to_path_buf();
527 fs::create_dir_all(root.join("blobs"))?;
528 fs::create_dir_all(root.join("roots"))?;
529 Ok(Self { root })
530 }
531 fn blob_path(&self, id: &BlobId) -> PathBuf {
532 self.root.join("blobs").join(id.as_str())
533 }
534 fn root_path(&self, name: &str) -> Result<PathBuf, StoreError> {
535 if name.is_empty()
536 || name == "."
537 || name == ".."
538 || name.contains('/')
539 || name.contains('\\')
540 {
541 return Err(StoreError::InvalidRootName(name.to_owned()));
542 }
543 Ok(self.root.join("roots").join(name))
544 }
545
546 fn root_lock(&self, name: &str) -> Result<RootLock, StoreError> {
547 let root_path = self.root_path(name)?;
548 RootLock::acquire(&root_path.with_extension("lock"))
549 }
550}
551#[async_trait]
552impl BlobStore for FsStore {
553 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
554 let store = self.clone();
555 let bytes = bytes.to_vec();
556 run_blocking(move || {
557 let id = digest(&bytes);
558 let path = store.blob_path(&id);
559 if !path.exists() {
560 let tmp = path.with_extension("tmp");
561 fs::write(&tmp, &bytes)?;
562 fs::rename(tmp, path)?;
563 }
564 Ok(id)
565 })
566 .await
567 }
568 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
569 let store = self.clone();
570 let id = id.clone();
571 run_blocking(move || {
572 let path = store.blob_path(&id);
573 if !path.exists() {
574 return Ok(None);
575 }
576 let bytes = fs::read(path)?;
577 if digest(&bytes) != id {
578 return Err(StoreError::CorruptBlob(id));
579 }
580 Ok(Some(bytes))
581 })
582 .await
583 }
584 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
585 let store = self.clone();
586 let id = id.clone();
587 run_blocking(move || Ok(store.blob_path(&id).is_file())).await
588 }
589 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
590 let store = self.clone();
591 let id = id.clone();
592 run_blocking(move || match fs::remove_file(store.blob_path(&id)) {
593 Ok(()) => Ok(()),
594 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
595 Err(error) => Err(error.into()),
596 })
597 .await
598 }
599 async fn list(&self) -> Result<BlobIdStream, StoreError> {
600 let path = self.root.join("blobs");
601 let entries = run_blocking(move || Ok(fs::read_dir(path)?)).await?;
602 let (tx, rx) = tokio::sync::mpsc::channel(64);
603 let failure_tx = tx.clone();
604 tokio::spawn(async move {
605 let result = tokio::task::spawn_blocking(move || {
606 for entry in entries {
607 let id = (|| {
608 let entry = entry?;
609 if !entry.file_type()?.is_file() {
610 return Ok(None);
611 }
612 Ok(entry.file_name().to_str().and_then(BlobId::from_hex))
613 })();
614 match id {
615 Ok(Some(id)) => {
616 if tx.blocking_send(Ok(id)).is_err() {
617 return;
618 }
619 }
620 Ok(None) => {}
621 Err(error) => {
622 let _ = tx.blocking_send(Err(StoreError::Io(error)));
623 return;
624 }
625 }
626 }
627 })
628 .await;
629 if let Err(error) = result {
630 let _ = failure_tx
631 .send(Err(StoreError::BlockingTask(error.to_string())))
632 .await;
633 }
634 });
635 Ok(Box::pin(ReceiverStream::new(rx)))
636 }
637 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
638 let store = self.clone();
639 let id = id.clone();
640 run_blocking(move || match fs::metadata(store.blob_path(&id)) {
641 Ok(metadata) => Ok(Some(metadata.modified()?)),
642 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
643 Err(error) => Err(error.into()),
644 })
645 .await
646 }
647}
648#[async_trait]
649impl RootStore for FsStore {
650 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
651 let store = self.clone();
652 let name = name.to_owned();
653 run_blocking(move || match fs::read(store.root_path(&name)?) {
654 Ok(value) => Ok(Some(value)),
655 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
656 Err(error) => Err(error.into()),
657 })
658 .await
659 }
660 async fn cas_root(
661 &self,
662 name: &str,
663 expected: Option<&[u8]>,
664 new: &[u8],
665 ) -> Result<(), StoreError> {
666 let store = self.clone();
667 let name = name.to_owned();
668 let expected = expected.map(<[u8]>::to_vec);
669 let new = new.to_vec();
670 run_blocking(move || {
671 let _lock = store.root_lock(&name)?;
672 let path = store.root_path(&name)?;
673 let actual = match fs::read(&path) {
674 Ok(value) => Some(value),
675 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
676 Err(error) => return Err(error.into()),
677 };
678 if actual != expected {
679 return Err(StoreError::CasFailed { expected, actual });
680 }
681 let tmp = path.with_extension("tmp");
682 fs::write(&tmp, new)?;
683 fs::rename(tmp, path)?;
684 Ok(())
685 })
686 .await
687 }
688 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
689 let store = self.clone();
690 let name = name.to_owned();
691 run_blocking(move || {
692 let _lock = store.root_lock(&name)?;
693 match fs::remove_file(store.root_path(&name)?) {
694 Ok(()) => Ok(()),
695 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
696 Err(error) => Err(error.into()),
697 }
698 })
699 .await
700 }
701 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
702 let root = self.root.clone();
703 let prefix = prefix.to_owned();
704 run_blocking(move || {
705 let mut names = Vec::new();
706 for entry in fs::read_dir(root.join("roots"))? {
707 let entry = entry?;
708 if !entry.file_type()?.is_file() {
709 continue;
710 }
711 if let Some(name) = entry.file_name().to_str() {
712 let auxiliary = Path::new(name).extension().is_some_and(|ext| {
713 ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("tmp")
714 });
715 if name.starts_with(&prefix) && !auxiliary {
716 names.push(name.to_owned());
717 }
718 }
719 }
720 names.sort();
721 Ok(names)
722 })
723 .await
724 }
725}
726
727#[must_use]
729pub fn digest(bytes: &[u8]) -> BlobId {
730 BlobId(blake3::hash(bytes).to_hex().to_string())
731}
732
733#[must_use]
735pub fn db_root_name(db: &str) -> String {
736 format!("db:{db}")
737}
738
739#[must_use]
741pub fn meta_root_name(db: &str) -> String {
742 format!("meta:{db}")
743}
744
745pub const FORMAT_VERSION: u32 = 4;
763
764#[derive(Clone, Debug, Eq, PartialEq)]
773pub struct DbRoot {
774 pub format_version: u32,
776 pub lease_version: u64,
778 pub owner: String,
781 pub lease_expires_unix_ms: i64,
783 pub owner_endpoint: String,
786 pub index_basis_t: u64,
788 pub roots: Option<[BlobId; 4]>,
791 pub next_entity_id: u64,
801 pub last_tx_instant: i64,
808 pub key_manifest_version: u64,
818}
819
820fn field_line(out: &mut String, value: &str) {
822 if value.is_empty() {
823 out.push('-');
824 } else {
825 out.push_str(value);
826 }
827 out.push('\n');
828}
829
830fn parse_field(line: &str) -> String {
831 if line == "-" {
832 String::new()
833 } else {
834 line.to_owned()
835 }
836}
837
838impl DbRoot {
839 #[must_use]
841 pub fn encode(&self) -> Vec<u8> {
842 let mut out = format!(
843 "corium-root-v{}\n{}\n{}\n",
844 self.format_version, self.lease_version, self.index_basis_t
845 );
846 match &self.roots {
847 Some(roots) => {
848 for root in roots {
849 out.push_str(root.as_str());
850 out.push('\n');
851 }
852 }
853 None => out.push_str("-\n-\n-\n-\n"),
854 }
855 field_line(&mut out, &self.owner);
856 out.push_str(&self.lease_expires_unix_ms.to_string());
857 out.push('\n');
858 field_line(&mut out, &self.owner_endpoint);
859 out.push_str(&self.next_entity_id.to_string());
863 out.push('\n');
864 out.push_str(&self.last_tx_instant.to_string());
865 out.push('\n');
866 out.push_str(&self.key_manifest_version.to_string());
867 out.into_bytes()
868 }
869
870 #[must_use]
874 pub fn decode(bytes: &[u8]) -> Option<Self> {
875 let text = std::str::from_utf8(bytes).ok()?;
876 let mut lines = text.lines();
877 let first = lines.next()?;
878 let (format_version, lease_version) =
879 if let Some(version) = first.strip_prefix("corium-root-v") {
880 let format_version = version.parse().ok()?;
881 let lease_version = lines.next()?.parse().ok()?;
882 (format_version, lease_version)
883 } else {
884 (1, first.parse().ok()?)
887 };
888 let index_basis_t = lines.next()?.parse().ok()?;
889 let ids: Vec<&str> = lines.by_ref().take(4).collect();
890 if ids.len() != 4 {
891 return None;
892 }
893 let roots = if ids.iter().all(|id| *id == "-") {
894 None
895 } else {
896 Some([
897 BlobId::from_hex(ids[0])?,
898 BlobId::from_hex(ids[1])?,
899 BlobId::from_hex(ids[2])?,
900 BlobId::from_hex(ids[3])?,
901 ])
902 };
903 let owner = lines.next().map(parse_field).unwrap_or_default();
905 let lease_expires_unix_ms = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
906 let owner_endpoint = lines.next().map(parse_field).unwrap_or_default();
907 let next_entity_id = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
911 let last_tx_instant = lines
912 .next()
913 .and_then(|l| l.parse().ok())
914 .unwrap_or(i64::MIN);
915 let key_manifest_version = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
918 Some(Self {
919 format_version,
920 lease_version,
921 owner,
922 lease_expires_unix_ms,
923 owner_endpoint,
924 index_basis_t,
925 roots,
926 next_entity_id,
927 last_tx_instant,
928 key_manifest_version,
929 })
930 }
931}
932
933#[derive(Clone, Copy, Debug, Eq, PartialEq)]
935pub struct GcReport {
936 pub marked: usize,
938 pub swept: usize,
940 pub retained: usize,
942}
943
944pub async fn mark_and_sweep(
953 store: &dyn BlobStore,
954 live_roots: impl IntoIterator<Item = BlobId>,
955 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
956) -> Result<GcReport, StoreError> {
957 mark_and_sweep_retained(
958 store,
959 live_roots,
960 &mut children,
961 Duration::ZERO,
962 SystemTime::now(),
963 )
964 .await
965}
966
967pub async fn mark_and_sweep_retained(
973 store: &dyn BlobStore,
974 live_roots: impl IntoIterator<Item = BlobId>,
975 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
976 retention: Duration,
977 now: SystemTime,
978) -> Result<GcReport, StoreError> {
979 let mut marked = HashSet::new();
980 mark_reachable(store, live_roots, &mut children, &mut marked).await?;
981 sweep_unmarked(store, &marked, retention, now).await
982}
983
984pub async fn mark_reachable<S: std::hash::BuildHasher>(
996 store: &dyn BlobStore,
997 live_roots: impl IntoIterator<Item = BlobId>,
998 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
999 marked: &mut HashSet<BlobId, S>,
1000) -> Result<(), StoreError> {
1001 let mut pending = live_roots.into_iter().collect::<Vec<_>>();
1002 while let Some(id) = pending.pop() {
1003 if !marked.insert(id.clone()) {
1004 continue;
1005 }
1006 let bytes = store
1007 .get(&id)
1008 .await?
1009 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
1010 pending.extend(children(&id, &bytes)?);
1011 }
1012 Ok(())
1013}
1014
1015pub async fn sweep_unmarked<S: std::hash::BuildHasher>(
1020 store: &dyn BlobStore,
1021 marked: &HashSet<BlobId, S>,
1022 retention: Duration,
1023 now: SystemTime,
1024) -> Result<GcReport, StoreError> {
1025 let mut swept = 0;
1026 let mut retained = 0;
1027 let mut ids = store.list().await?;
1028 while let Some(id) = ids.next().await {
1029 let id = id?;
1030 if !marked.contains(&id) {
1031 let old_enough = retention.is_zero()
1035 || store.modified_at(&id).await?.is_some_and(|modified| {
1036 now.duration_since(modified).unwrap_or_default() >= retention
1037 });
1038 if old_enough {
1039 store.delete(&id).await?;
1040 swept += 1;
1041 } else {
1042 retained += 1;
1043 }
1044 }
1045 }
1046 Ok(GcReport {
1047 marked: marked.len(),
1048 swept,
1049 retained,
1050 })
1051}
1052
1053struct RootLock {
1054 file: File,
1055}
1056
1057impl RootLock {
1058 fn acquire(path: &Path) -> Result<Self, StoreError> {
1059 let file = OpenOptions::new()
1060 .read(true)
1061 .write(true)
1062 .create(true)
1063 .truncate(false)
1064 .open(path)?;
1065 file.lock_exclusive()?;
1066 Ok(Self { file })
1067 }
1068}
1069
1070impl Drop for RootLock {
1071 fn drop(&mut self) {
1072 let _ = FileExt::unlock(&self.file);
1073 }
1077}