How to "try again" when exceptions happen in Ruby

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:

  • When retry is called, all of code in between begin and rescue is run again. It most definitely does not "pick up where it left off" or anything like that.

  • If you don't provide some mechanism to limit retries, you will wind up with an infinite loop.

  • Code in both the begin and rescue blocks are able to access the same retries variable in the parent scope.

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:

  • We use the callcc method to create a Continuation object. There's no clean OO syntax for this.

  • The first time the continuation variable is assigned, it is set to the return value of callcc's block. That's why the block has to be there.

  • Each time we jump back to the savepoint, the continuation variable is assigned whatever argument we pass the call method. That's why we use the weird looking continuation.call(continuation) syntax.

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.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349

推荐阅读更多精彩内容