Skip to content

Commit a27f02e

Browse files
committed
Simplify lookup-or-miss: replace RetryingDelay and locking with delay/force
Remove the custom RetryingDelay deftype, d-lay, and r-force helpers. Use standard Clojure delay/force instead, relying on swap! CAS semantics for stampede prevention rather than locking the cache atom. - Each thread creates its own delay; swap! CAS ensures only one wins - All threads deref the same winning delay (no stampede) - realized? distinguishes own-delay failure (rethrow) from another thread's failure (retry) - TTL expiration retry loop preserved
1 parent ec4632c commit a27f02e

File tree

1 file changed

+30
-53
lines changed

1 file changed

+30
-53
lines changed

src/main/clojure/clojure/core/cache/wrapped.clj

Lines changed: 30 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -24,50 +24,16 @@
2424

2525
(def ^{:private true} default-wrapper-fn #(%1 %2))
2626

27-
;; Similar to clojure.lang.Delay, but will not memoize an exception and will
28-
;; instead retry.
29-
;; fun - the function, never nil
30-
;; available? - indicates a memoized value is available, volatile for visibility
31-
;; value - the value (if available) - volatile for visibility
32-
(deftype RetryingDelay [fun ^:volatile-mutable available? ^:volatile-mutable value]
33-
clojure.lang.IDeref
34-
(deref [this]
35-
;; first check (safe with volatile flag)
36-
(if available?
37-
value
38-
(locking fun
39-
;; second check (race condition with locking)
40-
(if available?
41-
value
42-
;; fun may throw - will retry on next deref
43-
(let [v (fun)]
44-
;; this ordering is important - MUST set value before setting available?
45-
;; or you have a race with the first check above
46-
(set! value v)
47-
(set! available? true)
48-
v)))))
49-
clojure.lang.IPending
50-
(isRealized [this]
51-
available?))
52-
53-
(defn- d-lay [fun]
54-
(->RetryingDelay fun false nil))
55-
56-
(defn- r-force [maybe-d-lay]
57-
(if (instance? RetryingDelay maybe-d-lay)
58-
(deref maybe-d-lay)
59-
maybe-d-lay))
60-
6127
(defn lookup
6228
"Retrieve the value associated with `e` if it exists, else `nil` in
6329
the 2-arg case. Retrieve the value associated with `e` if it exists,
6430
else `not-found` in the 3-arg case.
6531
6632
Reads from the current version of the atom."
6733
([cache-atom e]
68-
(r-force (c/lookup @cache-atom e)))
34+
(force (c/lookup @cache-atom e)))
6935
([cache-atom e not-found]
70-
(r-force (c/lookup @cache-atom e not-found))))
36+
(force (c/lookup @cache-atom e not-found))))
7137

7238
(defn lookup-or-miss
7339
"Retrieve the value associated with `e` if it exists, else compute the
@@ -82,25 +48,36 @@
8248
([cache-atom e value-fn]
8349
(lookup-or-miss cache-atom e default-wrapper-fn value-fn))
8450
([cache-atom e wrap-fn value-fn]
85-
(let [d-new-value (d-lay #(wrap-fn value-fn e))
86-
hit-or-miss
51+
(let [my-delay (delay (wrap-fn value-fn e))
52+
attempt
8753
(fn []
88-
(locking cache-atom ; I really do not like this... :(
89-
(try
90-
(r-force (c/lookup (swap! cache-atom
91-
c/through-cache
92-
e
93-
default-wrapper-fn
94-
(fn [_] d-new-value))
95-
e
96-
::expired))
97-
(catch Throwable t
98-
(swap! cache-atom c/evict e)
99-
(throw t)))))]
100-
(loop [n 0 v (hit-or-miss)]
54+
(let [v (c/lookup (swap! cache-atom
55+
c/through-cache
56+
e
57+
default-wrapper-fn
58+
(fn [_] my-delay))
59+
e
60+
::expired)]
61+
(cond
62+
;; TTL expiration race — retry
63+
(= ::expired v) ::expired
64+
;; not a delay — return directly
65+
(not (delay? v)) v
66+
:else
67+
(try
68+
(force v)
69+
(catch Throwable t
70+
;; our own delay failed — evict and rethrow
71+
(when (realized? my-delay)
72+
(swap! cache-atom c/evict e)
73+
(throw t))
74+
;; another thread's delay failed — evict and retry
75+
(swap! cache-atom c/evict e)
76+
::retry)))))]
77+
(loop [n 0 v (attempt)]
10178
(when (< n 10)
102-
(if (= ::expired v)
103-
(recur (inc n) (hit-or-miss))
79+
(case v
80+
(::expired ::retry) (recur (inc n) (attempt))
10481
v))))))
10582

10683
(defn has?

0 commit comments

Comments
 (0)