Reputation: 7328
I read at clojure.org/refs that
All reads of Refs will see a consistent snapshot of the 'Ref world' as of the starting point of the transaction (its 'read point'). The transaction will see any changes it has made. This is called the in-transaction-value.
There's also a link to Snapshot Isolation on wikipedia that implies that reads of any number of refs will be consistent with each other once a transaction has started.
I made a test case...
(def r1 (ref 0))
(def r2 (ref 0))
(defn delay-then-inc-ref [id ref delay]
(.start
(Thread.
#((println id " start")
(Thread/sleep delay)
(dosync
(alter ref inc))
(println id " end")))))
(defn deref-delay-deref [ref1 ref2 delay]
(.start
(Thread.
#((println "S start")
(dosync
(let [a @ref2]
(Thread/sleep delay)
(println "S r1=" @ref1))) ; @ref1 consistent with @ref2 ?
(println "S end")))))
*clojure-version*
;=> {:major 1, :minor 3, :incremental 0, :qualifier nil}
(deref-delay-deref r1 r2 2000)
(delay-then-inc-ref "1" r1 500)
(delay-then-inc-ref "2" r1 1000)
(delay-then-inc-ref "3" r1 1500)
The output is:
S start
1 start
2 start
3 start
1 end
2 end
3 end
r1 = 3
S end
nil
The value of r1 = 3
rather than r1 = 0
suggests that in deref-delay-deref
the deref of ref1 after the sleep
is picking the value of r1 after the three delay-then-inc-ref
transactions have occurred.
Note that I know about ensure
to prevent updates to refs by other transactions during a particular transaction, but I don't believe that applies here. I don't care if ref1
changes as long as I see a value consistent with the start of my transaction.
How does this behaviour fit with the above referenced documentation?
Upvotes: 4
Views: 348
Reputation: 7328
It turns out that if the ref has some history it behaves as I expect, so changing the ref declaration to add a :min-history
and then setting both refs as shown, seems to make it work....
(def r1 (ref 0 :min-history 5))
(def r2 (ref 0 :min-history 5))
(dosync
(ref-set r1 0)
(ref-set r2 0))
Then the output is:
S start
1 start
1 end
2 start
2 end
3 start
3 end
S r1= 0
S end
nil
Reading here, it's clear what's going on. The read transaction is restarting because there is no entry in the ref history from before the transaction started. To confim I added some more logging:
(defn deref-delay-deref [ref1 ref2 delay]
(.start
(Thread.
#((println "S start")
(dosync
(println "transaction starting")
(let [a @ref2]
(Thread/sleep delay)
(println "S r1=" @ref1))) ; should be consistent with @ref2
(println "S end")))))
Output without history mods:
S start
transaction starting
1 start
2 start
3 start
1 end
2 end
3 end
transaction starting
S r1= 3
S end
and with history mods:
S start
transaction starting
1 start
2 start
3 start
1 end
2 end
3 end
S r1= 0
S end
nil
UPDATE: It turns out my answer above is something of a distraction because of the artificial nature of the test case. In real world usage it doesn't matter whether the transaction re-starts or not, since the transactions MUST be written such that they are re-startable. The runtime provides no guarantees about whether read only transactions will complete in the presence/absence of history. Rather it can do whatever's necessary to get the world of transactions to complete and the transaction code MUST be written with this in mind. More detailed discussion here
I'm leaving the above for reference.
Upvotes: 1