TTL Cache sometimes returns nil

Description

I believe this is related to https://dev.clojure.org/jira/browse/CMEMOIZE-15

As an example, run this a few times and you'll eventually see "fail" printed.

(let [key "foo"
cache (atom (cache/ttl-cache-factory {} :ttl 5))]
(dotimes [_ 5]
(future
(dotimes [_ 1000]
(when-not (-> (swap! cache (fn [c]
(if (cache/has? c key)
(cache/hit c key)
(cache/miss c key (str key "-value")))))
(cache/lookup key))
(println "fail"))
(Thread/sleep 1)))))

Vince

Environment

None

Activity

Show:
Sean Corfield
November 1, 2019, 4:51 PM

Released/addressed in 0.8.0 through 0.8.2 (0.8.0 and 0.8.1 both had bugs in the wrapped API that are fixed in 0.8.2).

Sean Corfield
March 19, 2019, 5:14 AM

While I understand that some people think this is surprising, it's not a bug but an artifact of how a TTL lookup has to work. When you perform a lookup, the item you are looking for may have already expired and the lookup itself must take care of that and it will not return an expired item. It has to do this because if all you do to a cache is a series of lookup calls, then at some point, they must start to return nil when the item has passed its TTL.

What you have in the usage above is a race condition in the code using the cache because it is inherently not an atomic operation: it performs the has?/hit/miss code atomically but then calls lookup separately.

There's a similar race condition possible in the usage discussed in https://dev.clojure.org/jira/browse/CCACHE-50

I'm looking at wrapping the current, low-level API, in a higher-level API that ensures users don't trip over these race conditions. Such an API will need to be based around the atom containing the cache, rather than the (immutable) cache itself and it will be a much more restrictive API.

Declined

Assignee

Sean Corfield

Reporter

import

Labels

None

Approval

None

Patch

None

Priority

Major