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, 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}