Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyで例外発生時に処理を再試行する方法:retryキーワードとContinuationの活用術

すべてのエラーが致命的というわけではありません。中には、単に「もう一度試せば解決する」ことを示しているだけのものもあります。幸い、Rubyには「再試行」を簡単に実現できる興味深い仕組みがいくつか用意されています。ただし、そのすべてが自明だったり、広く知られていたりするわけではありません。この記事では、これらの仕組みと、実務での活用方法について詳しく見ていきます。

retryキーワードの基本

これは一見当たり前の話に思えるかもしれませんが、それはその存在を知っている人だけの話です。筆者自身も、Rubyでのキャリアがある程度進んでから、便利なretryキーワードの存在を初めて知りました。

retryはRubyの例外処理システムに組み込まれている機能です。使い方は非常にシンプルで、rescueブロック内でretryを使うと、例外が発生したコード部分がもう一度実行されます。実際の例を見てみましょう。

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

# ... 出力結果:
# try #0
# try #1
# try #2

ここで注意すべきポイントがいくつかあります。

  • retryが呼び出されると、beginからrescueまでの間のコードがすべて再実行されます。「中断した箇所から処理を再開する」といった動作は一切行われないので注意してください。

  • 再試行回数を制限する仕組みを実装しないと、無限ループに陥ってしまいます。

  • beginブロックとrescueブロックのどちらのコードも、親スコープにある同じretries変数にアクセスできます。

retryの課題

retryは非常に便利ですが、いくつか制限があります。最大の問題は、beginブロック全体が再実行されてしまう点です。場合によっては、それが望ましくないこともあります。

例えば、1つのメソッド呼び出しでTwitterやFacebookなど複数のサービスに投稿できるgemを使っているとしましょう。イメージとしては次のようなものです。

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

# ... Twitter APIへ投稿
# ... Facebook APIへ投稿
# ... その他のAPIへ投稿

もし途中のAPIが応答しなかった場合、gemはSocialMedia::TimeoutErrorを発生させて処理を中断します。ここでこの例外を捕捉してretryすると、最初からやり直しになるため、重複した投稿が発生してしまいます。

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

# ... Twitter APIへ投稿
# Facebookでエラー
# ... Twitter APIへ投稿(重複)
# Facebookでエラー
# ... Twitter APIへ投稿(また重複)
# 以降ずっと同じ繰り返し

「Facebookはスキップして、残りのAPIへの投稿を続けてほしい」とgemに指示できたら理想的だと思いませんか?

幸いなことに、Rubyではまさにそれが実現できます。

※補足:もちろん、この問題の根本的な解決策はソーシャルメディアライブラリ自体の設計を見直すことです。しかし、これから紹介するテクニックのユースケースは、この例だけにとどまりません。

Continuationによる解決

Continuationは多くの人にとってとっつきにくい存在です。しかし、それは単に使用頻度が低く、見た目が少し奇妙だからにすぎません。基本さえ理解すれば、実はとてもシンプルなものです。

Continuationは、コードにおける「セーブポイント」のようなものです。ゲームのセーブと同じイメージで、別の処理を実行してからセーブポイントに戻ると、離れた時点の状態がそのまま保たれています。

…完璧な比喩ではありませんが、だいたいそんな感じです。コードを見てみましょう。

require "continuation"
counter = 0
continuation = callcc { |c| c } # セーブポイントを定義
puts(counter += 1)
continuation.call(continuation) if counter < 5 # セーブポイントへジャンプして戻る

少し奇妙な点に気づいたかもしれません。順番に見ていきましょう。

  • Continuationオブジェクトの生成にはcallccメソッドを使用します。これには洗練されたオブジェクト指向的な構文が用意されていません。

  • continuation変数が最初に代入される際には、callccのブロックの戻り値が設定されます。そのため、ブロックが必須になります。

  • セーブポイントにジャンプして戻るたびに、continuation変数にはcallメソッドに渡した引数が代入されます。そのため、一見奇妙なcontinuation.call(continuation)という構文を使っているのです。

例外へのContinuationの組み込み

それでは、Continuationを使ってすべての例外にskipメソッドを追加してみましょう。以下の例は、期待される動作を示しています。例外をrescueした際にskipを呼び出せるようにし、例外が発生しなかったかのようにコードの続きを実行させます。

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

# ... "The exception was ignored" が出力される

これを実現するには、多少「罪」を犯す覚悟が必要です。Exceptionはただのクラスなので、モンキーパッチを適用してskipメソッドを追加できます。

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

次に、発生するすべての例外に対してcontinuation属性を設定する必要があります。実はraiseもただのメソッドなので、オーバーライドすることができます。

ちなみに、以下のコードはAdvi氏の優れたスライド資料「Things You Didn't Know About Exceptions」からほぼそのまま引用したものです。筆者自身、これより良い実装方法を思いつくことができませんでした。

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

これで、任意の例外に対してskipメソッドを呼び出せるようになり、まるで例外が最初から発生しなかったかのように処理を続行できます。

実務で使う際の注意点

なお、callccは現在非推奨の扱いになっており、Ruby 3系では標準ライブラリから外れています。利用するには別途continuation gemのインストールが必要です。また、Continuationは制御フローを複雑にしやすいため、本番環境での利用は慎重に検討し、単純なリトライであればretryretryableなどのgem、あるいはFiberベースの設計を優先することをおすすめします。

  1. Rubyのマトリックス(行列)とは?配列・Matrixクラス・Daruを使いこなす完全ガイド

    マトリックス(行列)とは、表計算ソフトのようなデータを格納・操作するための2次元(2D)配列のことです。 マトリックスが活躍する場面: チェスやオセロなど、テーブルゲームの盤面を表現する 統計処理やデータ分析を行う グラフやプロットを生成する マトリックスは非常に強力なデータ構造なので、その使い方をマスターしておくと開発の幅が大きく広がります。この記事では、Rubyでマトリックスを扱う3つの方法——配列、Matrixクラス、そしてDaru gem——を順番に解説していきます。 Rubyでマトリックスを作成する方法 最もシンプルな方法は、配列の入れ子(ネスト)を使ってマトリックスを作ることで

  2. RubyのStructとOpenStructの使い方を徹底解説!値オブジェクトを簡単に作る方法

    Rubyには、関連する属性をまとめて保持する「値オブジェクト」を手軽に作れるStructという組み込みクラスが用意されています。本記事では、Structの基本的な使い方から注意点、そしてOpenStructとの違いまで詳しく解説します。 そもそもStructとは何か? StructはRubyに組み込まれたクラスで、新しいクラスを動的に生成し、値オブジェクト(Value Object)を作成するために使われます。値オブジェクトとは、関連性のある複数の属性をひとまとめにして扱うためのオブジェクトです。 具体例を挙げてみましょう。 2つの座標(xとy)を持つPointというデータを表したい場合、この