Skip to content

Instantly share code, notes, and snippets.

Created February 21, 2015 04:08
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