reddb_server/storage/cache/
mod.rs1pub mod aggregates;
30pub mod bgwriter;
31pub mod blob;
32pub mod compressor;
33pub mod extended_ttl;
34pub mod promotion_pool;
35pub mod result;
36pub mod ring;
37pub mod sieve;
38pub mod spill;
39pub mod strategy;
40pub mod sweeper;
41
42pub use aggregates::{AggCacheStats, AggValue, AggregationCache, CardinalityEstimate, NumericAgg};
43pub use blob::{
44 BlobCache, BlobCacheConfig, BlobCacheHit, BlobCachePolicy, BlobCachePut, BlobCacheStats,
45 CacheError, L1Admission, L2Compression, DEFAULT_BLOB_L1_BYTES_MAX, DEFAULT_BLOB_L2_BYTES_MAX,
46 DEFAULT_BLOB_MAX_NAMESPACES, METRIC_CACHE_BLOB_L1_BYTES_IN_USE,
47 METRIC_CACHE_BLOB_L2_BYTES_IN_USE, METRIC_CACHE_BLOB_L2_FULL_REJECTIONS_TOTAL,
48 METRIC_CACHE_VERSION_MISMATCH_TOTAL,
49};
50pub use compressor::{CompressError, CompressOpts, Compressed, L2BlobCompressor};
51pub use extended_ttl::{EffectiveExpiry, ExpiryDecision, ExtendedTtlPolicy};
52pub use promotion_pool::{
53 AsyncPromotionPool, PoolOpts, PromotionExecutor, PromotionMetrics, PromotionRequest,
54 ScheduleOutcome,
55};
56pub use result::{
57 CacheKey, CachePolicy, MaterializedViewCache, MaterializedViewDef, RefreshPolicy, ResultCache,
58 ResultCacheStats,
59};
60pub use ring::BufferRing;
61pub use sieve::{CacheConfig, CacheStats, PageCache, PageId};
62pub use spill::{SpillConfig, SpillError, SpillManager, SpillStats, SpillableGraph};
63pub use strategy::BufferAccessStrategy;
64
65pub fn archive_blob_cache_l2(
99 backend: &dyn crate::storage::backend::RemoteBackend,
100 l2_path: &std::path::Path,
101 prefix: &str,
102) -> Result<usize, crate::storage::backend::BackendError> {
103 let mut count = 0usize;
104 if l2_path.is_file() {
105 backend.upload(l2_path, &reddb_file::blob_cache_l2_backup_pager_key(prefix))?;
106 count += 1;
107 }
108 let control = reddb_file::blob_cache_control_path(l2_path);
109 if control.is_file() {
110 backend.upload(
111 &control,
112 &reddb_file::blob_cache_l2_backup_control_key(prefix),
113 )?;
114 count += 1;
115 }
116 Ok(count)
117}
118
119pub fn restore_blob_cache_l2(
128 backend: &dyn crate::storage::backend::RemoteBackend,
129 prefix: &str,
130 l2_path: &std::path::Path,
131) -> Result<usize, crate::storage::backend::BackendError> {
132 if let Some(parent) = l2_path.parent() {
133 if !parent.as_os_str().is_empty() {
134 std::fs::create_dir_all(parent)
135 .map_err(|err| crate::storage::backend::BackendError::Transport(err.to_string()))?;
136 }
137 }
138 let mut count = 0usize;
139 if backend.download(&reddb_file::blob_cache_l2_backup_pager_key(prefix), l2_path)? {
140 count += 1;
141 }
142 let control = reddb_file::blob_cache_control_path(l2_path);
143 if backend.download(
144 &reddb_file::blob_cache_l2_backup_control_key(prefix),
145 &control,
146 )? {
147 count += 1;
148 }
149 Ok(count)
150}
151
152#[cfg(test)]
153mod backup_helpers_tests {
154 use super::*;
155 use crate::storage::backend::LocalBackend;
156 use std::sync::atomic::{AtomicU64, Ordering};
157
158 fn write_file(path: &std::path::Path, bytes: &[u8]) {
159 if let Some(parent) = path.parent() {
160 std::fs::create_dir_all(parent).unwrap();
161 }
162 std::fs::write(path, bytes).unwrap();
163 }
164
165 static SCRATCH_COUNTER: AtomicU64 = AtomicU64::new(0);
171 fn scratch(label: &str) -> std::path::PathBuf {
172 let pid = std::process::id();
173 let n = SCRATCH_COUNTER.fetch_add(1, Ordering::SeqCst);
174 let p = std::env::temp_dir().join(format!("reddb-blobcache-bk-{label}-{pid}-{n}"));
175 let _ = std::fs::remove_dir_all(&p);
176 std::fs::create_dir_all(&p).unwrap();
177 p
178 }
179
180 #[test]
186 fn archive_then_restore_round_trips_l2_pager_and_control_files() {
187 let scratch_dir = scratch("pair-src");
188 let l2_src = scratch_dir.join("cache.rdb");
189 write_file(&l2_src, b"pager-bytes-on-disk");
190 write_file(
191 &reddb_file::blob_cache_control_path(&l2_src),
192 b"control-sidecar-bytes",
193 );
194
195 let backend_root = scratch("pair-be");
196 let prefix = format!("{}/blob_cache/", backend_root.display());
197
198 let uploaded =
199 archive_blob_cache_l2(&LocalBackend, &l2_src, &prefix).expect("archive succeeds");
200 assert_eq!(uploaded, 2, "pager + control sidecar uploaded");
201
202 let dst_dir = scratch("pair-dst");
203 let l2_dst = dst_dir.join("cache.rdb");
204 let downloaded =
205 restore_blob_cache_l2(&LocalBackend, &prefix, &l2_dst).expect("restore succeeds");
206 assert_eq!(downloaded, 2);
207
208 assert_eq!(std::fs::read(&l2_dst).unwrap(), b"pager-bytes-on-disk");
209 assert_eq!(
210 std::fs::read(reddb_file::blob_cache_control_path(&l2_dst)).unwrap(),
211 b"control-sidecar-bytes"
212 );
213
214 let _ = std::fs::remove_dir_all(&scratch_dir);
215 let _ = std::fs::remove_dir_all(&backend_root);
216 let _ = std::fs::remove_dir_all(&dst_dir);
217 }
218
219 #[test]
220 fn archive_missing_l2_path_is_noop() {
221 let backend_root = scratch("be-missing");
222 let prefix = format!("{}/blob_cache/", backend_root.display());
223 let count = archive_blob_cache_l2(
224 &LocalBackend,
225 std::path::Path::new("/nonexistent/path/for/reddb-test.rdb"),
226 &prefix,
227 )
228 .expect("missing path treated as nothing to archive");
229 assert_eq!(count, 0);
230 let _ = std::fs::remove_dir_all(&backend_root);
231 }
232
233 #[test]
234 fn restore_with_no_objects_creates_empty_parent_dir() {
235 let backend_root = scratch("be-empty");
236 let prefix = format!("{}/blob_cache/", backend_root.display());
237 let dst_dir = scratch("dst-empty");
238 let l2_dst = dst_dir.join("cache.rdb");
239 let count =
240 restore_blob_cache_l2(&LocalBackend, &prefix, &l2_dst).expect("empty restore is ok");
241 assert_eq!(count, 0);
242 let _ = std::fs::remove_dir_all(&backend_root);
243 let _ = std::fs::remove_dir_all(&dst_dir);
244 }
245
246 #[test]
261 fn full_round_trip_via_blob_cache_preserves_entries_after_restore() {
262 use crate::storage::cache::blob::{BlobCache, BlobCacheConfig, BlobCachePut};
263
264 let src_dir = scratch("rt-src");
265 let dst_dir = scratch("rt-dst");
266 let backend_root = scratch("rt-be");
267 let l2_src = src_dir.join("blob-cache.rdb");
268 let l2_dst = dst_dir.join("blob-cache.rdb");
269 let prefix = format!("{}/blob_cache/", backend_root.display());
270
271 {
273 let cache = BlobCache::open_with_l2(
274 BlobCacheConfig::default()
275 .with_l1_bytes_max(64 * 1024)
276 .with_shard_count(2)
277 .with_max_namespaces(8)
278 .with_l2_path(&l2_src),
279 )
280 .expect("open l2 src");
281 cache
282 .put("ns-a", "k1", BlobCachePut::new(b"value-1".to_vec()))
283 .expect("put k1");
284 cache
285 .put(
286 "ns-b",
287 "k2",
288 BlobCachePut::new(b"value-2-longer-payload".to_vec()),
289 )
290 .expect("put k2");
291 assert_eq!(cache.l2_path(), Some(l2_src.as_path()));
293 } let uploaded = archive_blob_cache_l2(&LocalBackend, &l2_src, &prefix).expect("archive l2");
297 assert_eq!(uploaded, 2, "pager + control uploaded");
298
299 let restored = restore_blob_cache_l2(&LocalBackend, &prefix, &l2_dst).expect("restore l2");
301 assert_eq!(restored, 2, "pager + control downloaded");
302
303 let restored_cache = BlobCache::open_with_l2(
307 BlobCacheConfig::default()
308 .with_l1_bytes_max(64 * 1024)
309 .with_shard_count(2)
310 .with_max_namespaces(8)
311 .with_l2_path(&l2_dst),
312 )
313 .expect("open l2 dst");
314 let hit_a = restored_cache
315 .get("ns-a", "k1")
316 .expect("k1 survives restore");
317 assert_eq!(hit_a.value(), b"value-1");
318 let hit_b = restored_cache
319 .get("ns-b", "k2")
320 .expect("k2 survives restore");
321 assert_eq!(hit_b.value(), b"value-2-longer-payload");
322
323 let _ = std::fs::remove_dir_all(&src_dir);
324 let _ = std::fs::remove_dir_all(&dst_dir);
325 let _ = std::fs::remove_dir_all(&backend_root);
326 }
327
328 #[test]
334 fn skipped_archive_leaves_restored_cache_cold() {
335 use crate::storage::cache::blob::{BlobCache, BlobCacheConfig, BlobCachePut};
336
337 let src_dir = scratch("cold-src");
338 let dst_dir = scratch("cold-dst");
339 let l2_src = src_dir.join("blob-cache.rdb");
340 let l2_dst = dst_dir.join("blob-cache.rdb");
341
342 {
343 let cache = BlobCache::open_with_l2(BlobCacheConfig::default().with_l2_path(&l2_src))
344 .expect("open l2 src");
345 cache
346 .put("ns", "k", BlobCachePut::new(b"value".to_vec()))
347 .expect("put k");
348 }
349
350 let cold_cache = BlobCache::open_with_l2(BlobCacheConfig::default().with_l2_path(&l2_dst))
353 .expect("open l2 dst");
354 assert!(
355 cold_cache.get("ns", "k").is_none(),
356 "restore without include_blob_cache must yield a cold cache"
357 );
358
359 let _ = std::fs::remove_dir_all(&src_dir);
360 let _ = std::fs::remove_dir_all(&dst_dir);
361 }
362}