Search code examples
concurrencyclojure

Clojure locking on string value


I have a piece of code in clojure which should run on isolation. Lets say this function is

(defn isolate [string1])

Its easy to isolate the whole function on all inputs, calling it like so :

(def o (Object. ))

(locking o (isolate string1))

However this only allows one process/thread to access isolate simultaneously.

What I have now implemented is the following :

(def current-locks (ref {}))

(defn mergeReverse [x y] (merge y x))

(defn merge-with-current-locks [key val]
  (dosync (alter current-locks mergeReverse {key val})))

(defn remove-lock [key]
  (dosync (alter current-locks dissoc key)))

and finally the threads block calling this method

(defn block-until-free [key val]
  (let [_ (merge-with-current-locks key val)]
    (if (dosync (and (contains? current-locks key)
                     (not= (get current-locks key) val)))
      (do
        (Thread/sleep 10)
        (block-until-free key val)))))

As you can see in the solution I used keys and values here and although I only lock on the keys but being able to use maps instead of arrays was beneficial since I used the merge property that merges in a map only if map does not contain this value and since the current-locks is a ref I used alter and swapped the merge inputs to acquire the needed behaviour.

This hack works as far as I can tell (and I tested it). But my question is how can I do this in a correct clojure way? This solution seems complicated

Off course the remove-lock has to be called once the critical function is executed.


Solution

  • You should use a database transaction for this. Here is an example of the Clojure code:

      ; Wraps all commands in a single transaction
      (jdbc/with-db-transaction
        [tx db-conn]
        (let [clj-id (grab :id (only (jdbc/query tx ["select id from langs where lang='Clojure'"])))]
          (jdbc/insert-multi! tx :releases
                              [{:desc "ancients" :langId clj-id}
                               {:desc "1.8" :langId clj-id}
                               {:desc "1.9" :langId clj-id}]))
        (let [java-id (grab :id (only (jdbc/query tx ["select id from langs where lang='Java'"])))]
          (jdbc/insert-multi! tx :releases
                              [{:desc "dusty" :langId java-id}
                               {:desc "8" :langId java-id}
                               {:desc "9" :langId java-id}
                               {:desc "10" :langId java-id}])))
    

    Here we query a table langs for the id values of languages Clojure and Java. We then add rows to table releases with cols desc and foreign key langId. Because both insert-multi! statements are wrapped via (jdbc/with-db-transaction, the transaction will roll-back if any other thread updated the db before it completed.

    The above code would need a retry loop to catch an exception if our transaction failed, then retry (perhaps with a random delay). You can find the entire sample code here.


    Update

    My example was for a SQL db such as Postgres. For Datomic, I believe you will want a function such as db.fn/cas. See the Datomic docs for full details. You can also ask on the Datomic mailing list or post a more specific Datomic question on StackOverflow.

    For either Postgres or Datomic, the transaction will only abort if the specific row/entity you change is also changed by another thread. It does not lock the entire database.