Search code examples
clojuretransactionsdeadlock

Is it possible to replicate a transaction deadlock in Clojure?


In SQL it is relatively easy to replicate a transaction deadlock.

==SESSION1==
begin tran
update table1 set ... where ...
[hold off further action - begin on next session]

==SESSION2==
begin 
update table1 set ... where ...
[hold off further action - begin on next session]

==SESSION3==
<list blocked transactions - see session2>

Now with Clojure transactions - you can't just open them and leave them open, the s-expressions don't let you do that.

So I'm curious with respect to the scenario above.

My question is: Is it possible to replicate a transaction deadlock in Clojure?


Solution

  • The STM in Clojure is designed to provide atomic, consistent and isolated actions on refs, without locking. Several features are implemented in order to achieve this as described in refs documentation, but one of the main point is to have a "optimistic" strategy, which handles version of data for each transaction and compare this version and the ref version at write time.

    This kind of optimistic strategy can be implemented in database as well, e.g. in Oracle.

    Anyway, in Clojure, if you really want to create a deadlock, you will have to use low level mecanism, for instance the locking macro that explicitly create a lock on an object (same as synchronized in Java) and manage explicitly the access to the shared resource.

    EDIT : Example of livelock This example comes from Clojure Programming, @cgrand and al.

    (let [retry-count (agent 0)
          x (ref 0)]
      (try
        (dosync  ;; transaction A
          @(future (dosync ;; transaction B
                     (send-off retry-count inc)
                     (ref-set x 1)))
          (ref-set x 2))
        (catch Exception e (println (str "caught exception: " (.getMessage e))))
        (finally
        (await retry-count)))
      [@x @retry-count])
    
    caught exception: Transaction failed after reaching retry limit
    [1 10000]
    user> 
    

    Transaction A executes in the repl thread. Transaction B will execute in a separate thread, but since the future is deref in A, it blocks until B finishes. When A tries (ref-set x 2), x has already been modified by B, and this trigger a retry of A, which spawn a new thread and B transaction... until the maximum retries is reached and an exception raised.