Welcome! Please see the About page for a little more info on how this works.

0 votes
in Clojure by

I'm a noob trying to do something that ought to be (and probably is) simple.

I would like to retry a function if it throws a particular exception. If the function keeps failing (throws the exception for three more consecutive calls), I want to stop retrying and bubble up a new exception of my own.

I played around with loop and recur, but couldn't make it work.

Ideas?

2 Answers

+1 vote
by
selected by
 
Best answer

Here is some hacked-up code briefly tested in a Clojure 1.10.1 REPL:

(def call-count (atom 0))

(defn throws-every-other-time [x]
  (let [new-count (swap! call-count inc)]
    (if (zero? (mod new-count 2))
      (assert false)  ;; throws java.lang.AssertionError
      (* 2 x))))

(throws-every-other-time 17)
;; Try this in a REPL and you see it throws on every other call

(defn retry-n-times [f n]
  (let [sentinel (volatile! nil)]
    (loop [i 0]
      (if (= i n)
        ;; Here is where you would throw your own desired exception.
        ;; In this example, I am just returning a map.
        {:call-count i, :failed-every-time true}
        ;; No println necessary - just nice to see what is going on
        ;; when testing a new function like this.
        (let [_ (println "Call (f) try" (inc i) "...")
              ret (try
                    (f)
                    (catch java.lang.AssertionError e
                      (vreset! sentinel e)
                      sentinel))]
          (if (identical? ret sentinel)
            (recur (inc i))
            ret))))))

(retry-n-times (fn [] (throws-every-other-time 17)) 3)
by
If you don't want to roll your own, you can use some circuit breaker library. Diehard has pretty much this exact functionality: https://github.com/sunng87/diehard The first example in their readme does this.
by
This worked well, Andy. I still wish I could find a way to do it without resorting to using what amounts to a local variable. Not sure if that's cheating from a FP standpoint.

I thought that maybe counting by adding stuff to a collection might be better. But it seems like I would be using that to create state equivalent to what you proposed anyway.
by
Note that in my code example, the only reason I used a mutable volatile! object was to save the original exception thrown.  If it is acceptable to forget the original exception in all of your use cases, then you could change the value assigned to sentinel to be `(Object.)` (a freshly allocated instance of the java.lang.Object class), and eliminate the `vreset!` call, which is the only occurrence of mutation in my code.
by
Sorry. I don't follow. Any chance you could tweak your response above to show me what you mean?
by
Here is the variation of the function retry-n-times that I was trying to describe.  Again, this version assumes that you never care to keep around the exception thrown by the call (f).

(defn retry-n-times [f n]
  (let [sentinel (Object.)]
    (loop [i 0]
      (if (= i n)
        ;; Here is where you would throw your own desired exception.
        ;; In this example, I am just returning a map.
        {:call-count i, :failed-every-time true}
        ;; No println necessary - just nice to see what is going on
        ;; when testing a new function like this.
        (let [_ (println "Call (f) try" (inc i) "...")
              ret (try
                    (f)
                    (catch java.lang.AssertionError e
                      sentinel))]
          (if (identical? ret sentinel)
            (recur (inc i))
            ret))))))
0 votes
by

What you are hoping to do should be perfectly possible using a loop that iterates at most 3 times, each iteration doing a try with catch inside of it to catch the exception you want to cause to try again. You are welcome to share code snippets you have tried here, or link to them from here.

I do not recall off hand whether you can do a recur within a try that is inside of a loop, but even if not, you can make the entire try expression return a unique distinguishable 'sentinel' value if the loop should go around again, where by 'sentinel' value I simply mean "some value that you know the function you are trying to call can never return". A freshly allocated (Object.) instance on Clojure/Java would satisfy that property, in general.

...