/
sqlite_cache.clj
224 lines (189 loc) 路 7.88 KB
/
sqlite_cache.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
(ns cljdoc.util.sqlite-cache
"Provides `SQLCache`, an implementation of `clojure.core.cache/CacheProtocol`.
It is used with `clojure.core.memoize/PluggableMemoization`
This namespace exposes `memo-sqlite` function which takes a
function to memoize and cache-spec, it retuns the memoized function.
This function uses the `SQLCache` for memoization.
"
(:require [clojure.core.cache :as cache]
[clojure.core.memoize :as memo]
[clojure.string :as string]
[next.jdbc :as jdbc]
[next.jdbc.result-set :as rs])
(:import (java.time Instant)))
(defn instant-now
"abstraction for time so we can redef it in tests"
^Instant []
(Instant/now))
(defn- derefable? [v]
(instance? clojure.lang.IDeref v))
(def query-templates
{:fetch "SELECT * FROM %s WHERE prefix = ? AND %s = ?"
:evict "DELETE FROM %s WHERE prefix = ? AND %s = ?"
:cache "INSERT OR IGNORE INTO %s (prefix, cached_ts, ttl ,%s, %S) VALUES (?, ?, ?, ?, ?)"
:refresh "UPDATE %s SET cached_ts = ?, ttl = ?, %s = ? WHERE prefix = ? and %s = ?"
:clear "DELETE FROM %s WHERE prefix = ?"})
(defn stale?
"Tests if `ttl` has expired for a cached item.
When `ttl` is `nil` item is not stale.
Otherwise `ttl` is compared with `cached_ts`."
[{:keys [ttl cached_ts]}]
(if (nil? ttl)
false
(> (- (.toEpochMilli (instant-now))
(.toEpochMilli (Instant/parse cached_ts)))
ttl)))
(defn fetch-item!
"Performs lookup by querying on the cache table.
Returns deserialized cached item."
[k {:keys [db-spec key-prefix deserialize-fn table key-col value-col]}]
(let [query (format (:fetch query-templates) table key-col)]
(some-> (jdbc/execute-one! db-spec
[query key-prefix (pr-str k)]
{:builder-fn rs/as-unqualified-maps})
(get (keyword value-col))
deserialize-fn)))
(defn fetch!
[k {:keys [db-spec key-prefix table key-col]}]
(let [query (format (:fetch query-templates) table key-col)]
(jdbc/execute-one! db-spec [query key-prefix (pr-str k)]
{:builder-fn rs/as-unqualified-maps})))
(defn refresh!
[k v {:keys [db-spec key-prefix table key-col value-col serialize-fn ttl]}]
(let [query (format (:refresh query-templates) table value-col key-col)
value (serialize-fn @v)]
(jdbc/execute-one! db-spec [query (instant-now) ttl value key-prefix (pr-str k)])))
(defn cache!
[k v {:keys [db-spec key-prefix serialize-fn table key-col value-col ttl]}]
(let [query (format (:cache query-templates) table key-col value-col)
value (serialize-fn @v)]
(jdbc/execute-one! db-spec [query key-prefix (instant-now) ttl (pr-str k) value])))
(defn evict!
[k {:keys [db-spec key-prefix table key-col]}]
(let [query (format (:evict query-templates) table key-col)]
(jdbc/execute-one! db-spec [query key-prefix (pr-str k)])))
(defn seed!
[{:keys [db-spec key-prefix table key-col value-col]}]
;; this might seem a bit odd, ya'd think this table would be created by db migrations,
;; but it is useful for REPL testing
(let [create-cmd (string/join " " [(format "create table if not exists %s (" table)
"ttl INTEGER,"
"prefix TEXT NOT NULL,"
"cached_ts TEXT NOT NULL,"
(format "%s TEXT NOT NULL," key-col)
(format "%s TEXT NOT NULL," value-col)
(format "CONSTRAINT unique_prefix_and_key UNIQUE (prefix, %s)" key-col)
")"])]
(jdbc/execute! db-spec [create-cmd])))
(defn clear-all!
[{:keys [db-spec key-prefix table]}]
(let [query (format (:clear query-templates) table)]
(jdbc/execute! db-spec [query key-prefix])))
(defn- d-ref [v]
(if (derefable? v) (deref v) v))
;; memoize kind of assumes we are carrying around our cache in state.
;; this is not the case for us, our state is our config and never changes
;; after init.
(cache/defcache SQLCache [state]
cache/CacheProtocol
(lookup [_ k]
(delay (fetch-item! k (:cache-spec state))))
(lookup [_this k _not-found]
(when-let [item (fetch-item! k (:cache-spec state))]
(delay item)))
(has? [_ k]
(let [item (fetch! k (:cache-spec state))]
(and (not (nil? item))
(not (stale? item)))))
(hit [this _k]
this)
(miss [this k v]
;; never cache nil values
(when (not (nil? (d-ref v)))
(let [item (fetch! k (:cache-spec state))]
(if (and (not (nil? item)) (stale? item))
(refresh! k v (:cache-spec state))
(cache! k v (:cache-spec state)))))
this)
(evict [this k]
(evict! k (:cache-spec state))
this)
(seed [this base]
(if (empty? base)
;; if we are being seeded with an empty base, it is a request from memoize to clear the cache
(do
(clear-all! (:cache-spec state))
this)
;; otherwise we are being initialized
(do
(seed! (:cache-spec base))
(SQLCache. base))))
Object
(toString [_] (str state)))
(defn memo-sqlite
"Memoizes the given function `f` using `SQLCache` for caching.
`SQLCache` uses a SQL backend to store return values of `f`.
Example usage with SQLite:
```
(def memo-f
(memo-sqlite (fn [arg] (Thread/sleep 5000) arg)
{:key-prefix \"artifact-repository\"
:key-col \"key\"
:value-col \"value\"
:ttl 2000
:table \"cache\"
:serialize-fn identity
:deserialize-fn read-string
:db-spec {:dbtype \"sqlite\"
:host :none
:dbname \"data/my-cache.db\"}}))
(memo-f 1) ;; takes more than 5 seconds to return.
(memo-f 1) ;; return immediately from cache.
```
"
[f cache-spec]
(memo/build-memoizer
#(memo/->PluggableMemoization
% (cache/seed (SQLCache. {}) {:cache-spec cache-spec}))
f))
(comment
(def db-artifact-repository
{:key-prefix "artifact-repository"
:table "cache2"
:key-col "key"
:value-col "val"
:ttl 2000
:serialize-fn taoensso.nippy/freeze
:deserialize-fn taoensso.nippy/thaw
:db-spec {:dbtype "sqlite"
:host :none
:dbname "data/cache.db"}})
(require '[cljdoc.util.repositories])
(time (cljdoc.util.repositories/find-artifact-repository 'bidi "2.1.3"))
(def memoized-find-artifact-repository
(memo-sqlite cljdoc.util.repositories/find-artifact-repository
db-artifact-repository))
(time (memoized-find-artifact-repository 'bidi "2.1.3"))
(time (memoized-find-artifact-repository 'neverfindme "2.1.3"))
(time (memoized-find-artifact-repository 'com.bhauman/spell-spec "0.1.0"))
(memo/memo-clear!
memoized-find-artifact-repository '(com.bhauman/spell-spec "0.1.0"))
(memo/memo-clear! memoized-find-artifact-repository)
(time (memoized-find-artifact-repository 'com.bhauman/spell-spec "0.1.0"))
(time (cljdoc.util.repositories/artifact-uris 'bidi "2.0.9-SNAPSHOT"))
(def db-artifact-uris
{:key-prefix "artifact-uris"
:table "cache2"
:key-col "key"
:value-col "val"
:serialize-fn identity
:deserialize-fn read-string
:db-spec {:dbtype "sqlite"
:host :none
:dbname "data/cache.db"}})
(def memoized-artifact-uris
(memo-sqlite cljdoc.util.repositories/artifact-uris
db-artifact-uris))
(time (memoized-artifact-uris 'bidi "2.0.9-SNAPSHOT"))
(time (memoized-artifact-uris 'com.bhauman/spell-spec "0.1.0"))
nil)