Recently, I had to write some Ruby code that would publish data to an API
endpoint. Sometimes this endpoint would fail due to bad network conditions so I
wanted to retry the request a few times before marking the attempt as failed.
Ruby has a very nice syntax for doing this sort of thing, but it wasn’t obvious
to me at first. Let’s walk through a contrived example and attempt to refactor
this method so that it automatically retries on an API failure:
def publish_to_api(data={})
begin
DataLibrary.publish(data)
logger.info "success!"
rescue DataLibraryFailureException => e
logger.info "Oh Noes!"
end
end
Without knowing any better, the naive ruby developer (me, in this case) might
add the ability to retry a failed request like so:
def publish_to_api(data={})
3.times do
begin
DataLibrary.publish(data)
logger.info "success!"
break
rescue DataLibraryFailureException => e
logger.info "Oh Noes!"
end
end
end
Hmm… this is starting to look pretty ugly right? Let’s try to clean it up a
bit by using some of the nicities of the ruby language.
The first thing we can do is use the retry keyword:
def publish_to_api(data={})
tries = 3
begin
DataLibrary.publish(data)
logger.info "success!"
rescue DataLibraryFailureException => e
tries -= 1
if tries > 0
retry
else
logger.info "Oh Noes!"
end
end
end
We removed the awkward enclosing loop and replaced it with retry. It reads a
little better, but it’s actually more code than before. Another nice feature of
ruby is that def can be used in place of begin for a rescue block so we
can actually clean it up a bit more by removing the explicit begin call, like
so:
def publish_to_api(data={})
tries ||= 3
DataLibrary.publish(data)
logger.info "success!"
rescue DataLibraryFailureException => e
tries -= 1
if tries > 0
retry
else
logger.info "Oh Noes!"
end
end
Much better. One thing to note here is that we changed tries = 3 to tries
||= 3 so that tries will only be set to 3 if it hasn’t already been set.
Otherwise we’d have an infinite loop when the block is retried and tries is
set to zero again.
We can still clean it up a bit more:
def publish_to_api(data={})
tries ||= 3
DataLibrary.publish(data)
rescue DataLibraryFailureException => e
if (tries -= 1) > 0
retry
else
logger.info "Oh Noes!"
end
else
logger.info "success!"
end
Here we’ve moved the implicit success call (logger.info "success") to an
explicit else clause. This else clause is only triggered if a rescue
doesn’t happen. It can be thought of as “rescue, otherwise do this thing.”
Also, we’ve dropped tries -= 1 directly into the if statement.
This is looking much better than our original attempt at adding retry on
failure. It’s clean, concise and easy to read.
If you only care about the success case, you could shorten it even further:
def publish_to_api(data={})
tries ||= 3
DataLibrary.publish(data)
rescue DataLibraryFailureException => e
retry unless (tries -= 1).zero?
else
logger.info "success!"
end
There are a number of gems (retriable for example) that attempt to
abstract this ruby idiom away into a cleaner DSL but they all seem a little
heavy-weight to me. I find the standard ruby way of doing this very flexible
and succinct. You can learn more about the begin, rescue, and end syntax
at the Pragmatic Programmer’s Guide.