Skip to main content

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