top | item 9156230

Try Three Times

34 points| michaelsbradley | 11 years ago |lispcast.com | reply

27 comments

order
[+] mjt0229|11 years ago|reply
That's nice, but what about adding backoff? I just came across this blog post[1] over at AWS about exponential backoff - timely because I'd just been working on some retry logic myself and wondered if I'd made good decisions about how to implement jitter. Turns out, I hadn't.

[1] http://www.awsarchitectureblog.com/2015/03/backoff.html

[+] wtbob|11 years ago|reply
> You pass it a function and a number of times to retry it. The base case is when n is 0. In that case, it will just try it (not retry).

So, TRY-N-TIMES should either be named RETRY-N-TIMES or the base case should be 1; as it is, `(try-n-times #(foobar) 1)` will potentially try twice.

[+] suvelx|11 years ago|reply
I'm not familar with Clojure, but isn't `(catch Throwable)` going to catch everything? Shouldn't we only be retrying if an accepted failure occurs?
[+] seabee|11 years ago|reply
For that to work, you need to enumerate either the acceptable failures (network timeouts) or the unacceptable failures (logic error).

However, for idempotent actions, why does it matter what caused the exception? The worst that happens is you waste 3x the resources, and the benefit is you avoid false negatives from overspecifying your unacceptable failures.

[+] copsarebastards|11 years ago|reply
Wait a second--is this really using exceptions for control flow?

One of the Pragmatic Programmer's tips[1] is "Use Exceptions for Exceptional Problems". Network delays and problems aren't exceptional--they should be expected. It might make sense to throw an exception if you aren't going to handle the network issue, or if you have already tried to handle it and failed. But if you're trying to handle the issue then it shouldn't throw an exception.

Even if you don't buy this from a theoretical standpoint, you should look at your performance using this method. Exception performance is terrible in almost every language. I'm not sure how Clojure exception performance is, but guessing from Java, it's probably not an exception (har har) to the rule.

I'm not a Clojure expert, but I would be really surprised if Clojure didn't offer a non-exception way to do retries.

[1] https://pragprog.com/the-pragmatic-programmer/extracts/tips

[+] MereInterest|11 years ago|reply
The problem with "Use Exceptions for Exceptional Problems" is that it avoids ever defining "exceptional". Usually, it is then interpreted to mean "couldn't be predicted". But if there is a try-catch anywhere in the code, then obviously it could be predicted. Therefore, a corollary to "use exceptions for exceptional problems" is "never use exceptions", which is ridiculous.

My rule of thumb is to use an exception whenever the function cannot complete its contract. After that, it is up to the calling function to fix the problem, or however high up the stack until some function knows what is going on. If I have a function LogIn(username, password), I expect to be logged in after the function completes. Anything else, and I should have a exception thrown, whether it be due to lost connection, incorrect username/password, or anything else. On the other hand, if I have a function AttemptLogIn(username, password), then it would be sufficient for the function to return a boolean for success/failure.

I would modify the statement to "Use Exceptions for Infrequent Problems". Interacting with a user? Sure. Anything that trickles back up to the user and is displayed as an error message will have lag based on the user's reading speed. During heavy numerical calculations? No.

[+] scardine|11 years ago|reply
> Exception performance is terrible in almost every language.

Duck-typing using exceptions is very idiomatic in Python. Also, raising an exception instead of returning error values is a popular API design. Hell, the iterator API - the foundation of loops in Python - raises an exception (StopIteration).

I guess this prescription about exceptions was made with some specific language in mind and it is dangerous to generalize.

[+] ICWiener|11 years ago|reply
> Wait a second--is this really using exceptions for control flow?

No.

If it used exceptions for control-flow, the defined function would actually throw an exception. Here, we catch "Exceptional Problems" and ignore them at most 3 times. A common example of using non-local exit is to return from deeply nested recursive calls.

(edit: of course, you can argue that network delays are not exceptional (surley you love Go), but the code shown by OP is supposed to handle any kind of exceptional situations. Of course, if there was an error value, you could define another function that checks that error value)

That being said, "Exceptions for exceptional situations" is exactly like "never use goto". Non-local exits are definitely useful and not necessarly costly.

> Performance is terrible in almost every language

Even in cases where this is true, it can make sense to use them for control flow according to your requirements and actual measures.

[+] logicallee|11 years ago|reply
> Network delays and problems aren't exceptional--they should be expected. ... might make sense to throw an exception if you aren't going to handle the network issue, or if you have already tried to handle it and failed....But if you're trying to handle the issue then it shouldn't throw an exception.

It sounds like you think exceptions should be used for things you've never thought of, and have no code to handle. But if you don't think there's a way to handle it, what good are exceptions for you - you might as well just barf.

I think in practice people do use exceptions for things they both expect to happen, and know how to handle at that point.

[+] mattexx|11 years ago|reply
I agree that using exceptions as flow control is b-a-d. This is an easy fix in this case, set `:throw-exceptions false` in your http call and check the status code, rather than catch exceptions.
[+] Gurkenmaster|11 years ago|reply
The most expensive part of Java exceptions is building the stacktrace. You can avoid this by overriding the fillInStacktrace method with an empty implementation.
[+] Gurkenmaster|11 years ago|reply
HTTP PUT should be used in place of HTTP POST if you need idempotency.
[+] siscia|11 years ago|reply
I wouldn't have use a macro...

A normal function would have done pretty much the same job...

[+] wtbob|11 years ago|reply
The advantage of a macro is that one doesn't have to wrap one's own code in a closure. It's cleaner to write `(try-n-times (let ((foo (bar 1))) (baz foo 2)) 3)` than `(try-n-times (lambda () (let ((foo (bar 1))) (baz foo 2))) 3)`.

Also: man, writing Lisp without paren-matching bites.

[+] JamesSwift|11 years ago|reply
The core logic was defined as a function. The shorthand 'try3' was macro which calls through to that function.