Reputation: 1745
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.
Upvotes: 0
Views: 328
Reputation: 29958
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.
Upvotes: 1