Skip to content

Commit 1be60f8

Browse files
committed
Merge pull request #183 from metosin/coercion_memoization_fix
Coercion memoization fix
2 parents 2b4dc3a + f3c183a commit 1be60f8

File tree

4 files changed

+81
-14
lines changed

4 files changed

+81
-14
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 0.24.3 (13.12.2015)
2+
3+
* coercer-cache is now per Route instead beeing global and based on a FIFO size 100 cache. Avoids potential memory
4+
leaks when using anonymous coercion matchers (which never hit the cache).
5+
6+
* Updated deps:
7+
8+
```clj
9+
[prismatic/schema "1.0.4"] is available but we use "1.0.3"
10+
```
11+
112
## 0.24.2 (8.12.2015)
213

314
**[compare](https://github.com/metosin/compojure-api/compare/0.24.1...0.24.2)**

project.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
[potemkin "0.4.2"]
1010
[cheshire "5.5.0"]
1111
[compojure "1.4.0"]
12-
[prismatic/schema "1.0.3"]
12+
[prismatic/schema "1.0.4"]
1313
[org.tobereplaced/lettercase "1.0.0"]
1414
[frankiesardo/linked "1.2.6"]
1515
[metosin/ring-http-response "0.6.5"]

src/compojure/api/meta.clj

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
[schema.core :as s]
1414
[schema.coerce :as sc]
1515
[schema.utils :as su]
16-
[schema-tools.core :as st]))
16+
[schema-tools.core :as st]
17+
[linked.core :as linked]
18+
[compojure.api.impl.logging :as logging]))
1719

1820
;;
1921
;; Meta Evil
@@ -27,6 +29,10 @@
2729
"lexically bound meta-data for handlers."
2830
'+compojure-api-meta+)
2931

