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 snapshot;
26pub use snapshot::{
27 INDEX_MANIFEST_MAGIC, chunk_segment_keys, decode_index_manifest, decode_segment_keys,
28 encode_index_manifest, index_blob_children, is_index_manifest,
29};
30
31#[cfg(feature = "postgres")]
32mod postgres_store;
33#[cfg(feature = "postgres")]
34pub use postgres_store::PostgresBlobStore;
35#[cfg(feature = "turso")]
36mod turso_store;
37#[cfg(feature = "turso")]
38pub use turso_store::TursoBlobStore;
39#[cfg(feature = "s3")]
40mod s3_store;
41#[cfg(feature = "s3")]
42pub use s3_store::S3BlobStore;
43
44#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
46pub struct BlobId(String);
47
48impl BlobId {
49 #[must_use]
51 pub fn as_str(&self) -> &str {
52 &self.0
53 }
54
55 #[must_use]
57 pub fn from_hex(text: &str) -> Option<Self> {
58 (text.len() == 64 && text.bytes().all(|byte| byte.is_ascii_hexdigit()))
59 .then(|| Self(text.to_owned()))
60 }
61}
62
63impl fmt::Display for BlobId {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 f.write_str(&self.0)
66 }
67}
68
69#[derive(Debug, Error)]
71pub enum StoreError {
72 #[error("store I/O failed: {0}")]
74 Io(#[from] io::Error),
75 #[error("root CAS failed: expected {expected:?}, actual {actual:?}")]
77 CasFailed {
78 expected: Option<Vec<u8>>,
80 actual: Option<Vec<u8>>,
82 },
83 #[error("blob content did not match digest {0}")]
85 CorruptBlob(BlobId),
86 #[error("reachable blob is missing: {0}")]
88 MissingBlob(BlobId),
89 #[error("invalid root name {0:?}")]
91 InvalidRootName(String),
92 #[error("store blocking task failed: {0}")]
94 BlockingTask(String),
95 #[cfg(feature = "postgres")]
97 #[error("PostgreSQL store failed: {0}")]
98 Postgres(#[from] deadpool_postgres::tokio_postgres::Error),
99 #[cfg(feature = "postgres")]
101 #[error("PostgreSQL connection pool failed: {0}")]
102 PostgresPool(#[from] deadpool_postgres::PoolError),
103 #[cfg(feature = "postgres")]
105 #[error("PostgreSQL connection pool configuration failed: {0}")]
106 PostgresPoolCreate(#[from] deadpool_postgres::CreatePoolError),
107 #[cfg(feature = "postgres")]
109 #[error("cannot load native certificate roots for PostgreSQL TLS: {0}")]
110 PostgresTlsRoots(String),
111 #[cfg(feature = "postgres")]
113 #[error("PostgreSQL store contains invalid data: {0}")]
114 InvalidPostgresData(String),
115 #[cfg(feature = "turso")]
117 #[error("Turso blob store failed: {0}")]
118 Turso(#[from] turso::Error),
119 #[cfg(feature = "turso")]
121 #[error("Turso database path is not valid UTF-8: {0:?}")]
122 InvalidTursoPath(PathBuf),
123 #[cfg(feature = "turso")]
125 #[error("Turso blob store contains invalid data: {0}")]
126 InvalidTursoData(String),
127 #[cfg(feature = "s3")]
129 #[error("S3 store failed: {0}")]
130 S3(String),
131 #[cfg(feature = "s3")]
133 #[error("S3 store contains invalid data: {0}")]
134 InvalidS3Data(String),
135}
136
137#[cfg(feature = "s3")]
146impl<E> From<aws_sdk_s3::error::SdkError<E>> for StoreError
147where
148 E: std::error::Error + Send + Sync + 'static,
149{
150 fn from(error: aws_sdk_s3::error::SdkError<E>) -> Self {
151 StoreError::S3(aws_sdk_s3::error::DisplayErrorContext(error).to_string())
152 }
153}
154
155pub type BlobIdStream = Pin<Box<dyn Stream<Item = Result<BlobId, StoreError>> + Send + 'static>>;
157
158async fn run_blocking<T>(
159 operation: impl FnOnce() -> Result<T, StoreError> + Send + 'static,
160) -> Result<T, StoreError>
161where
162 T: Send + 'static,
163{
164 tokio::task::spawn_blocking(operation)
165 .await
166 .map_err(|error| StoreError::BlockingTask(error.to_string()))?
167}
168
169#[async_trait]
171pub trait BlobStore: Send + Sync {
172 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError>;
178 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError>;
184 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
190 Ok(self.get(id).await?.is_some())
191 }
192 async fn put_if_absent(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
199 let id = digest(bytes);
200 if self.contains(&id).await? {
201 Ok(id)
202 } else {
203 self.put(bytes).await
204 }
205 }
206 async fn delete(&self, id: &BlobId) -> Result<(), StoreError>;
212 async fn list(&self) -> Result<BlobIdStream, StoreError>;
218 async fn modified_at(&self, _id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
225 Ok(None)
226 }
227}
228
229#[async_trait]
231pub trait RootStore: Send + Sync {
232 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError>;
238 async fn cas_root(
244 &self,
245 name: &str,
246 expected: Option<&[u8]>,
247 new: &[u8],
248 ) -> Result<(), StoreError>;
249 async fn delete_root(&self, name: &str) -> Result<(), StoreError>;
255 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError>;
261}
262
263#[derive(Clone, Default)]
265pub struct MemoryStore {
266 inner: Arc<RwLock<MemoryInner>>,
267}
268#[derive(Default)]
269struct MemoryInner {
270 blobs: HashMap<BlobId, Vec<u8>>,
271 roots: BTreeMap<String, Vec<u8>>,
272}
273
274#[async_trait]
275impl BlobStore for MemoryStore {
276 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
277 let inner = Arc::clone(&self.inner);
278 let bytes = bytes.to_vec();
279 run_blocking(move || {
280 let id = digest(&bytes);
281 inner
282 .write()
283 .expect("poisoned store lock")
284 .blobs
285 .insert(id.clone(), bytes);
286 Ok(id)
287 })
288 .await
289 }
290 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
291 let inner = Arc::clone(&self.inner);
292 let id = id.clone();
293 run_blocking(move || {
294 Ok(inner
295 .read()
296 .expect("poisoned store lock")
297 .blobs
298 .get(&id)
299 .cloned())
300 })
301 .await
302 }
303 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
304 let inner = Arc::clone(&self.inner);
305 let id = id.clone();
306 run_blocking(move || {
307 inner
308 .write()
309 .expect("poisoned store lock")
310 .blobs
311 .remove(&id);
312 Ok(())
313 })
314 .await
315 }
316 async fn list(&self) -> Result<BlobIdStream, StoreError> {
317 let inner = Arc::clone(&self.inner);
318 let ids = run_blocking(move || {
319 Ok(inner
320 .read()
321 .expect("poisoned store lock")
322 .blobs
323 .keys()
324 .cloned()
325 .collect::<Vec<_>>())
326 })
327 .await?;
328 Ok(Box::pin(tokio_stream::iter(ids.into_iter().map(Ok))))
329 }
330}
331#[async_trait]
332impl RootStore for MemoryStore {
333 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
334 let inner = Arc::clone(&self.inner);
335 let name = name.to_owned();
336 run_blocking(move || {
337 Ok(inner
338 .read()
339 .expect("poisoned store lock")
340 .roots
341 .get(&name)
342 .cloned())
343 })
344 .await
345 }
346 async fn cas_root(
347 &self,
348 name: &str,
349 expected: Option<&[u8]>,
350 new: &[u8],
351 ) -> Result<(), StoreError> {
352 let inner = Arc::clone(&self.inner);
353 let name = name.to_owned();
354 let expected = expected.map(<[u8]>::to_vec);
355 let new = new.to_vec();
356 run_blocking(move || {
357 let mut inner = inner.write().expect("poisoned store lock");
358 let actual = inner.roots.get(&name).cloned();
359 if actual != expected {
360 return Err(StoreError::CasFailed { expected, actual });
361 }
362 inner.roots.insert(name, new);
363 Ok(())
364 })
365 .await
366 }
367 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
368 let inner = Arc::clone(&self.inner);
369 let name = name.to_owned();
370 run_blocking(move || {
371 inner
372 .write()
373 .expect("poisoned store lock")
374 .roots
375 .remove(&name);
376 Ok(())
377 })
378 .await
379 }
380 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
381 let inner = Arc::clone(&self.inner);
382 let prefix = prefix.to_owned();
383 run_blocking(move || {
384 Ok(inner
385 .read()
386 .expect("poisoned store lock")
387 .roots
388 .keys()
389 .filter(|name| name.starts_with(&prefix))
390 .cloned()
391 .collect())
392 })
393 .await
394 }
395}
396
397#[derive(Clone)]
399pub struct FsStore {
400 root: PathBuf,
401}
402impl FsStore {
403 pub fn open(root: impl AsRef<Path>) -> Result<Self, StoreError> {
409 let root = root.as_ref().to_path_buf();
410 fs::create_dir_all(root.join("blobs"))?;
411 fs::create_dir_all(root.join("roots"))?;
412 Ok(Self { root })
413 }
414 fn blob_path(&self, id: &BlobId) -> PathBuf {
415 self.root.join("blobs").join(id.as_str())
416 }
417 fn root_path(&self, name: &str) -> Result<PathBuf, StoreError> {
418 if name.is_empty()
419 || name == "."
420 || name == ".."
421 || name.contains('/')
422 || name.contains('\\')
423 {
424 return Err(StoreError::InvalidRootName(name.to_owned()));
425 }
426 Ok(self.root.join("roots").join(name))
427 }
428
429 fn root_lock(&self, name: &str) -> Result<RootLock, StoreError> {
430 let root_path = self.root_path(name)?;
431 RootLock::acquire(&root_path.with_extension("lock"))
432 }
433}
434#[async_trait]
435impl BlobStore for FsStore {
436 async fn put(&self, bytes: &[u8]) -> Result<BlobId, StoreError> {
437 let store = self.clone();
438 let bytes = bytes.to_vec();
439 run_blocking(move || {
440 let id = digest(&bytes);
441 let path = store.blob_path(&id);
442 if !path.exists() {
443 let tmp = path.with_extension("tmp");
444 fs::write(&tmp, &bytes)?;
445 fs::rename(tmp, path)?;
446 }
447 Ok(id)
448 })
449 .await
450 }
451 async fn get(&self, id: &BlobId) -> Result<Option<Vec<u8>>, StoreError> {
452 let store = self.clone();
453 let id = id.clone();
454 run_blocking(move || {
455 let path = store.blob_path(&id);
456 if !path.exists() {
457 return Ok(None);
458 }
459 let bytes = fs::read(path)?;
460 if digest(&bytes) != id {
461 return Err(StoreError::CorruptBlob(id));
462 }
463 Ok(Some(bytes))
464 })
465 .await
466 }
467 async fn contains(&self, id: &BlobId) -> Result<bool, StoreError> {
468 let store = self.clone();
469 let id = id.clone();
470 run_blocking(move || Ok(store.blob_path(&id).is_file())).await
471 }
472 async fn delete(&self, id: &BlobId) -> Result<(), StoreError> {
473 let store = self.clone();
474 let id = id.clone();
475 run_blocking(move || match fs::remove_file(store.blob_path(&id)) {
476 Ok(()) => Ok(()),
477 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
478 Err(error) => Err(error.into()),
479 })
480 .await
481 }
482 async fn list(&self) -> Result<BlobIdStream, StoreError> {
483 let path = self.root.join("blobs");
484 let entries = run_blocking(move || Ok(fs::read_dir(path)?)).await?;
485 let (tx, rx) = tokio::sync::mpsc::channel(64);
486 let failure_tx = tx.clone();
487 tokio::spawn(async move {
488 let result = tokio::task::spawn_blocking(move || {
489 for entry in entries {
490 let id = (|| {
491 let entry = entry?;
492 if !entry.file_type()?.is_file() {
493 return Ok(None);
494 }
495 Ok(entry.file_name().to_str().and_then(BlobId::from_hex))
496 })();
497 match id {
498 Ok(Some(id)) => {
499 if tx.blocking_send(Ok(id)).is_err() {
500 return;
501 }
502 }
503 Ok(None) => {}
504 Err(error) => {
505 let _ = tx.blocking_send(Err(StoreError::Io(error)));
506 return;
507 }
508 }
509 }
510 })
511 .await;
512 if let Err(error) = result {
513 let _ = failure_tx
514 .send(Err(StoreError::BlockingTask(error.to_string())))
515 .await;
516 }
517 });
518 Ok(Box::pin(ReceiverStream::new(rx)))
519 }
520 async fn modified_at(&self, id: &BlobId) -> Result<Option<SystemTime>, StoreError> {
521 let store = self.clone();
522 let id = id.clone();
523 run_blocking(move || match fs::metadata(store.blob_path(&id)) {
524 Ok(metadata) => Ok(Some(metadata.modified()?)),
525 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
526 Err(error) => Err(error.into()),
527 })
528 .await
529 }
530}
531#[async_trait]
532impl RootStore for FsStore {
533 async fn get_root(&self, name: &str) -> Result<Option<Vec<u8>>, StoreError> {
534 let store = self.clone();
535 let name = name.to_owned();
536 run_blocking(move || match fs::read(store.root_path(&name)?) {
537 Ok(value) => Ok(Some(value)),
538 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
539 Err(error) => Err(error.into()),
540 })
541 .await
542 }
543 async fn cas_root(
544 &self,
545 name: &str,
546 expected: Option<&[u8]>,
547 new: &[u8],
548 ) -> Result<(), StoreError> {
549 let store = self.clone();
550 let name = name.to_owned();
551 let expected = expected.map(<[u8]>::to_vec);
552 let new = new.to_vec();
553 run_blocking(move || {
554 let _lock = store.root_lock(&name)?;
555 let path = store.root_path(&name)?;
556 let actual = match fs::read(&path) {
557 Ok(value) => Some(value),
558 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
559 Err(error) => return Err(error.into()),
560 };
561 if actual != expected {
562 return Err(StoreError::CasFailed { expected, actual });
563 }
564 let tmp = path.with_extension("tmp");
565 fs::write(&tmp, new)?;
566 fs::rename(tmp, path)?;
567 Ok(())
568 })
569 .await
570 }
571 async fn delete_root(&self, name: &str) -> Result<(), StoreError> {
572 let store = self.clone();
573 let name = name.to_owned();
574 run_blocking(move || {
575 let _lock = store.root_lock(&name)?;
576 match fs::remove_file(store.root_path(&name)?) {
577 Ok(()) => Ok(()),
578 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
579 Err(error) => Err(error.into()),
580 }
581 })
582 .await
583 }
584 async fn list_roots(&self, prefix: &str) -> Result<Vec<String>, StoreError> {
585 let root = self.root.clone();
586 let prefix = prefix.to_owned();
587 run_blocking(move || {
588 let mut names = Vec::new();
589 for entry in fs::read_dir(root.join("roots"))? {
590 let entry = entry?;
591 if !entry.file_type()?.is_file() {
592 continue;
593 }
594 if let Some(name) = entry.file_name().to_str() {
595 let auxiliary = Path::new(name).extension().is_some_and(|ext| {
596 ext.eq_ignore_ascii_case("lock") || ext.eq_ignore_ascii_case("tmp")
597 });
598 if name.starts_with(&prefix) && !auxiliary {
599 names.push(name.to_owned());
600 }
601 }
602 }
603 names.sort();
604 Ok(names)
605 })
606 .await
607 }
608}
609
610#[must_use]
612pub fn digest(bytes: &[u8]) -> BlobId {
613 BlobId(blake3::hash(bytes).to_hex().to_string())
614}
615
616#[must_use]
618pub fn db_root_name(db: &str) -> String {
619 format!("db:{db}")
620}
621
622#[must_use]
624pub fn meta_root_name(db: &str) -> String {
625 format!("meta:{db}")
626}
627
628pub const FORMAT_VERSION: u32 = 3;
640
641#[derive(Clone, Debug, Eq, PartialEq)]
650pub struct DbRoot {
651 pub format_version: u32,
653 pub lease_version: u64,
655 pub owner: String,
658 pub lease_expires_unix_ms: i64,
660 pub owner_endpoint: String,
663 pub index_basis_t: u64,
665 pub roots: Option<[BlobId; 4]>,
668 pub next_entity_id: u64,
678 pub last_tx_instant: i64,
685}
686
687fn field_line(out: &mut String, value: &str) {
689 if value.is_empty() {
690 out.push('-');
691 } else {
692 out.push_str(value);
693 }
694 out.push('\n');
695}
696
697fn parse_field(line: &str) -> String {
698 if line == "-" {
699 String::new()
700 } else {
701 line.to_owned()
702 }
703}
704
705impl DbRoot {
706 #[must_use]
708 pub fn encode(&self) -> Vec<u8> {
709 let mut out = format!(
710 "corium-root-v{}\n{}\n{}\n",
711 self.format_version, self.lease_version, self.index_basis_t
712 );
713 match &self.roots {
714 Some(roots) => {
715 for root in roots {
716 out.push_str(root.as_str());
717 out.push('\n');
718 }
719 }
720 None => out.push_str("-\n-\n-\n-\n"),
721 }
722 field_line(&mut out, &self.owner);
723 out.push_str(&self.lease_expires_unix_ms.to_string());
724 out.push('\n');
725 field_line(&mut out, &self.owner_endpoint);
726 out.push_str(&self.next_entity_id.to_string());
730 out.push('\n');
731 out.push_str(&self.last_tx_instant.to_string());
732 out.into_bytes()
733 }
734
735 #[must_use]
739 pub fn decode(bytes: &[u8]) -> Option<Self> {
740 let text = std::str::from_utf8(bytes).ok()?;
741 let mut lines = text.lines();
742 let first = lines.next()?;
743 let (format_version, lease_version) =
744 if let Some(version) = first.strip_prefix("corium-root-v") {
745 let format_version = version.parse().ok()?;
746 let lease_version = lines.next()?.parse().ok()?;
747 (format_version, lease_version)
748 } else {
749 (1, first.parse().ok()?)
752 };
753 let index_basis_t = lines.next()?.parse().ok()?;
754 let ids: Vec<&str> = lines.by_ref().take(4).collect();
755 if ids.len() != 4 {
756 return None;
757 }
758 let roots = if ids.iter().all(|id| *id == "-") {
759 None
760 } else {
761 Some([
762 BlobId::from_hex(ids[0])?,
763 BlobId::from_hex(ids[1])?,
764 BlobId::from_hex(ids[2])?,
765 BlobId::from_hex(ids[3])?,
766 ])
767 };
768 let owner = lines.next().map(parse_field).unwrap_or_default();
770 let lease_expires_unix_ms = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
771 let owner_endpoint = lines.next().map(parse_field).unwrap_or_default();
772 let next_entity_id = lines.next().and_then(|l| l.parse().ok()).unwrap_or(0);
776 let last_tx_instant = lines
777 .next()
778 .and_then(|l| l.parse().ok())
779 .unwrap_or(i64::MIN);
780 Some(Self {
781 format_version,
782 lease_version,
783 owner,
784 lease_expires_unix_ms,
785 owner_endpoint,
786 index_basis_t,
787 roots,
788 next_entity_id,
789 last_tx_instant,
790 })
791 }
792}
793
794#[derive(Clone, Copy, Debug, Eq, PartialEq)]
796pub struct GcReport {
797 pub marked: usize,
799 pub swept: usize,
801 pub retained: usize,
803}
804
805pub async fn mark_and_sweep(
814 store: &dyn BlobStore,
815 live_roots: impl IntoIterator<Item = BlobId>,
816 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
817) -> Result<GcReport, StoreError> {
818 mark_and_sweep_retained(
819 store,
820 live_roots,
821 &mut children,
822 Duration::ZERO,
823 SystemTime::now(),
824 )
825 .await
826}
827
828pub async fn mark_and_sweep_retained(
834 store: &dyn BlobStore,
835 live_roots: impl IntoIterator<Item = BlobId>,
836 mut children: impl FnMut(&BlobId, &[u8]) -> Result<Vec<BlobId>, StoreError>,
837 retention: Duration,
838 now: SystemTime,
839) -> Result<GcReport, StoreError> {
840 let mut marked = HashSet::new();
841 let mut pending = live_roots.into_iter().collect::<Vec<_>>();
842 while let Some(id) = pending.pop() {
843 if !marked.insert(id.clone()) {
844 continue;
845 }
846 let bytes = store
847 .get(&id)
848 .await?
849 .ok_or_else(|| StoreError::MissingBlob(id.clone()))?;
850 pending.extend(children(&id, &bytes)?);
851 }
852
853 let mut swept = 0;
854 let mut retained = 0;
855 let mut ids = store.list().await?;
856 while let Some(id) = ids.next().await {
857 let id = id?;
858 if !marked.contains(&id) {
859 let old_enough = retention.is_zero()
863 || store.modified_at(&id).await?.is_some_and(|modified| {
864 now.duration_since(modified).unwrap_or_default() >= retention
865 });
866 if old_enough {
867 store.delete(&id).await?;
868 swept += 1;
869 } else {
870 retained += 1;
871 }
872 }
873 }
874 Ok(GcReport {
875 marked: marked.len(),
876 swept,
877 retained,
878 })
879}
880
881struct RootLock {
882 file: File,
883}
884
885impl RootLock {
886 fn acquire(path: &Path) -> Result<Self, StoreError> {
887 let file = OpenOptions::new()
888 .read(true)
889 .write(true)
890 .create(true)
891 .truncate(false)
892 .open(path)?;
893 file.lock_exclusive()?;
894 Ok(Self { file })
895 }
896}
897
898impl Drop for RootLock {
899 fn drop(&mut self) {
900 let _ = FileExt::unlock(&self.file);
901 }
905}