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}