Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure / Jetty: Force URL to only be Hit Once at a Time

I am working on a Clojure / Jetty web service. I have a special url that I want to only be serviced one request at a time. If the url was requested, and before it returns, the url is requested again, I want to immediately return. So in more core.clj, where I defined my routes, I have something like this:

(def work-in-progress (ref false)) 

Then sometime later

(compojure.core/GET "/myapp/internal/do-work" []
    (if @work-in-progress
        "Work in Progress please try again later"
        (do
            (dosync
                (ref-set work-in-progress true))
            (do-the-work)
            (dosync
                (ref-set rebuild-in-progress false))
            "Job completed Successfully")))

I have tried this on local Jetty server but I seem to be able to hit the url twice and double the work. What is a good pattern / way to implement this in Clojure in a threaded web server environment?

like image 837
David Williams Avatar asked Mar 23 '23 16:03

David Williams


2 Answers

Imagine a following race condition for the solution proposed in the question.

  1. Thread A starts to execute handler's body. @work-in-progress is false, so it enters the do expression. However, before it managed to set the value of work-in-progress to true...
  2. Thread B starts to execute handler's body. @work-in-progress is false, so it enters the do expression.

Now two threads are executing (do-the-work) concurrently. That's not what we want.

To prevent this problem check and set the value of the ref in a dosync transaction.

(compojure.core/GET "/myapp/internal/do-work" []
  (if (dosync
        (when-not @work-in-progress
          (ref-set work-in-progress true)))
    (try
      (do-the-work)
      "Job completed Successfully"
      (finally
        (dosync
          (ref-set work-in-progress false))))
    "Work in Progress please try again later"))

Another abstraction which you might find useful in this scenario is an atom and compare-and-set!.

(def work-in-progress (atom false))

(compojure.core/GET "/myapp/internal/do-work" []
  (if (compare-and-set! work-in-progress false true)
    (try
      (do-the-work)
      "Job completed Successfully"
      (finally
        (reset! work-in-progress false)))
    "Work in Progress please try again later"))
like image 83
Jan Avatar answered Apr 01 '23 02:04

Jan


Actually this is the natural use case for a lock; in particular, a java.util.concurrent.locks.ReentrantLock.

The same pattern came up in my answer to an earlier SO question, Canonical Way to Ensure Only One Instance of a Service Is Running / Starting / Stopping in Clojure?; I'll repeat the relevant piece of code here:

(import java.util.concurrent.locks.ReentrantLock)

(def lock (ReentrantLock.))

(defn start []
  (if (.tryLock lock)
    (try
      (do-stuff)
      (finally (.unlock lock)))
    (do-other-stuff)))

The tryLock method attempts to acquire the lock, returning true if it succeeds in doing so and false otherwise, not blocking in either case.

like image 34
Michał Marczyk Avatar answered Apr 01 '23 02:04

Michał Marczyk