1use std::collections::HashMap;
10use std::io;
11use std::sync::{Arc, Mutex};
12
13use crate::backend::{Backend, BoxFuture};
14
15pub const DEFAULT_CHUNK_SIZE: usize = 64 * 1024;
17pub const DEFAULT_MAX_BYTES: usize = 32 * 1024 * 1024;
19const MAX_BATCH_BYTES: usize = 8 * 1024 * 1024;
21
22pub struct CachedBackend<B: Backend> {
29 inner: B,
30 chunk_size: usize,
31 max_bytes: usize,
32 state: Mutex<CacheState>,
33 len: tokio::sync::OnceCell<u64>,
34 fetch: Option<Arc<dyn Fn(u64, u64) + Send + Sync>>,
35}
36
37struct CacheState {
38 chunks: HashMap<u64, CacheEntry>,
39 bytes: usize,
40 clock: u64,
41 batch_chunks: usize,
43 previous_miss: Option<u64>,
45}
46
47struct CacheEntry {
48 data: Vec<u8>,
49 stamp: u64,
50}
51
52impl<B: Backend> CachedBackend<B> {
53 pub fn new(inner: B) -> Self {
55 Self::with_capacity(inner, DEFAULT_CHUNK_SIZE, DEFAULT_MAX_BYTES)
56 }
57
58 pub fn with_capacity(inner: B, chunk_size: usize, max_bytes: usize) -> Self {
63 assert!(chunk_size > 0, "chunk_size must be nonzero");
64 CachedBackend {
65 inner,
66 chunk_size,
67 max_bytes,
68 state: Mutex::new(CacheState {
69 chunks: HashMap::new(),
70 bytes: 0,
71 clock: 0,
72 batch_chunks: 1,
73 previous_miss: None,
74 }),
75 len: tokio::sync::OnceCell::new(),
76 fetch: None,
77 }
78 }
79
80 pub fn on_fetch(mut self, fetch: impl Fn(u64, u64) + Send + Sync + 'static) -> Self {
85 self.fetch = Some(Arc::new(fetch));
86 self
87 }
88
89 async fn chunk(&self, index: u64, file_len: u64) -> io::Result<Vec<u8>> {
94 if let Some(hit) = self.lookup(index) {
95 return Ok(hit);
96 }
97 let (first, run) = self.batch_run(index, file_len);
98 let start = first * self.chunk_size as u64;
99 let size = usize::try_from((file_len - start).min((run * self.chunk_size) as u64))
100 .expect("batch size fits usize");
101 let mut data = vec![0u8; size];
102 let mut filled = 0;
103 while filled < size {
104 let count = self
105 .inner
106 .read_at(start + filled as u64, &mut data[filled..])
107 .await?;
108 if count == 0 {
109 break;
110 }
111 filled += count;
112 }
113 data.truncate(filled);
114 if let Some(fetch) = &self.fetch {
115 fetch(start, filled as u64);
116 }
117 let wanted = (index - first) as usize;
118 let mut result = Vec::new();
119 for (step, piece) in data.chunks(self.chunk_size).enumerate() {
120 if step == wanted {
121 result = piece.to_vec();
122 }
123 self.insert(first + step as u64, piece.to_vec());
124 }
125 if data.is_empty() {
126 self.insert(index, Vec::new());
129 }
130 Ok(result)
131 }
132
133 fn batch_run(&self, index: u64, file_len: u64) -> (u64, usize) {
147 let max_chunks = (MAX_BATCH_BYTES.min(self.max_bytes / 4) / self.chunk_size).max(1);
148 let mut state = self.state.lock().expect("cache mutex");
149 let dense = state
150 .previous_miss
151 .is_some_and(|previous| index.abs_diff(previous) <= 4 * state.batch_chunks as u64);
152 state.batch_chunks = if dense {
153 (state.batch_chunks * 2).min(max_chunks)
154 } else {
155 (state.batch_chunks / 2).max(1)
156 };
157 state.previous_miss = Some(index);
158 let chunks_total = file_len.div_ceil(self.chunk_size as u64);
159 let mut budget = state.batch_chunks - 1;
160 let mut first = index;
161 while budget > 0 && first > 0 && !state.chunks.contains_key(&(first - 1)) {
162 first -= 1;
163 budget -= 1;
164 }
165 let mut last = index;
166 while budget > 0 && last + 1 < chunks_total && !state.chunks.contains_key(&(last + 1)) {
167 last += 1;
168 budget -= 1;
169 }
170 (first, (last - first + 1) as usize)
171 }
172
173 fn lookup(&self, index: u64) -> Option<Vec<u8>> {
175 let mut state = self.state.lock().expect("cache mutex");
176 state.clock += 1;
177 let stamp = state.clock;
178 let entry = state.chunks.get_mut(&index)?;
179 entry.stamp = stamp;
180 Some(entry.data.clone())
181 }
182
183 fn insert(&self, index: u64, data: Vec<u8>) {
186 let mut state = self.state.lock().expect("cache mutex");
187 state.clock += 1;
188 let stamp = state.clock;
189 state.bytes += data.len();
190 let old_entry = state.chunks.insert(index, CacheEntry { data, stamp });
191 if let Some(old) = old_entry {
192 state.bytes -= old.data.len();
193 }
194 while state.bytes > self.max_bytes && state.chunks.len() > 1 {
195 let coldest = state
196 .chunks
197 .iter()
198 .filter(|(candidate, _)| **candidate != index)
199 .min_by_key(|(_, entry)| entry.stamp)
200 .map(|(candidate, _)| *candidate);
201 match coldest {
202 Some(victim) => {
203 if let Some(gone) = state.chunks.remove(&victim) {
204 state.bytes -= gone.data.len();
205 }
206 }
207 None => break,
208 }
209 }
210 }
211}
212
213impl<B: Backend> Backend for CachedBackend<B> {
214 fn len(&self) -> BoxFuture<'_, io::Result<u64>> {
215 Box::pin(async move { self.len.get_or_try_init(|| self.inner.len()).await.copied() })
216 }
217
218 fn read_at<'a>(&'a self, offset: u64, buf: &'a mut [u8]) -> BoxFuture<'a, io::Result<usize>> {
219 Box::pin(async move {
220 let file_len = self.len().await?;
221 if offset >= file_len || buf.is_empty() {
222 return Ok(0);
223 }
224 let available = usize::try_from(file_len - offset).unwrap_or(usize::MAX);
225 let want = buf.len().min(available);
226 let mut done = 0;
227 while done < want {
228 let pos = offset + done as u64;
229 let index = pos / self.chunk_size as u64;
230 let within = (pos % self.chunk_size as u64) as usize;
231 let chunk = self.chunk(index, file_len).await?;
232 if within >= chunk.len() {
233 break; }
235 let count = (want - done).min(chunk.len() - within);
236 buf[done..done + count].copy_from_slice(&chunk[within..within + count]);
237 done += count;
238 }
239 Ok(done)
240 })
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::backend::MemBackend;
248 use std::sync::atomic::{AtomicUsize, Ordering};
249 use std::sync::Arc;
250
251 #[derive(Default)]
254 struct Counts {
255 calls: AtomicUsize,
256 bytes: AtomicUsize,
257 }
258
259 struct CountingBackend {
261 inner: MemBackend,
262 counts: Arc<Counts>,
263 }
264
265 impl Backend for CountingBackend {
266 fn len(&self) -> BoxFuture<'_, std::io::Result<u64>> {
267 self.inner.len()
268 }
269 fn read_at<'a>(
270 &'a self,
271 offset: u64,
272 buf: &'a mut [u8],
273 ) -> BoxFuture<'a, std::io::Result<usize>> {
274 self.counts.calls.fetch_add(1, Ordering::SeqCst);
275 Box::pin(async move {
276 let count = self.inner.read_at(offset, buf).await?;
277 self.counts.bytes.fetch_add(count, Ordering::SeqCst);
278 Ok(count)
279 })
280 }
281 }
282
283 fn counting(data: Vec<u8>) -> (CountingBackend, Arc<Counts>) {
284 let counts = Arc::new(Counts::default());
285 (
286 CountingBackend {
287 inner: MemBackend::from(data),
288 counts: Arc::clone(&counts),
289 },
290 counts,
291 )
292 }
293
294 #[tokio::test]
295 async fn repeated_reads_fetch_each_chunk_once() {
296 let (inner, fetches) = counting((0u8..=255).collect());
297 let cached = CachedBackend::with_capacity(inner, 64, 1024);
298 let mut buf = [0u8; 8];
299 assert_eq!(cached.read_at(10, &mut buf).await.unwrap(), 8);
300 assert_eq!(&buf, &[10, 11, 12, 13, 14, 15, 16, 17]);
301 assert_eq!(cached.read_at(20, &mut buf).await.unwrap(), 8);
302 assert_eq!(&buf, &[20, 21, 22, 23, 24, 25, 26, 27]);
303 assert_eq!(fetches.calls.load(Ordering::SeqCst), 1);
305 }
306
307 #[tokio::test]
308 async fn reads_spanning_chunks_and_eof_are_stitched() {
309 let (inner, fetches) = counting((0u8..=255).collect());
310 let cached = CachedBackend::with_capacity(inner, 64, 1024);
311 let mut buf = [0u8; 100];
312 assert_eq!(cached.read_at(30, &mut buf).await.unwrap(), 100);
316 assert_eq!(buf[0], 30);
317 assert_eq!(buf[99], 129);
318 assert_eq!(fetches.calls.load(Ordering::SeqCst), 2);
319 assert_eq!(cached.read_at(250, &mut buf).await.unwrap(), 6);
321 assert_eq!(&buf[..6], &[250, 251, 252, 253, 254, 255]);
322 assert_eq!(cached.read_at(256, &mut buf).await.unwrap(), 0);
323 }
324
325 #[tokio::test]
326 async fn lru_evicts_the_coldest_chunk() {
327 let (inner, fetches) = counting((0u8..=255).collect());
328 let cached = CachedBackend::with_capacity(inner, 64, 128);
330 let mut buf = [0u8; 4];
331 cached.read_at(0, &mut buf).await.unwrap(); cached.read_at(64, &mut buf).await.unwrap(); cached.read_at(0, &mut buf).await.unwrap(); cached.read_at(128, &mut buf).await.unwrap(); assert_eq!(fetches.calls.load(Ordering::SeqCst), 3);
336 cached.read_at(0, &mut buf).await.unwrap(); assert_eq!(fetches.calls.load(Ordering::SeqCst), 3);
338 cached.read_at(64, &mut buf).await.unwrap(); assert_eq!(fetches.calls.load(Ordering::SeqCst), 4);
340 assert_eq!(&buf, &[64, 65, 66, 67]);
341 }
342
343 #[tokio::test]
344 async fn default_capacity_uses_64_kib_chunks() {
345 let (inner, fetches) = counting(vec![7u8; 200_000]);
346 let cached = CachedBackend::new(inner);
347 let mut buf = [0u8; 16];
348 cached.read_at(0, &mut buf).await.unwrap();
349 cached.read_at(70_000, &mut buf).await.unwrap();
350 assert_eq!(fetches.calls.load(Ordering::SeqCst), 2);
354 cached.read_at(1000, &mut buf).await.unwrap();
355 assert_eq!(fetches.calls.load(Ordering::SeqCst), 2);
356 }
357
358 #[tokio::test]
359 async fn dense_scan_escalates_batches_and_collapses_fetches() {
360 let data: Vec<u8> = (0..4096u32).map(|v| v as u8).collect();
362 let (inner, fetches) = counting(data.clone());
363 let cached = CachedBackend::with_capacity(inner, 64, 1 << 20);
364 let mut got = Vec::new();
365 let mut buf = [0u8; 64];
366 for step in 0..64 {
367 let count = cached.read_at(step * 64, &mut buf).await.unwrap();
368 got.extend_from_slice(&buf[..count]);
369 }
370 assert_eq!(got, data, "batched fetches must not corrupt the bytes");
371 assert_eq!(fetches.calls.load(Ordering::SeqCst), 7);
374 }
375
376 #[tokio::test]
377 async fn backward_scan_escalates_batches_toward_lower_offsets() {
378 let data: Vec<u8> = (0..4096u32).map(|v| v as u8).collect();
382 let (inner, fetches) = counting(data.clone());
383 let cached = CachedBackend::with_capacity(inner, 64, 1 << 20);
384 let mut got = Vec::new();
385 let mut buf = [0u8; 64];
386 for step in (0..64u64).rev() {
387 let count = cached.read_at(step * 64, &mut buf).await.unwrap();
388 got.splice(0..0, buf[..count].iter().copied());
389 }
390 assert_eq!(got, data, "backward batched fetches must not corrupt");
391 assert_eq!(fetches.calls.load(Ordering::SeqCst), 7);
393 assert_eq!(fetches.bytes.load(Ordering::SeqCst), 4096, "each byte once");
394 }
395
396 #[tokio::test]
397 async fn far_jumps_reset_the_batch_to_one_chunk() {
398 let (inner, fetches) = counting(vec![9u8; 8192]);
399 let cached = CachedBackend::with_capacity(inner, 64, 1 << 20);
400 let mut buf = [0u8; 16];
401 cached.read_at(0, &mut buf).await.unwrap();
404 cached.read_at(4096, &mut buf).await.unwrap();
405 cached.read_at(1024, &mut buf).await.unwrap();
406 assert_eq!(fetches.calls.load(Ordering::SeqCst), 3);
407 cached.read_at(64, &mut buf).await.unwrap();
410 assert_eq!(fetches.calls.load(Ordering::SeqCst), 4);
411 }
412
413 #[tokio::test]
414 async fn batch_runs_trim_at_cached_chunks_and_eof() {
415 let data: Vec<u8> = (0..4096u32).map(|v| v as u8).collect();
416 let (inner, fetches) = counting(data);
417 let cached = CachedBackend::with_capacity(inner, 64, 1 << 20);
418 let mut buf = [0u8; 16];
419 cached.read_at(5 * 64, &mut buf).await.unwrap(); cached.read_at(2 * 64, &mut buf).await.unwrap(); cached.read_at(4 * 64, &mut buf).await.unwrap();
425 cached.read_at(3 * 64, &mut buf).await.unwrap();
426 assert_eq!(fetches.calls.load(Ordering::SeqCst), 3);
427 assert_eq!(
428 fetches.bytes.load(Ordering::SeqCst),
429 5 * 64,
430 "chunks 5, 1-2 and 3-4 fetched exactly once, nothing beyond"
431 );
432 let mut tail = [0u8; 64];
434 assert_eq!(cached.read_at(4032, &mut tail).await.unwrap(), 64);
435 assert_eq!(tail[63], 4095u32 as u8);
436 }
437
438 #[tokio::test]
439 async fn fetch_observer_reports_offset_and_length_of_each_inner_fetch() {
440 let data: Vec<u8> = (0..4096u32).map(|v| v as u8).collect();
441 let (inner, _) = counting(data);
442 let seen: Arc<Mutex<Vec<(u64, u64)>>> = Arc::new(Mutex::new(Vec::new()));
443 let sink = Arc::clone(&seen);
444 let cached = CachedBackend::with_capacity(inner, 64, 1 << 20)
445 .on_fetch(move |offset, len| sink.lock().unwrap().push((offset, len)));
446 let mut buf = [0u8; 16];
447 cached.read_at(5 * 64, &mut buf).await.unwrap(); cached.read_at(4 * 64, &mut buf).await.unwrap(); cached.read_at(5 * 64, &mut buf).await.unwrap(); assert_eq!(*seen.lock().unwrap(), vec![(320, 64), (192, 128)]);
451 }
452
453 #[test]
454 fn duplicate_insert_does_not_double_count() {
455 let (inner, _) = counting(vec![0u8; 200]);
457 let cached = CachedBackend::with_capacity(inner, 64, 256);
458
459 cached.insert(0, vec![0u8; 64]);
461 cached.insert(0, vec![0u8; 64]);
462
463 let state = cached.state.lock().unwrap();
465 assert_eq!(
466 state.bytes, 64,
467 "Duplicate insert should not double-count bytes"
468 );
469 assert_eq!(state.chunks.len(), 1, "Should have exactly one chunk");
470 }
471}