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