···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+let log_src = Logs.Src.create "cache"
77+88+module Log = (val Logs.src_log log_src : Logs.LOG)
99+1010+module type KEY = sig
1111+ type t
1212+1313+ val equal : t -> t -> bool
1414+ val hash : t -> int
1515+end
1616+1717+module type S = sig
1818+ type key
1919+ type 'a t
2020+2121+ val create :
2222+ clock:_ Eio.Time.clock -> ?base_ttl:float -> ?jitter:float -> unit -> 'a t
2323+2424+ val get : 'a t -> key -> 'a option
2525+ val set : 'a t -> key -> 'a -> unit
2626+ val remove : 'a t -> key -> unit
2727+ val get_or_compute : 'a t -> key -> (unit -> 'a) -> 'a
2828+ val gc : 'a t -> unit
2929+ val clear : 'a t -> unit
3030+ val stats : 'a t -> int * int
3131+end
3232+3333+module Make (K : KEY) : S with type key = K.t = struct
3434+ type key = K.t
3535+3636+ module H = Hashtbl.Make (K)
3737+3838+ type 'a entry = { value : 'a; expires_at : float }
3939+4040+ type 'a t = {
4141+ entries : 'a entry H.t;
4242+ mutex : Eio.Mutex.t;
4343+ base_ttl : float;
4444+ jitter : float;
4545+ now : unit -> float;
4646+ }
4747+4848+ let create ~clock ?(base_ttl = 60.0) ?(jitter = 0.2) () =
4949+ {
5050+ entries = H.create 256;
5151+ mutex = Eio.Mutex.create ();
5252+ base_ttl;
5353+ jitter;
5454+ now = (fun () -> Eio.Time.now clock);
5555+ }
5656+5757+ let now t = t.now ()
5858+5959+ let ttl_with_jitter t =
6060+ let jitter_range = t.base_ttl *. t.jitter in
6161+ let random_jitter = Random.float jitter_range -. (jitter_range /. 2.0) in
6262+ t.base_ttl +. random_jitter
6363+6464+ let get t key =
6565+ Eio.Mutex.use_ro t.mutex @@ fun () ->
6666+ match H.find_opt t.entries key with
6767+ | None -> None
6868+ | Some entry ->
6969+ if now t < entry.expires_at then Some entry.value
7070+ else begin
7171+ H.remove t.entries key;
7272+ None
7373+ end
7474+7575+ let set t key value =
7676+ Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () ->
7777+ let entry = { value; expires_at = now t +. ttl_with_jitter t } in
7878+ H.replace t.entries key entry
7979+8080+ let remove t key =
8181+ Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () -> H.remove t.entries key
8282+8383+ let get_or_compute t key compute =
8484+ match get t key with
8585+ | Some v -> v
8686+ | None ->
8787+ let v = compute () in
8888+ set t key v;
8989+ v
9090+9191+ let gc t =
9292+ Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () ->
9393+ let current = now t in
9494+ let to_remove =
9595+ H.fold
9696+ (fun k entry acc ->
9797+ if current >= entry.expires_at then k :: acc else acc)
9898+ t.entries []
9999+ in
100100+ List.iter (H.remove t.entries) to_remove;
101101+ Log.debug (fun m ->
102102+ m "Cache GC: removed %d expired entries" (List.length to_remove))
103103+104104+ let clear t =
105105+ Eio.Mutex.use_rw ~protect:true t.mutex @@ fun () -> H.clear t.entries
106106+107107+ let stats t =
108108+ Eio.Mutex.use_ro t.mutex @@ fun () ->
109109+ let total = H.length t.entries in
110110+ let current = now t in
111111+ let valid =
112112+ H.fold
113113+ (fun _ entry acc -> if current < entry.expires_at then acc + 1 else acc)
114114+ t.entries 0
115115+ in
116116+ (total, valid)
117117+end
118118+119119+module String = Make (struct
120120+ type t = string
121121+122122+ let equal = String.equal
123123+ let hash = Hashtbl.hash
124124+end)
+70
lib/cache.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Generic TTL-based in-memory cache.
77+88+ Provides thread-safe caching with configurable TTL (time-to-live) and jitter
99+ to prevent thundering herd problems.
1010+1111+ {1 Example}
1212+ {[
1313+ module MyCache = Cache.Make (struct
1414+ type t = int
1515+1616+ let equal = Int.equal
1717+ let hash = Hashtbl.hash
1818+ end)
1919+2020+ let cache = MyCache.create ~clock:(Eio.Stdenv.clock env) ()
2121+ let () = MyCache.set cache 42 "answer"
2222+ let value = MyCache.get cache 42 (* Some "answer" *)
2323+ ]} *)
2424+2525+module type KEY = sig
2626+ type t
2727+2828+ val equal : t -> t -> bool
2929+ val hash : t -> int
3030+end
3131+3232+module type S = sig
3333+ type key
3434+ type 'a t
3535+3636+ val create :
3737+ clock:_ Eio.Time.clock -> ?base_ttl:float -> ?jitter:float -> unit -> 'a t
3838+ (** [create ~clock ?base_ttl ?jitter ()] creates a new cache.
3939+ @param clock Eio time source
4040+ @param base_ttl Base time-to-live in seconds (default: 60.0)
4141+ @param jitter Random jitter factor 0.0-1.0 (default: 0.2) *)
4242+4343+ val get : 'a t -> key -> 'a option
4444+ (** [get t key] returns the cached value if present and not expired. *)
4545+4646+ val set : 'a t -> key -> 'a -> unit
4747+ (** [set t key value] stores a value with TTL + jitter. *)
4848+4949+ val remove : 'a t -> key -> unit
5050+ (** [remove t key] removes a key from the cache. *)
5151+5252+ val get_or_compute : 'a t -> key -> (unit -> 'a) -> 'a
5353+ (** [get_or_compute t key f] returns cached value or computes and caches it.
5454+ *)
5555+5656+ val gc : 'a t -> unit
5757+ (** [gc t] removes all expired entries. *)
5858+5959+ val clear : 'a t -> unit
6060+ (** [clear t] removes all entries. *)
6161+6262+ val stats : 'a t -> int * int
6363+ (** [stats t] returns [(total_entries, valid_entries)]. *)
6464+end
6565+6666+(** Functor to create a cache with custom key type. *)
6767+module Make (K : KEY) : S with type key = K.t
6868+6969+module String : S with type key = string
7070+(** Pre-instantiated cache with string keys. *)