reddb_server/storage/cache/
mod.rs1pub mod aggregates;
29pub mod bgwriter;
30pub mod blob;
31pub mod compressor;
32pub mod extended_ttl;
33pub mod promotion_pool;
34pub mod result;
35pub mod spill;
36pub mod sweeper;
37
38pub use aggregates::{AggCacheStats, AggValue, AggregationCache, CardinalityEstimate, NumericAgg};
39pub use blob::{
40 BlobCache, BlobCacheConfig, BlobCacheHit, BlobCachePolicy, BlobCachePut, BlobCacheStats,
41 CacheError, L1Admission, L2Compression, DEFAULT_BLOB_L1_BYTES_MAX, DEFAULT_BLOB_L2_BYTES_MAX,
42 DEFAULT_BLOB_MAX_NAMESPACES, METRIC_CACHE_BLOB_L1_BYTES_IN_USE,
43 METRIC_CACHE_BLOB_L2_BYTES_IN_USE, METRIC_CACHE_BLOB_L2_FULL_REJECTIONS_TOTAL,
44 METRIC_CACHE_VERSION_MISMATCH_TOTAL,
45};
46pub use compressor::{CompressError, CompressOpts, Compressed, L2BlobCompressor};
47pub use extended_ttl::{EffectiveExpiry, ExpiryDecision, ExtendedTtlPolicy};
48pub use promotion_pool::{
49 AsyncPromotionPool, PoolOpts, PromotionExecutor, PromotionMetrics, PromotionRequest,
50 ScheduleOutcome,
51};
52pub use result::{
53 CacheKey, CachePolicy, MaterializedViewCache, MaterializedViewDef, RefreshPolicy, ResultCache,
54 ResultCacheStats,
55};
56pub use spill::{SpillConfig, SpillError, SpillManager, SpillStats, SpillableGraph};
57
58pub fn archive_blob_cache_l2(
92 backend: &dyn crate::storage::backend::RemoteBackend,
93 l2_path: &std::path::Path,
94 prefix: &str,
95) -> Result<usize, crate::storage::backend::BackendError> {
96 let mut count = 0usize;
97 if l2_path.is_file() {
98 backend.upload(l2_path, &reddb_file::blob_cache_l2_backup_pager_key(prefix))?;
99 count += 1;
100 }
101 let control = reddb_file::blob_cache_control_path(l2_path);
102 if control.is_file() {
103 backend.upload(
104 &control,
105 &reddb_file::blob_cache_l2_backup_control_key(prefix),
106 )?;
107 count += 1;
108 }
109 Ok(count)
110}
111
112pub fn restore_blob_cache_l2(
121 backend: &dyn crate::storage::backend::RemoteBackend,
122 prefix: &str,
123 l2_path: &std::path::Path,
124) -> Result<usize, crate::storage::backend::BackendError> {
125 if let Some(parent) = l2_path.parent() {
126 if !parent.as_os_str().is_empty() {
127 std::fs::create_dir_all(parent)
128 .map_err(|err| crate::storage::backend::BackendError::Transport(err.to_string()))?;
129 }
130 }
131 let mut count = 0usize;
132 if backend.download(&reddb_file::blob_cache_l2_backup_pager_key(prefix), l2_path)? {
133 count += 1;
134 }
135 let control = reddb_file::blob_cache_control_path(l2_path);
136 if backend.download(
137 &reddb_file::blob_cache_l2_backup_control_key(prefix),
138 &control,
139 )? {
140 count += 1;
141 }
142 Ok(count)
143}
144
145#[cfg(test)]
146mod backup_helpers_tests {
147 use super::*;
148 use crate::storage::backend::LocalBackend;
149 use std::sync::atomic::{AtomicU64, Ordering};
150
151 fn write_file(path: &std::path::Path, bytes: &[u8]) {
152 if let Some(parent) = path.parent() {
153 std::fs::create_dir_all(parent).unwrap();
154 }
155 std::fs::write(path, bytes).unwrap();
156 }
157
158 static SCRATCH_COUNTER: AtomicU64 = AtomicU64::new(0);
164 fn scratch(label: &str) -> std::path::PathBuf {
165 let pid = std::process::id();
166 let n = SCRATCH_COUNTER.fetch_add(1, Ordering::SeqCst);
167 let p = std::env::temp_dir().join(format!("reddb-blobcache-bk-{label}-{pid}-{n}"));
168 let _ = std::fs::remove_dir_all(&p);
169 std::fs::create_dir_all(&p).unwrap();
170 p
171 }
172
173 #[test]
179 fn archive_then_restore_round_trips_l2_pager_and_control_files() {
180 let scratch_dir = scratch("pair-src");
181 let l2_src = scratch_dir.join("cache.rdb");
182 write_file(&l2_src, b"pager-bytes-on-disk");
183 write_file(
184 &reddb_file::blob_cache_control_path(&l2_src),
185 b"control-sidecar-bytes",
186 );
187
188 let backend_root = scratch("pair-be");
189 let prefix = format!("{}/blob_cache/", backend_root.display());
190
191 let uploaded =
192 archive_blob_cache_l2(&LocalBackend, &l2_src, &prefix).expect("archive succeeds");
193 assert_eq!(uploaded, 2, "pager + control sidecar uploaded");
194
195 let dst_dir = scratch("pair-dst");
196 let l2_dst = dst_dir.join("cache.rdb");
197 let downloaded =
198 restore_blob_cache_l2(&LocalBackend, &prefix, &l2_dst).expect("restore succeeds");
199 assert_eq!(downloaded, 2);
200
201 assert_eq!(std::fs::read(&l2_dst).unwrap(), b"pager-bytes-on-disk");
202 assert_eq!(
203 std::fs::read(reddb_file::blob_cache_control_path(&l2_dst)).unwrap(),
204 b"control-sidecar-bytes"
205 );
206
207 let _ = std::fs::remove_dir_all(&scratch_dir);
208 let _ = std::fs::remove_dir_all(&backend_root);
209 let _ = std::fs::remove_dir_all(&dst_dir);
210 }
211
212 #[test]
213 fn archive_missing_l2_path_is_noop() {
214 let backend_root = scratch("be-missing");
215 let prefix = format!("{}/blob_cache/", backend_root.display());
216 let count = archive_blob_cache_l2(
217 &LocalBackend,
218 std::path::Path::new("/nonexistent/path/for/reddb-test.rdb"),
219 &prefix,
220 )
221 .expect("missing path treated as nothing to archive");
222 assert_eq!(count, 0);
223 let _ = std::fs::remove_dir_all(&backend_root);
224 }
225
226 #[test]
227 fn restore_with_no_objects_creates_empty_parent_dir() {
228 let backend_root = scratch("be-empty");
229 let prefix = format!("{}/blob_cache/", backend_root.display());
230 let dst_dir = scratch("dst-empty");
231 let l2_dst = dst_dir.join("cache.rdb");
232 let count =
233 restore_blob_cache_l2(&LocalBackend, &prefix, &l2_dst).expect("empty restore is ok");
234 assert_eq!(count, 0);
235 let _ = std::fs::remove_dir_all(&backend_root);
236 let _ = std::fs::remove_dir_all(&dst_dir);
237 }
238
239 #[test]
254 fn full_round_trip_via_blob_cache_preserves_entries_after_restore() {
255 use crate::storage::cache::blob::{BlobCache, BlobCacheConfig, BlobCachePut};
256
257 let src_dir = scratch("rt-src");
258 let dst_dir = scratch("rt-dst");
259 let backend_root = scratch("rt-be");
260 let l2_src = src_dir.join("blob-cache.rdb");
261 let l2_dst = dst_dir.join("blob-cache.rdb");
262 let prefix = format!("{}/blob_cache/", backend_root.display());
263
264 {
266 let cache = BlobCache::open_with_l2(
267 BlobCacheConfig::default()
268 .with_l1_bytes_max(64 * 1024)
269 .with_shard_count(2)
270 .with_max_namespaces(8)
271 .with_l2_path(&l2_src),
272 )
273 .expect("open l2 src");
274 cache
275 .put("ns-a", "k1", BlobCachePut::new(b"value-1".to_vec()))
276 .expect("put k1");
277 cache
278 .put(
279 "ns-b",
280 "k2",
281 BlobCachePut::new(b"value-2-longer-payload".to_vec()),
282 )
283 .expect("put k2");
284 assert_eq!(cache.l2_path(), Some(l2_src.as_path()));
286 } let uploaded = archive_blob_cache_l2(&LocalBackend, &l2_src, &prefix).expect("archive l2");
290 assert_eq!(uploaded, 2, "pager + control uploaded");
291
292 let restored = restore_blob_cache_l2(&LocalBackend, &prefix, &l2_dst).expect("restore l2");
294 assert_eq!(restored, 2, "pager + control downloaded");
295
296 let restored_cache = BlobCache::open_with_l2(
300 BlobCacheConfig::default()
301 .with_l1_bytes_max(64 * 1024)
302 .with_shard_count(2)
303 .with_max_namespaces(8)
304 .with_l2_path(&l2_dst),
305 )
306 .expect("open l2 dst");
307 let hit_a = restored_cache
308 .get("ns-a", "k1")
309 .expect("k1 survives restore");
310 assert_eq!(hit_a.value(), b"value-1");
311 let hit_b = restored_cache
312 .get("ns-b", "k2")
313 .expect("k2 survives restore");
314 assert_eq!(hit_b.value(), b"value-2-longer-payload");
315
316 let _ = std::fs::remove_dir_all(&src_dir);
317 let _ = std::fs::remove_dir_all(&dst_dir);
318 let _ = std::fs::remove_dir_all(&backend_root);
319 }
320
321 #[test]
327 fn skipped_archive_leaves_restored_cache_cold() {
328 use crate::storage::cache::blob::{BlobCache, BlobCacheConfig, BlobCachePut};
329
330 let src_dir = scratch("cold-src");
331 let dst_dir = scratch("cold-dst");
332 let l2_src = src_dir.join("blob-cache.rdb");
333 let l2_dst = dst_dir.join("blob-cache.rdb");
334
335 {
336 let cache = BlobCache::open_with_l2(BlobCacheConfig::default().with_l2_path(&l2_src))
337 .expect("open l2 src");
338 cache
339 .put("ns", "k", BlobCachePut::new(b"value".to_vec()))
340 .expect("put k");
341 }
342
343 let cold_cache = BlobCache::open_with_l2(BlobCacheConfig::default().with_l2_path(&l2_dst))
346 .expect("open l2 dst");
347 assert!(
348 cold_cache.get("ns", "k").is_none(),
349 "restore without include_blob_cache must yield a cold cache"
350 );
351
352 let _ = std::fs::remove_dir_all(&src_dir);
353 let _ = std::fs::remove_dir_all(&dst_dir);
354 }
355}