Joe
Joe

Reputation: 47609

Unexplained behaviour in atom

I have an atom which stores current and historical event counts.

(def buckets (atom (list)))

It starts as 20 empty values.

(reset! buckets (apply list (repeat 20 0)))

=> (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)

The head bucket gets incremented for each event.

(defn- inc-bucket [buckets]
  (conj (rest buckets) (inc (first buckets))))

(defn event-happened [] (swap! buckets inc-bucket))

=> (event-happened)
(1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)

It gets shifted once a second to give a history of counts.

(defn- shift-buckets [buckets]
  (conj (drop-last buckets) 0))

(once-a-second-run (swap! buckets shift-bucket))

=>  (swap! buckets shift-bucket)
(0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)

This all works fine most of the time. But occasionally (say once a week) I find that the length of buckets has been reset to 5, not the correct value (20 in this example).

=> @buckets
(0 1 0 0 0)

The only place buckets is used is in these functions and with a deref to observe the value.

Something went wrong, and I can't work out how or where. The only place buckets is modified is during the two above swap!s and as I think they're being used correctly. Any ideas?

Upvotes: 1

Views: 101

Answers (2)

Joe
Joe

Reputation: 47609

I found the answer. Laziness and unhandled exceptions in atoms. There is no magic.

user=> (reset! buckets (apply list (repeat 20 0)))
(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
user=> (dotimes [_ 99999] (swap! buckets shift-buckets))
nil
user=> @buckets

StackOverflowError   clojure.core/map/fn--4557 (core.clj:2627)

user=> (type @buckets)
clojure.lang.Cons

This had showed up in the log, but I hadn't seen it (there's a lot else going on). But here's the crazy thing:

user=> @buckets ; again
(0 0)

I don't know why it would give that value. It's not because a cons cell has a head and a tail, because in production that was (0 0 0 0 0). Either way it's undefined behaviour so there's no point guessing.

To fix it, the list must be realised each time.

user=> (defn- shift-buckets-nonlazy [buckets]
  #_=>   (apply list (conj (drop-last buckets) 0)))
#'user/shift-buckets-nonlazy
user=> (reset! buckets (apply list (repeat 20 0)))
(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
user=> (dotimes [_ 999999] (swap! buckets shift-buckets-nonlazy))
nil
user=> (type @buckets)
clojure.lang.PersistentList
user=> @buckets
(0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)

Upvotes: 3

amalloy
amalloy

Reputation: 91907

The problem has to be in code you haven't posted: everything in your question looks perfectly correct, and can't cause the behavior you describe.

Upvotes: 0

Related Questions