Created
February 21, 2015 04:08
-
-
Save overthink/b55d96f3d1df385163cd to your computer and use it in GitHub Desktop.
Simple cache made hard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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. | |
Later: | |
- 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 | |
" | |
(:require | |
[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." | |
[cache | |
tname :- s/Str] | |
(if-let [cached (cache-get cache tname)] | |
cached | |
(thing-raw tname))) | |
(s/defn caching-thing :- Parsed | |
[cache | |
tname :- s/Str] | |
(let [raw (caching-thing-raw cache tname) | |
parsed (parse-thing-raw raw)] | |
(when (= :ok (:status parsed)) | |
(cache-put! cache tname raw)) | |
parsed)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment