sui_castore/lib.rs
1//! Content-addressed store backends for sui.
2//!
3//! This crate owns the [`StorageBackend`] trait and every concrete
4//! implementation (Local / S3 / Redis / Postgres / Tiered), plus the
5//! [`BackendConfig`] config-select enum and the [`build_backend`] factory.
6//! Both `sui-cache` (the Nix binary-cache server) and `sui-registry` (the OCI
7//! registry `porto`) depend on this crate; neither depends on the other.
8//!
9//! # Feature flags
10//!
11//! - **`redis-client`** — enables the production [`RedisConnectionManager`]
12//! transport. Without it the Redis arm of [`build_backend`] returns a typed
13//! [`StoreError::NotImplemented`].
14//! - **`postgres`** — enables the production [`SqlxPgCacheConn`] transport.
15//! Without it the Postgres arm returns [`StoreError::NotImplemented`].
16
17pub mod config;
18pub mod env_expand;
19pub mod storage;
20
21pub use config::BackendConfig;
22pub use env_expand::{expand_env_vars, ExpandEnvError};
23pub use storage::{
24 advertised_nar_url, advertised_url_line, build_backend, bytes_stream, collect_nar,
25 empty_stream, file_stream, is_addressable_nar_path, referrer_of, spool_or_buffer,
26 whole_value_stream, BytesNarSource,
27 FileNarSource, LocalStorage, MemNarRefIndex, NarRefIndex, NarRefKey, NarRefScan, NarResidency,
28 NarSource, NarStream, PgCacheConn, PgStorageBackend, PgTable, RedisBackend, RedisConn,
29 S3Storage, SpooledNarSource, StorageBackend, StorageIndex, TieredBackend, TieredTier,
30 WritePolicy, DEFAULT_INGEST_MEMORY_CAP, NAR_CHUNK_BYTES, NAR_REF_PREFIX, TIERED_BACKEND_TIER,
31};
32
33#[cfg(feature = "redis-client")]
34pub use storage::RedisConnectionManager;
35
36#[cfg(feature = "postgres")]
37pub use storage::SqlxPgCacheConn;
38
39/// Error type for content-addressed store operations.
40#[derive(Debug, thiserror::Error)]
41pub enum StoreError {
42 /// An I/O operation failed.
43 #[error("io error: {0}")]
44 Io(#[from] std::io::Error),
45
46 /// A store path was not found on the local filesystem.
47 #[error("path not found: {0}")]
48 PathNotFound(String),
49
50 /// A signing or verification operation failed.
51 #[error("signing error: {0}")]
52 Signing(String),
53
54 /// A feature is not yet implemented (missing Cargo feature flag).
55 #[error("not implemented: {0}")]
56 NotImplemented(&'static str),
57
58 /// A narinfo could not be parsed or was invalid UTF-8.
59 #[error("narinfo error: {0}")]
60 NarInfo(String),
61
62 /// A value exceeded a tier's configured byte cap and was **refused**
63 /// rather than buffered.
64 ///
65 /// This is a *bound*, not a failure of the cache: the refusing tier is
66 /// always a best-effort hot tier, and the durable tiers below it stream the
67 /// same content without a cap. Refusing is the whole point — a tier that
68 /// buffers whatever it is handed is exactly how a 6 GiB pod is killed by one
69 /// large NAR.
70 ///
71 /// `at_least` is a lower bound, not the true size: collection stops the
72 /// moment the cap is crossed, so the rest of the value is never read.
73 #[error("value too large: refused at {at_least}+ bytes against a {limit}-byte cap")]
74 TooLarge {
75 /// The tier's configured cap, in bytes.
76 limit: u64,
77 /// A lower bound on the value's size, in bytes.
78 at_least: u64,
79 },
80
81 /// The backend's schema is **absent** — its tables do not exist (e.g. a
82 /// durable tier came back up on a fresh volume, or its database was
83 /// dropped/recreated under a live connection pool).
84 ///
85 /// Kept as its own variant, distinct from [`Io`](StoreError::Io), precisely
86 /// because it is **self-healable**: the owning backend's DDL is idempotent
87 /// (`CREATE TABLE IF NOT EXISTS`), so a consumer that sees this can re-run
88 /// it and retry rather than failing the request. A backend that cannot
89 /// re-create its own schema must return `Io` instead — never round a
90 /// permanent failure up into a healable one.
91 #[error("schema missing: {0}")]
92 SchemaMissing(String),
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn store_error_display_path_not_found() {
101 let e = StoreError::PathNotFound("/nix/store/abc".to_string());
102 assert!(format!("{e}").contains("/nix/store/abc"));
103 }
104
105 #[test]
106 fn store_error_display_io() {
107 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing");
108 let e = StoreError::Io(io_err);
109 assert!(format!("{e}").contains("missing"));
110 }
111
112 #[test]
113 fn store_error_display_signing() {
114 let e = StoreError::Signing("bad key".to_string());
115 assert!(format!("{e}").contains("bad key"));
116 }
117
118 #[test]
119 fn store_error_display_not_implemented() {
120 let e = StoreError::NotImplemented("redis L1");
121 assert!(format!("{e}").contains("redis L1"));
122 }
123
124 #[test]
125 fn store_error_display_narinfo() {
126 let e = StoreError::NarInfo("parse failed".to_string());
127 assert!(format!("{e}").contains("parse failed"));
128 }
129}