In Progress
Unit 1, Lesson 21
In Progress

Redo

Video transcript & code

Let's say we have a list of files we want to download. Each file could be retrieved from any one of three different mirror sites. This is important to know, because the mirrors aren't always reliable. Sometimes they are overloaded with clients, or go down for maintenance. If we get an error response from one mirror, we can try the same request on a different one instead of giving up altogether.

For this example, I've simulated the unreliability of the mirrors using WebMock. Every fourth request to any of the mirror hosts will result in a 502 "Bad Gateway" response.

FILES = %W[file1 file2 file3 file4 file5 file6 file7 file8 file9]

MIRRORS = %W[a.example.org b.example.org c.example.org]

require "net/http"
require "webmock"

include WebMock::API

request_count = 0
err = {status: 502}
ok  = {status: 200, body: "OK!"}
stub_request(:get, /.*\.example\.org/)
  .to_return(->(r){ request_count += 1; request_count % 4 == 0 ? err : ok })

In order to download the files, we can loop over the list using #each. To begin with, we'll just use the first mirror and ignore the others. We'll build a URI by combining the mirror and the filename, and we'll log the URI. Then we'll make an HTTP request.

We then make a decision based on the response status. If it's 200, all is well and we log a successful download. But if it's some other status code, we note the failure and terminate the loop with break.

We run this code, and see that it makes four requests before ending with an error.

require "./setup"

FILES.each do |file|
  mirror = MIRRORS.first
  uri = URI("http://#{mirror}/#{file}")
  puts "Requesting #{uri}"
  result = Net::HTTP.get_response(uri)
  if result.code == "200"
    puts "Success!"
  else
    puts "Error #{result.code}"
    break
  end
end

# >> Requesting http://a.example.org/file1
# >> Success!
# >> Requesting http://a.example.org/file2
# >> Success!
# >> Requesting http://a.example.org/file3
# >> Success!
# >> Requesting http://a.example.org/file4
# >> Error 502

In order to make this code robust in the face of failures, we want it to switch mirrors when there is a network problem. But it's not enough to change mirrors and then go around the loop again, because this would mean skipping the file that failed entirely.

A typical approach to a problem like this would be to add an inner loop which re-tried downloading the same filename with successive mirrors. But in Ruby, we can avoid the need to write a second loop.

Instead, we add just two lines of code. First, when a request fails we shift the mirrors array by one, which has the effect of putting the next mirror at the head of the list. Then, we invoke the redo keyword.

redo is a special block control flow operator in Ruby. It causes execution to be thrown back up to the beginning of the current block. But unlike the next keyword, the block does not advance to the next iteration in a sequence. Instead, it is restarted with the same block argument as the last time around.

We can run the code and see the upshot of this behavior. After four requests, there is an error. But instead of terminating, the request is repeated with the same file but a new mirror. Then there are another four requests, followed by another mirror switch, and so on.

require "./setup"

FILES.each do |file|
  mirror = MIRRORS.first
  uri = URI("http://#{mirror}/#{file}")
  puts "Requesting #{uri}"
  result = Net::HTTP.get_response(uri)
  if result.code == "200"
    puts "Success!"
  else
    puts "Error #{result.code}; switching mirrors"
    MIRRORS.shift
    redo
  end
end

# >> Requesting http://a.example.org/file1
# >> Success!
# >> Requesting http://a.example.org/file2
# >> Success!
# >> Requesting http://a.example.org/file3
# >> Success!
# >> Requesting http://a.example.org/file4
# >> Error 502; switching mirrors
# >> Requesting http://b.example.org/file4
# >> Success!
# >> Requesting http://b.example.org/file5
# >> Success!
# >> Requesting http://b.example.org/file6
# >> Success!
# >> Requesting http://b.example.org/file7
# >> Error 502; switching mirrors
# >> Requesting http://c.example.org/file7
# >> Success!
# >> Requesting http://c.example.org/file8
# >> Success!
# >> Requesting http://c.example.org/file9
# >> Success!

redo is very similar to the retry keyword we discussed in episode #257. The difference is that retry is for exception rescue clauses, and redo is for blocks.

Like with retry, we have to take care with redo that we always move the program state forward in some way before redoing. Otherwise, we risk getting stuck in an infinite loop. In today's example, we're ensuring this by shifting a mirror off of the mirror list before every redo. Eventually, with enough failures this will cause the code to run out of mirrors, a scenario we haven't handled yet.

By using redo, we are able to "try, try again" at a given loop iteration, without adding the code complexity of an inner loop. And that's it for today. Happy hacking!

Responses