Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Simple cache made hard
(ns exp.cache
"I keep screwing myself up on this and need to write it out.
Caching impl with these constraints:
- caching is layered onto oblivious source fns
- data to cache is some unparsed source data that is transformed in a non-trivial way
- thr raw source data is not interesting to most users and shouln't mess up the api
- caching the final transformed thing is not desireable since we often change the transformation fn
- not all source data should be cached
- to determine if source data should be cached, it must first be parsed
- retryable error messages should never appear in the cache, even for a short time
- no duplicate code
- no repeated work
- don't punish users of non-cached fns with awkward api
Why? Often want to cache the raw input to a function that does some sort of
complex parsing. Then can change/improve the parsing fn without having to
invalidate the cache.
Canonical e.g. Parse raw HTML of a web page, from which structured data parsed.
- Issue was me failing to realize the cache-get and cache-put! calls need to
be in different fns (see below, the get is in caching-thing-raw, and the put
is in caching-thing)
- There's a bit of awkwardness in that the fn that returns the raw cacheable
value has to be public, but this seems reasonable
[schema.core :as s]
[clojure.pprint :refer [pprint]]))
(s/set-fn-validation! true)
(def Parsed
{:name s/Str
:age s/Int
:status (s/enum :ok :error :rate-limited)})
(s/defn thing-raw :- s/Str
"Get the unparsed raw form of the thing with name tname. Slow, would benefit
from caching."
[tname :- s/Str]
(Thread/sleep 500)
;; My "raw" data format is just a Clojure-printed seq
(pr-str [tname
(rand-int 100) ; age
(rand-nth ["ok" "ok" "ok" "error" "rate-limited"]) ; status
(rand-int 1000000) ;noise
(s/defn parse-thing-raw :- Parsed
"Turn a raw thing string into a Parsed."
[raw :- s/Str]
(let [[tname age status & _] (read-string raw)]
{:name tname
:age age
:status (keyword status)}))
(s/defn thing :- Parsed
"Get the parsed thing for name tname."
[tname :- s/Str]
(-> tname thing-raw parse-thing-raw))
;; -- now wrap with caching version --
;; db-like cache impl
(defn cache-put! [c k v] (swap! c assoc k v) nil)
(defn cache-get [c k] (get @c k))
(def cache (atom {}))
(add-watch cache :watcher
(fn [_ _ _ newstate]
(println "cache contents:")
(pprint newstate)))
(s/defn caching-thing-raw :- s/Str
"Returns the raw thing data from cache, or if not available by calling the
underlying 'real' fn."
tname :- s/Str]
(if-let [cached (cache-get cache tname)]
(thing-raw tname)))
(s/defn caching-thing :- Parsed
tname :- s/Str]
(let [raw (caching-thing-raw cache tname)
parsed (parse-thing-raw raw)]
(when (= :ok (:status parsed))
(cache-put! cache tname raw))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.