How to "try again" whe

2017-08-19  本文已影响14人  柳辉

Not all errors are fatal(致命的). Some just indicate that you need to try again. Fortunately, Ruby provides a few interesting mechanisms(机制) that make it easy to "try again" - though not all of them are obvious or well-kown. In this post we'll take a look at these mechanisms and how they work in the real world.

Introducing retry(介绍一下retry)

Ok - this one is kind of obvious(常见的), but only you kown is exits. Personally, I was well into my Ruby career before I learned about the delightful(令人愉快的) "retry" keyword.

Retry is built into Ruby's exception rescuing system. It's quite simple. If you use "retry" in your rescue block it causes the section of code that was rescued to be run again. Let's look at an example.

begin 
  retries ||= 0
  puts "try ##{ retries }"
  raise "the roof"
rescue
  retry if (retries +=1) < 5
end

# ... outputs the following:
# try #0
# try #1
# try #2

The are a few things to note here:

The Problem With retry(retry的问题)

While retry is great it does have some limitations. The main one being that the entire begin block is re-run. But sometimes that's not ideal(明智).

For example, imagine that you're using a gem that lets you post status updates to Twitter, Facebook, and lots of other sites by using a single method call. It might look something like this.

SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")

# ...posts to Twitter API
# ...posts to Facebook API
# ...etc

If one of the APIs fail to respond, the gem raises a SocialMedia::TimeoutError and aborts. If we were to catch this exception and retry, we'd wind up with duplicate posts because the retry would start over from the beginning(如果我们能抓住这个超时异常,并且让代码重新开始)

begin
  SocialMedia.post_to_all("Zomg! I just ate the biggest hamburger")
rescue SocialMedia::TimeoutError
  retry
end

# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# facebook error
# ...posts to Twitter API
# and so on

Wouldn't it be nice if we were able to tell the gem "Just skip facebook, and keep on going down the list of APIs to post to."

Fortunately for us, Ruby allows us to do exactly that.

NOTE: of course the real solution to this problem is to re-architect the social media library. But this is far from the only use-case for the techniques I'm going to show you.

Continuations to the Rescue

Continuations(延长部分) tend to scare people. But that's just because they're not used very frequently and they look a little odd. But once you understand the basics they're really quite simple.

A continuation is like a "save point" in your code, just like in a video game. You can go off and do other things, then jump back to the save point and everything will be as you left it.

...ok, so it's not a perfect analogy, but it kind of works. Let's look at some code:

require "continuation"
counter = 0
continuation = callcc { |c| c } # define our savepoint
puts(counter += 1)
continuation.call(continuation) if counter < 5 # jump back to our savepoint

You may have noticed a few weird(不可思议的) things. Let's go through them:

Adding Continuations to Exceptions

We're going to use continuation to add an skip method to all exceptions. The example below shows how it should work. Whenever I rescue an exception I should be able to call skip, which will cause the code that raised the exception to act like it never happened

begin
  raise "the roof"
  puts "The exception was ignored"
rescue => e
  e.skip
end

# ...outputs "The exception was ignored"

To do this I'm going to have to commit a few sins. Exception is just a class. That means I can monekypatch it to add a skip method.

class Exception
  attr_accessor :continuation
  def skip
    continuation.call
  end
end

Now we need to set the continuation attribute for every exception. It turns out that raise is just a method, which we can override.

BTW, the code below is taken almost vervatim from Advi's excellent slide deck Things You Didn't kown about Exception. I just couldn't think of a better way to implement it than this:

require 'continuation'
module StoreContinuationOnRaise
  def raise(*args)
    callcc do |continuation|
      begin
        super
      rescue Exception => e
        e.continuation = continuation
        super(e)
      end
    end
  end
end

class Object
  include StoreContinuationOnRaise
end

Now I can call the skip method for any exception and it will be like the exception never happened.

上一篇 下一篇

猜你喜欢

热点阅读