RubyGemsのTwitter::Baseがraiseするサーバーエラー関連の例外を拾いリトライ処理を行う

RubyGemsのtwitterパッケージは非常に便利で,これを使うと少ないメソッド呼び出しで簡単にTwitter APIを利用することができる.このパッケージのTwitter::Baseを用いてTwitter botを運用している人も結構多いのではないかと思う.

# create Twitter::Base object
oauth = Twitter::OAuth.new(consumer_token_key, consumer_secret)
oauth.authorize_from_access(access_token_key, access_secret)
twitter = Twitter::Base.new(oauth)

# access Twitter API
twitter.update("test")
mentions = twitter.mentions

サーバーエラー時にTwitter::Baseがraiseする例外

Twitter botを開発する上でエラー処理は付き物だ.特に不安定なTwitterサーバーのせいで,Twitter APIとの通信エラーは割と頻繁に発生する.この時,Twitter::Baseは例外を発生することで呼び出し元にサーバーエラーを通知する.
通信エラーは大抵の場合一時的なものなので,エラー後に何秒か時間をおいて再度アクセスすれば大丈夫なことが多い.そのため,例外を捕捉して適切にリトライ処理を行うことが安定運用をする上で重要になる(サーバーエラーのせいでユーザからのreplyに反応しなかった,というようなことを防ぐため).

Twitterがサーバーエラーの際にTwitter::Baseがraiseする例外は,私が把握している範囲では以下の5つがある.

  • Errno::ECONNRESET
  • Errno::ETIMEDOUT
  • Timeout::Error
  • Twitter::InformTwitter
  • Twitter::Unavailable

せめて投げる例外を「Twitter::ほげほげ」だけにしてほしいのだが….

参考までに,私のTwitter botの「mickey24_bot」はcronを使って毎分起動していて,1回の起動ごとにTwitter API呼び出しを1〜10回程度行っている.その中で上述の例外が発生した回数は,2010/08/16〜2010/08/22は124回,2010/08/23〜2010/08/29は39回,2010/08/30〜2010/09/05は356回となっている.

リトライ処理用ラッパークラス

これだけの頻度でサーバーエラーの例外が発生するのだから,リトライ処理をきちんと書く必要性は非常に高い.しかし,Twitter::Baseのメソッド呼び出しがコードのあちこちに散在していたり,Twitter::Baseのいろいろな種類のメソッドを使っていたりすると,それら全てをうまくラップしてリトライ処理を簡潔に書くのはなかなか難しい場合もある.
そこで,リトライ処理用を簡単に行うために,method_missingを使って以下のようなTwitter::Base用のラッパークラスを書いてみた.

twitter_wrapper.rb
# -*- coding: utf-8 -*-

require "twitter"

class TwitterWrapper
  class Unavailable < StandardError; end

  def initialize(twitter)
    @twitter = twitter
  end

  def method_missing(message, *args, &block)
    safe_call(message, *args, &block)
  end

  private

  def safe_call(message, *args, &block)
    trial     = 0
    max_trial = 5

    sleep_time      = 1
    next_sleep_time = 1

    begin
      # send message to the Twitter::Base object
      @twitter.__send__(message, *args, &block)

    rescue Errno::ECONNRESET, Errno::ETIMEDOUT, Timeout::Error,
      Twitter::InformTwitter, Twitter::Unavailable
      trial += 1
      raise Unavailable unless trial < max_trial

      # wait Fibonatti seconds (1, 1, 2, 3, ...)
      sleep sleep_time

      # update sleep time
      sleep_time, next_sleep_time =
        next_sleep_time, sleep_time + next_sleep_time
      retry
    end
  end
end

上のTwitterWrapperはmethod_missingを使い,TwitterWrapperに対するメソッド呼び出しをTwitter::Baseに転送している.そのため,TwitterWrapperはあたかも自分がTwitter::Baseであるかのように振る舞うことができる.また,メソッド転送の際にTwitter::Baseがサーバーエラー関連の例外をraiseした場合はTwitterWrapperが自動的にrescueしてリトライ処理を行う.5回トライしても駄目な場合はTwitterWrapper::Unavailableをraiseする.

Twitter::Baseのメソッド一覧を見てもObjectクラスのメソッドと衝突しているものはないのでこの方法で多分大丈夫だと思う.

使い方

使い方は簡単.Twitter::BaseオブジェクトからTwitterWrapperオブジェクトを作り,それをTwitter::Baseと同じように扱うだけ.

# create Twitter::Base object
oauth = Twitter::OAuth.new(consumer_token_key, consumer_secret)
oauth.authorize_from_access(access_token_key, access_secret)
twitter_base = Twitter::Base.new(oauth)

# create TwitterWrapper object
twitter = TwitterWrapper.new(twitter_base)

# call twitter_base's method
twitter.update("test")
mentions = twitter.mentions

注意

Twitter::Baseが投げる例外はTwitter::GeneralやTwitter::RateLimitExceededなど他にもあるので注意.このTwitterWrapperはリトライ処理を行うことで解決できる例外だけをrescueしている.