Skip to main content

reddb_server/storage/cache/blob/
config.rs

1use std::path::{Path, PathBuf};
2
3pub const DEFAULT_BLOB_L1_BYTES_MAX: usize = 256 * 1024 * 1024;
4pub const DEFAULT_BLOB_NAMESPACE_BYTES_MAX: usize = DEFAULT_BLOB_L1_BYTES_MAX;
5pub const DEFAULT_BLOB_L2_BYTES_MAX: u64 = 4 * 1024 * 1024 * 1024;
6pub const DEFAULT_BLOB_MAX_NAMESPACES: usize = 256;
7pub const DEFAULT_BLOB_SHARDS: usize = 64;
8pub const DEFAULT_CONTENT_METADATA_KEYS_MAX: usize = 32;
9pub const DEFAULT_CONTENT_METADATA_BYTES_MAX: usize = 4 * 1024;
10pub const METRIC_CACHE_BLOB_L1_BYTES_IN_USE: &str = "cache_blob_l1_bytes_in_use";
11pub const METRIC_CACHE_VERSION_MISMATCH_TOTAL: &str = "cache_version_mismatch_total";
12pub const METRIC_CACHE_BLOB_L2_BYTES_IN_USE: &str = "reddb_cache_blob_l2_bytes_in_use";
13pub const METRIC_CACHE_BLOB_L2_FULL_REJECTIONS_TOTAL: &str =
14    "reddb_cache_blob_l2_full_rejections_total";
15pub const METRIC_CACHE_BLOB_SYNOPSIS_METADATA_READS_TOTAL: &str =
16    "cache_blob_synopsis_metadata_reads_total";
17pub const METRIC_CACHE_BLOB_SYNOPSIS_BYTES: &str = "cache_blob_synopsis_bytes";
18
19/// Default per-namespace Bloom synopsis sizing target. The filter is sized
20/// for ~10K entries at ~1% false-positive rate by the canonical split-block
21/// Bloom primitive.
22pub const DEFAULT_BLOB_SYNOPSIS_CAPACITY: usize = 10_000;
23
24/// L2-to-L1 promotion topology.
25#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
26pub enum L2PromotionPolicy {
27    Immediate,
28    #[default]
29    OnSecondHit,
30}
31
32/// Switch for L2 zstd compression (issue #192, lane 2/5).
33///
34/// `On` (default) routes every L2 spill through [`L2BlobCompressor`]; payloads
35/// that fail the shrinkage gate or hit a precompressed-media content type are
36/// still stored raw, but the L2 entry header carries the v2 framing. `Off`
37/// skips the compress call entirely (CPU-saving), still emitting v2 framing
38/// with `tag=0` so the on-disk format stays uniform across modes.
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub enum L2Compression {
41    Off,
42    #[default]
43    On,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct BlobCacheConfig {
48    pub(super) l1_bytes_max: usize,
49    pub(super) namespace_bytes_max: usize,
50    pub(super) l2_bytes_max: u64,
51    pub(super) l2_path: Option<PathBuf>,
52    pub(super) max_namespaces: usize,
53    pub(super) shard_count: usize,
54    pub(super) content_metadata_keys_max: usize,
55    pub(super) content_metadata_bytes_max: usize,
56    pub(super) l2_compression: L2Compression,
57    pub(super) l2_promotion_policy: L2PromotionPolicy,
58}
59
60impl Default for BlobCacheConfig {
61    fn default() -> Self {
62        Self {
63            l1_bytes_max: DEFAULT_BLOB_L1_BYTES_MAX,
64            namespace_bytes_max: DEFAULT_BLOB_NAMESPACE_BYTES_MAX,
65            l2_bytes_max: DEFAULT_BLOB_L2_BYTES_MAX,
66            l2_path: None,
67            max_namespaces: DEFAULT_BLOB_MAX_NAMESPACES,
68            shard_count: DEFAULT_BLOB_SHARDS,
69            content_metadata_keys_max: DEFAULT_CONTENT_METADATA_KEYS_MAX,
70            content_metadata_bytes_max: DEFAULT_CONTENT_METADATA_BYTES_MAX,
71            l2_compression: L2Compression::default(),
72            l2_promotion_policy: L2PromotionPolicy::default(),
73        }
74    }
75}
76
77impl BlobCacheConfig {
78    /// Returns a fresh builder primed with the cache defaults.
79    ///
80    /// Prefer this over field literals — fields are private so future
81    /// additions (PRD stories #8–#10) do not break callers.
82    pub fn builder() -> BlobCacheConfigBuilder {
83        BlobCacheConfigBuilder::new()
84    }
85
86    pub fn with_l1_bytes_max(mut self, l1_bytes_max: usize) -> Self {
87        self.l1_bytes_max = l1_bytes_max;
88        self
89    }
90
91    pub fn with_l2_bytes_max(mut self, l2_bytes_max: u64) -> Self {
92        self.l2_bytes_max = l2_bytes_max;
93        self
94    }
95
96    pub fn with_namespace_bytes_max(mut self, namespace_bytes_max: usize) -> Self {
97        self.namespace_bytes_max = namespace_bytes_max;
98        self
99    }
100
101    pub fn with_l2_path(mut self, path: impl Into<PathBuf>) -> Self {
102        self.l2_path = Some(path.into());
103        self
104    }
105
106    pub fn with_max_namespaces(mut self, max_namespaces: usize) -> Self {
107        self.max_namespaces = max_namespaces;
108        self
109    }
110
111    pub fn with_shard_count(mut self, shard_count: usize) -> Self {
112        self.shard_count = shard_count.max(1);
113        self
114    }
115
116    pub fn with_content_metadata_limits(mut self, keys_max: usize, bytes_max: usize) -> Self {
117        self.content_metadata_keys_max = keys_max;
118        self.content_metadata_bytes_max = bytes_max;
119        self
120    }
121
122    pub fn with_l2_compression(mut self, compression: L2Compression) -> Self {
123        self.l2_compression = compression;
124        self
125    }
126
127    pub fn with_l2_promotion_policy(mut self, policy: L2PromotionPolicy) -> Self {
128        self.l2_promotion_policy = policy;
129        self
130    }
131
132    pub fn l1_bytes_max(&self) -> usize {
133        self.l1_bytes_max
134    }
135
136    pub fn l2_bytes_max(&self) -> u64 {
137        self.l2_bytes_max
138    }
139
140    pub fn namespace_bytes_max(&self) -> usize {
141        self.namespace_bytes_max
142    }
143
144    pub fn l2_path(&self) -> Option<&Path> {
145        self.l2_path.as_deref()
146    }
147
148    pub fn max_namespaces(&self) -> usize {
149        self.max_namespaces
150    }
151
152    pub fn shard_count(&self) -> usize {
153        self.shard_count
154    }
155
156    pub fn content_metadata_keys_max(&self) -> usize {
157        self.content_metadata_keys_max
158    }
159
160    pub fn content_metadata_bytes_max(&self) -> usize {
161        self.content_metadata_bytes_max
162    }
163
164    pub fn l2_compression(&self) -> L2Compression {
165        self.l2_compression
166    }
167
168    pub fn l2_promotion_policy(&self) -> L2PromotionPolicy {
169        self.l2_promotion_policy
170    }
171}
172
173/// Builder for [`BlobCacheConfig`].
174///
175/// Created via [`BlobCacheConfig::builder`]. Each setter validates its
176/// argument; invalid configurations are rejected at [`build`](Self::build).
177#[derive(Debug, Clone)]
178pub struct BlobCacheConfigBuilder {
179    inner: BlobCacheConfig,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum BlobCacheConfigError {
184    /// `shard_count` must be at least 1.
185    ZeroShardCount,
186    /// `max_namespaces` must be at least 1.
187    ZeroMaxNamespaces,
188    /// `namespace_bytes_max` must be at least 1.
189    ZeroNamespaceBytesMax,
190}
191
192impl BlobCacheConfigBuilder {
193    fn new() -> Self {
194        Self {
195            inner: BlobCacheConfig::default(),
196        }
197    }
198
199    pub fn l1_bytes_max(mut self, value: usize) -> Self {
200        self.inner.l1_bytes_max = value;
201        self
202    }
203
204    pub fn l2_bytes_max(mut self, value: u64) -> Self {
205        self.inner.l2_bytes_max = value;
206        self
207    }
208
209    pub fn namespace_bytes_max(mut self, value: usize) -> Self {
210        self.inner.namespace_bytes_max = value;
211        self
212    }
213
214    pub fn l2_path(mut self, path: impl Into<PathBuf>) -> Self {
215        self.inner.l2_path = Some(path.into());
216        self
217    }
218
219    pub fn max_namespaces(mut self, value: usize) -> Self {
220        self.inner.max_namespaces = value;
221        self
222    }
223
224    pub fn shard_count(mut self, value: usize) -> Self {
225        self.inner.shard_count = value;
226        self
227    }
228
229    pub fn content_metadata_keys_max(mut self, value: usize) -> Self {
230        self.inner.content_metadata_keys_max = value;
231        self
232    }
233
234    pub fn content_metadata_bytes_max(mut self, value: usize) -> Self {
235        self.inner.content_metadata_bytes_max = value;
236        self
237    }
238
239    pub fn l2_compression(mut self, value: L2Compression) -> Self {
240        self.inner.l2_compression = value;
241        self
242    }
243
244    pub fn l2_promotion_policy(mut self, value: L2PromotionPolicy) -> Self {
245        self.inner.l2_promotion_policy = value;
246        self
247    }
248
249    pub fn try_build(self) -> Result<BlobCacheConfig, BlobCacheConfigError> {
250        if self.inner.shard_count == 0 {
251            return Err(BlobCacheConfigError::ZeroShardCount);
252        }
253        if self.inner.max_namespaces == 0 {
254            return Err(BlobCacheConfigError::ZeroMaxNamespaces);
255        }
256        if self.inner.namespace_bytes_max == 0 {
257            return Err(BlobCacheConfigError::ZeroNamespaceBytesMax);
258        }
259        Ok(self.inner)
260    }
261
262    /// Convenience wrapper around [`try_build`](Self::try_build) that
263    /// panics on invalid input. Tests and bootstrap code should prefer this.
264    pub fn build(self) -> BlobCacheConfig {
265        self.try_build().expect("blob cache config")
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    #[test]
274    fn blob_cache_config_builder_rejects_zero_shard_count() {
275        let err = BlobCacheConfig::builder()
276            .shard_count(0)
277            .try_build()
278            .expect_err("zero shard count must be rejected");
279        assert_eq!(err, BlobCacheConfigError::ZeroShardCount);
280    }
281
282    #[test]
283    fn blob_cache_config_builder_rejects_zero_max_namespaces() {
284        let err = BlobCacheConfig::builder()
285            .max_namespaces(0)
286            .try_build()
287            .expect_err("zero max_namespaces must be rejected");
288        assert_eq!(err, BlobCacheConfigError::ZeroMaxNamespaces);
289    }
290
291    #[test]
292    fn blob_cache_config_builder_rejects_zero_namespace_budget() {
293        let err = BlobCacheConfig::builder()
294            .namespace_bytes_max(0)
295            .try_build()
296            .expect_err("zero namespace_bytes_max must be rejected");
297        assert_eq!(err, BlobCacheConfigError::ZeroNamespaceBytesMax);
298    }
299}