32+
(def +compojure-api-coercer+
33+
"lexically bound (caching) coercer for handlers."
34+
'+compojure-api-coercer+)
35+
3036
(defmacro meta-container [meta & form]
3137
`(let [accumulated-meta# (get-local +compojure-api-meta+)
3238
~'+compojure-api-meta+ (deep-merge accumulated-meta# ~meta)]
@@ -47,7 +53,24 @@
4753
;; Schema
4854
;;
4955

50-
(def memoized-coercer (memoize sc/coercer))
56+
(defn memoized-coercer
57+
"Returns a memoized version of a referentially transparent coercer fn. The
58+
memoized version of the function keeps a cache of the mapping from arguments
59+
to results and, when calls with the same arguments are repeated often, has
60+
higher performance at the expense of higher memory use. FIFO with 100 entries.
61+
Cache will be filled if anonymous coercers are used (does not match the cache)"
62+
[]
63+
(let [cache (atom (linked/map))
64+
cache-size 100]
65+
(fn [& args]
66+
(or (@cache args)
67+
(let [coercer (apply sc/coercer args)]
68+
(swap! cache (fn [mem]
69+
(let [mem (assoc mem args coercer)]
70+
(if (>= (count mem) cache-size)
71+
(dissoc mem (-> mem first first))
72+
mem))))
73+
coercer)))))
5174

5275
(defn strict [schema]
5376
(dissoc schema 'schema.core/Keyword))
@@ -57,12 +80,12 @@
5780
(fnk-impl/letk-input-schema-and-body-form
5881
nil (with-meta bind {:schema s/Any}) [] nil)))
5982

60-
(defn body-coercer-middleware [handler responses]
83+
(defn body-coercer-middleware [handler coercer responses]
6184
(fn [request]
6285
(if-let [{:keys [status] :as response} (handler request)]
6386
(if-let [schema (:schema (responses status))]
6487
(if-let [matcher (:response (mw/get-coercion-matcher-provider request))]
65-
(let [coerce (memoized-coercer (value-of schema) matcher)
88+
(let [coerce (coercer (value-of schema) matcher)
6689
body (coerce (:body response))]
6790
(if (su/error? body)
6891
(throw+ (assoc body :type ::ex/response-validation))
@@ -79,7 +102,7 @@
79102
(assert (not (#{:query :json} type)) (str type " is DEPRECATED since 0.22.0. Use :body or :string instead."))
80103
`(let [value# (keywordize-keys (~key ~+compojure-api-request+))]
81104
(if-let [matcher# (~type (mw/get-coercion-matcher-provider ~+compojure-api-request+))]
82-
(let [coerce# (memoized-coercer ~schema matcher#)
105+
(let [coerce# (~+compojure-api-coercer+ ~schema matcher#)
83106
result# (coerce# value#)]
84107
(if (su/error? result#)
85108
(throw+ (assoc result# :type ::ex/request-validation))
@@ -116,7 +139,7 @@
116139

117140
(defmulti restructure-param
118141
"Restructures a key value pair in smart routes. By default the key
119-
is consumed form the :parameters map in acc. k = given key, v = value."
142+
is consumed form the :parameters map in acc. k = given key, v = value."
120143
(fn [k v acc] k))
121144

122145
;;
@@ -337,11 +360,14 @@
337360
(map-of lets letks responses middlewares parameters body)
338361
parameters)
339362

363+
pre-lets [+compojure-api-coercer+ `(memoized-coercer)]
364+
340365
body `(~body-wrap ~@body)
341366
body (if (seq letks) `(letk ~letks ~body) body)
342367
body (if (seq lets) `(let ~lets ~body) body)
343368
body (if (seq middlewares) `(route-middlewares ~middlewares ~body ~arg) body)
344369
body (if (seq parameters) `(meta-container ~parameters ~body) body)
345370
body `(~method-symbol ~path ~arg-with-request ~body)
346-
body (if responses `(body-coercer-middleware ~body ~responses) body)]
371+
body (if responses `(body-coercer-middleware ~body ~+compojure-api-coercer+ ~responses) body)
372+
body (if (seq pre-lets) `(let ~pre-lets ~body) body)]
347373
body))

test/compojure/api/coercion_test.clj

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
body => {:beers ["ipa" "apa"]})))
4444

4545
(fact "body-coersion can ba disabled"
46-
(let [no-body-coercion (fn [_] (dissoc mw/default-coercion-matchers :body))
46+
(let [no-body-coercion (constantly (dissoc mw/default-coercion-matchers :body))
4747
app (api
4848
{:coercion no-body-coercion}
4949
beer-route)]
@@ -52,7 +52,7 @@
5252
body => {:beers ["ipa" "apa" "ipa"]})))
5353

5454
(fact "body-coersion can ba changed"
55-
(let [nop-body-coercion (fn [_] (assoc mw/default-coercion-matchers :body (constantly nil)))
55+
(let [nop-body-coercion (constantly (assoc mw/default-coercion-matchers :body (constantly nil)))
5656
app (api
5757
{:coercion nop-body-coercion}
5858
beer-route)]
@@ -71,7 +71,7 @@
7171
body => {:i 10})))
7272

7373
(fact "query-coersion can ba disabled"
74-
(let [no-query-coercion (fn [_] (dissoc mw/default-coercion-matchers :string))
74+
(let [no-query-coercion (constantly (dissoc mw/default-coercion-matchers :string))
7575
app (api
7676
{:coercion no-query-coercion}
7777
query-route)]
@@ -80,7 +80,7 @@
8080
body => {:i "10"})))
8181

8282
(fact "query-coersion can ba changed"
83-
(let [nop-query-coercion (fn [_] (assoc mw/default-coercion-matchers :string (constantly nil)))
83+
(let [nop-query-coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
8484
app (api
8585
{:coercion nop-query-coercion}
8686
query-route)]
@@ -92,7 +92,7 @@
9292
:query-params [i :- s/Int]
9393
(ok {:i i}))
9494
(GET* "/disabled-coercion" []
95-
:coercion (fn [_] (assoc mw/default-coercion-matchers :string (constantly nil)))
95+
:coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
9696
:query-params [i :- s/Int]
9797
(ok {:i i}))
9898
(GET* "/no-coercion" []
@@ -111,4 +111,34 @@
111111
(fact "no coercion"
112112
(let [[status body] (get* app "/no-coercion" {:i 10})]
113113
status => 200
114-
body => {:i "10"})))))
114+
body => {:i "10"}))))
115+
116+
(fact "anonymous matchers, with 100+ calls to same endpoint"
117+
118+
#_(fact "at api-level, matcher is reused and the coercion matcher cache is not filled"
119+
(let [app (api
120+
{:coercion (constantly mw/default-coercion-matchers)}
121+
(GET* "/anonymous" []
122+
:query-params [i :- s/Str]
123+
(ok {:i i})))]
124+
125+
(dotimes [_ 200]
126+
(let [[status body] (get* app "/anonymous" {:i "10"})]
127+
status => 200
128+
body => {:i "10"})) => nil
129+
(provided
130+
(compojure.api.impl.logging/log! & anything) => irrelevant :times 0)))
131+
132+
#_(fact "at route-level, matcher is NOT reused and the the coercion matcher cache is filled"
133+
(let [app (api
134+
(GET* "/anonymous" []
135+
:coercion (constantly (assoc mw/default-coercion-matchers :string (constantly nil)))
136+
:query-params [i :- s/Str]
137+
(ok {:i i})))]
138+
139+
(dotimes [_ 200]
140+
(let [[status body] (get* app "/anonymous" {:i "10"})]
141+
status => 200
142+
body => {:i "10"})) => nil
143+
(provided
144+
(compojure.api.impl.logging/log! & anything) => irrelevant :times 1)))))

0 commit comments

Comments
 (0)