Generic TTL cache with Eio
at main 124 lines 3.1 kB view raw
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 SPDX-License-Identifier: MIT 4 ---------------------------------------------------------------------------*) 5 6let log_src = Logs.Src.create "cache" 7 8module Log = (val Logs.src_log log_src : Logs.LOG) 9 10module type KEY = sig 11 type t 12 13 val equal : t -> t -> bool 14 val hash : t -> int 15end 16 17module type S = sig 18 type key 19 type 'a t 20 21 val create : 22 clock:_ Eio.Time.clock -> ?base_ttl:float -> ?jitter:float -> unit -> 'a t 23 24 val get : 'a t -> key -> 'a option 25 val set : 'a t -> key -> 'a -> unit 26 val remove : 'a t -> key -> unit 27 val get_or_compute : 'a t -> key -> (unit -> 'a) -> 'a 28 val gc : 'a t -> unit 29 val clear : 'a t -> unit 30 val stats : 'a t -> int * int 31end 32 33module Make (K : KEY) : S with type key = K.t = struct 34 type key = K.t 35 36 module H = Hashtbl.Make (K) 37 38 type 'a entry = { value : 'a; expires_at : float } 39 40 type 'a t = { 41 entries : 'a entry H.t; 42 mutex : Eio.Mutex.t; 43 base_ttl : float; 44 jitter : float; 45 now : unit -> float; 46 } 47 48 let create ~clock ?(base_ttl = 60.0) ?(jitter = 0.2) () = 49 { 50 entries = H.create 256; 51 mutex = Eio.Mutex.create (); 52 base_ttl; 53 jitter; 54 now = (fun () -> Eio.Time.now clock); 55 } 56 57 let now t = t.now () 58 59 let ttl_with_jitter t = 60 let jitter_range = t.base_ttl *. t.jitter in 61 let random_jitter = Random.float jitter_range -. (jitter_range /. 2.0) in 62 t.base_ttl +. random_jitter 63 64 let get t key = 65 Eio.Mutex.use_ro t.mutex @@ fun () -> 66 match H.find_opt t.entries key with 67 | None -> None 68 | Some entry -> 69 if now t < entry.expires_at then Some entry.value 70 else begin 71 H.remove t.entries key; 72 None 73 end 74 75 let set t key value = 76 Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () -> 77 let entry = { value; expires_at = now t +. ttl_with_jitter t } in 78 H.replace t.entries key entry 79 80 let remove t key = 81 Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () -> H.remove t.entries key 82 83 let get_or_compute t key compute = 84 match get t key with 85 | Some v -> v 86 | None -> 87 let v = compute () in 88 set t key v; 89 v 90 91 let gc t = 92 Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () -> 93 let current = now t in 94 let to_remove = 95 H.fold 96 (fun k entry acc -> 97 if current >= entry.expires_at then k :: acc else acc) 98 t.entries [] 99 in 100 List.iter (H.remove t.entries) to_remove; 101 Log.debug (fun m -> 102 m "Cache GC: removed %d expired entries" (List.length to_remove)) 103 104 let clear t = 105 Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () -> H.clear t.entries 106 107 let stats t = 108 Eio.Mutex.use_ro t.mutex @@ fun () -> 109 let total = H.length t.entries in 110 let current = now t in 111 let valid = 112 H.fold 113 (fun _ entry acc -> if current < entry.expires_at then acc + 1 else acc) 114 t.entries 0 115 in 116 (total, valid) 117end 118 119module String = Make (struct 120 type t = string 121 122 let equal = String.equal 123 let hash = Hashtbl.hash 124end)