Skip to content

Commit a414f0c

Browse files
committed
Add example of simplified cache implementation
1 parent 2882c47 commit a414f0c

File tree

1 file changed

+191
-0
lines changed

1 file changed

+191
-0
lines changed

src/chunk_cache.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,194 @@ impl ChunkCache {
253253
println!("Finished pruning the cache");
254254
}
255255
}
256+
257+
mod test {
258+
259+
use bytes::Bytes;
260+
use num_traits::ToPrimitive;
261+
use serde::{Deserialize, Serialize};
262+
use std::{collections::HashMap, ops::Add, path::PathBuf, time::{SystemTime, UNIX_EPOCH}};
263+
use tokio::fs;
264+
265+
#[derive(Debug)]
266+
struct SimpleDiskCache {
267+
/// Cache folder name
268+
name: String,
269+
/// Cache parent directory
270+
dir: PathBuf,
271+
/// Max cache size in bytes
272+
max_size_bytes: usize,
273+
/// Max time to live for a single cache entry
274+
ttl_seconds: u64,
275+
}
276+
277+
#[derive(Debug, Serialize, Deserialize)]
278+
struct Metadata {
279+
/// Seconds after unix epoch for ache item expiry
280+
expires: u64,
281+
/// Cache value size
282+
size_bytes: usize,
283+
}
284+
285+
impl Metadata {
286+
fn new(size: usize, ttl: u64) -> Self {
287+
let expires = SystemTime::now()
288+
.duration_since(UNIX_EPOCH)
289+
.unwrap()
290+
.as_secs()
291+
.add(ttl);
292+
Metadata {
293+
expires,
294+
size_bytes: size,
295+
}
296+
}
297+
}
298+
299+
type CacheKeys = HashMap<String, Metadata>;
300+
301+
impl SimpleDiskCache {
302+
pub fn new(name: &str, dir: &str, max_size_bytes: usize, ttl_seconds: u64) -> Self {
303+
let name = name.to_string();
304+
let dir = PathBuf::from(dir);
305+
let path = dir.join(&name);
306+
if !dir.as_path().exists() {
307+
panic!("Cache parent dir {:?} must exist", dir)
308+
} else if path.exists() {
309+
panic!("Cache folder {:?} already exists", path.to_str())
310+
} else {
311+
std::fs::create_dir(path).unwrap();
312+
}
313+
SimpleDiskCache {
314+
name,
315+
dir,
316+
max_size_bytes,
317+
ttl_seconds,
318+
}
319+
}
320+
321+
async fn load_metadata(&self) -> CacheKeys {
322+
let file = self.dir.join(&self.name).join("metadata.json");
323+
if file.exists() {
324+
serde_json::from_str(fs::read_to_string(file).await.unwrap().as_str()).unwrap()
325+
} else {
326+
HashMap::new()
327+
}
328+
}
329+
330+
async fn save_metadata(&self, data: CacheKeys) {
331+
let file = self.dir.join(&self.name).join("metadata.json");
332+
fs::write(file, serde_json::to_string(&data).unwrap())
333+
.await
334+
.unwrap();
335+
}
336+
337+
async fn get(&self, key: &str) -> Option<Bytes> {
338+
match fs::read(self.dir.join(&self.name).join(key)).await {
339+
Ok(val) => Some(Bytes::from(val)),
340+
Err(err) => match err.kind() {
341+
std::io::ErrorKind::NotFound => {
342+
None
343+
},
344+
_ => panic!("{}", err)
345+
}
346+
}
347+
}
348+
349+
async fn set(&self, key: &str, value: Bytes) {
350+
// Prepare the metadata
351+
let size = value.len();
352+
let path = self.dir.join(&self.name).join(key);
353+
let mut md = self.load_metadata().await;
354+
// Write the cache value and then update the metadata
355+
fs::write(path, value).await.unwrap();
356+
md.insert(key.to_owned(), Metadata::new(size, self.ttl_seconds));
357+
self.save_metadata(md).await;
358+
}
359+
360+
async fn remove(&self, key: &str) {
361+
let mut md = self.load_metadata().await;
362+
if let Some(_) = md.get(key) {
363+
let path = self.dir.join(&self.name).join(key);
364+
fs::remove_file(path).await.unwrap();
365+
md.remove(key);
366+
self.save_metadata(md).await;
367+
}
368+
}
369+
370+
// Removes expired cache entries
371+
async fn prune_expired(&self) {
372+
let metadata = self.load_metadata().await;
373+
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
374+
for (key, data) in metadata.iter() {
375+
if data.expires < timestamp {
376+
self.remove(key).await;
377+
}
378+
}
379+
}
380+
381+
// Removes items which are closest to expiry
382+
// to free up disk space
383+
async fn prune_disk_space(&self) {
384+
let threshold = 0.7;
385+
let metadata = self.load_metadata().await;
386+
let cache_size = metadata.iter().fold(0, |_, (_, item)| item.size_bytes);
387+
if cache_size.to_f64().unwrap() > threshold * self.max_size_bytes.to_f64().unwrap() {
388+
// Remove items until cache size is below threshold
389+
todo!()
390+
}
391+
}
392+
393+
fn wipe(&self) {
394+
std::fs::remove_dir_all(self.dir.join(&self.name)).unwrap();
395+
}
396+
397+
// fn keys() {}
398+
}
399+
400+
#[tokio::test]
401+
async fn test_simple_disk_cache() {
402+
// Arrange
403+
let cache = SimpleDiskCache::new("test-cache-1", "./", 1024, 10);
404+
405+
// Act
406+
let key_1 = "item-1";
407+
let value_1 = Bytes::from(vec![1, 2, 3, 4]);
408+
cache.set(key_1, value_1.clone()).await;
409+
let cache_item_1 = cache.get(key_1).await;
410+
411+
// Assert
412+
let metadata = cache.load_metadata().await;
413+
println!("{:?}", metadata);
414+
assert_eq!(metadata.len(), 1);
415+
assert_eq!(metadata.get(key_1).unwrap().size_bytes, value_1.len());
416+
assert_eq!(cache_item_1.unwrap(), value_1);
417+
418+
// Act
419+
let key_2 = "item-2";
420+
let value_2 = Bytes::from("Test123");
421+
cache.set(key_2, value_2.clone()).await;
422+
let cache_item_2 = cache.get(key_2).await;
423+
424+
// Assert
425+
let metadata = cache.load_metadata().await;
426+
println!("{:?}", metadata);
427+
assert_eq!(metadata.len(), 2);
428+
assert_eq!(metadata.get(key_2).unwrap().size_bytes, value_2.len());
429+
assert_eq!(cache_item_2.unwrap(), value_2);
430+
431+
432+
// Act
433+
cache.remove(key_1).await;
434+
435+
// Assert
436+
let metadata = cache.load_metadata().await;
437+
println!("{:?}", metadata);
438+
assert_eq!(metadata.len(), 1);
439+
assert_eq!(metadata.contains_key(key_1), false);
440+
assert_eq!(metadata.contains_key(key_2), true);
441+
assert_eq!(cache.get(key_1).await, None);
442+
443+
cache.wipe();
444+
445+
}
446+
}

0 commit comments

Comments
 (0)