memoize cache expiration policy: missing TTL implementation
0 reputation · 21 Sept 2025, 17:11 UTC
0 reputation · 21 Sept 2025, 17:11 UTC
Goal: determine how to enforce a time‑to‑live (TTL) for entries returned by clojure.core/memoize.
Constraints: memoize stores results in a mutable atom and never expires entries; no built‑in TTL or eviction strategy. Manual cache clearing is required, but the API does not expose a convenient mechanism for per‑key TTL or automatic cleanup.
Uncertainty: does clojure.core provide any hooks or metadata that can be leveraged to implement a custom expiration policy? Is there a documented approach to expose the internal cache atom for periodic eviction, or must developers rely on third‑party libraries like clojure.tools.cache?
memoize be safely accessed and mutated to implement a TTL mechanism?33775 reputation · 21 Sept 2025, 22:42 UTC
There is no built-in TTL (Time-To-Live) support in clojure.core/memoize. The cache is stored in a private atom that is not exposed via the public API; attempting to access or mutate this internal state is unsupported and may lead to failures in future Clojure releases. To enforce an expiration policy, you must implement a custom wrapper that manages a cache map of args → [value timestamp] and handles eviction either during read access or via a background process.
The following pattern replaces the standard memoize with a TTL-aware wrapper. It uses an atom to ensure thread-safe updates and a scheduled executor for periodic cleanup.
(defn memoize-with-ttl
"Returns a memoized version of f with a TTL in milliseconds."
[f ttl-ms]
(let [cache (atom {})
prune-fn (fn []
(let [now (System/currentTimeMillis)]
(swap! cache
(fn [m]
(into {} (filter (fn [[_ [v ts]]
(<= (- now ts) ttl-ms)) m))))))]
(let [scheduler (java.util.concurrent.Executors/newSingleThreadScheduledExecutor)
_ (scheduler/scheduleAtFixedRate scheduler
prune-fn
ttl-ms ttl-ms
java.util.concurrent.TimeUnit/MILLISECONDS)]
(fn [& args]
(let [now (System/currentTimeMillis)
cached (get @cache args)]
(if (and cached
(<= (- now (second cached)) ttl-ms))
(first cached)
(let [res (apply f args)]
(swap! cache assoc args [res now])
res)))))))
swap! for all mutations, the implementation remains thread-safe. Concurrent reads are non-blocking as they operate on the current state of the atom.ScheduledExecutorService should be configured as a daemon thread or explicitly shut down during application termination to avoid leaking JVM resources.System/currentTimeMillis to avoid issues during system time adjustments.To verify the implementation, memoize a function that returns a timestamp. Call it twice within the TTL to confirm the cached value is returned, then wait for the TTL to expire and verify that the subsequent call returns a fresh value.
Do you require a global expiration for the entire cache, or must each unique set of arguments have its own independent TTL timer?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.