@@ -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