上京エンジニアの葛藤

都会に染まる日々

Retryable コードリーディング

今年に入ってまた Ruby をメインで書くようになりました。
少しずつ Ruby 力を高めたいので、今回は Retryable という Gem をコードリーディングします。

github.com

コード量も少なくて読みやすいです。

概要

Retryable は例外発生時に指定した回数 retry してくれる便利な Gem です。
option がいくつか用意されていて特定の例外だけ retry するようにしたり、sleep time を設定できたりもします。

サンプルコード

簡単なサンプルコードを書いてみます。

require 'retryable'
require 'benchmark'

class SampleRetryable
  class NotFoundRequest < StandardError; end

  def execute
    c = 0
    Retryable.retryable(tries: 3, on: [NotFoundRequest], sleep: 3) do
      c += 1
      puts c
      res = Net::HTTP.get("example.com", "/")
    end
  end
end
require 'minitest/autorun'
require 'webmock'
require_relative '../src/sample_retryable'

class SampleRetryableTest < MiniTest::Test
  def test_sample
    WebMock.enable!
    WebMock.stub_request(:get, 'example.com').to_raise(SampleRetryable::NotFoundRequest)

    SampleRetryable.new.execute
  end
end

このテストコードでは request 時に SampleRetryable::NotFoundRequest というエラークラスを stub しているようにしています。

$ bundle exec ruby test/sample_retryable_test.rb
Run options: --seed 3048

# Running:

1
2
3
E

Finished in 6.018869s, 0.1661 runs/s, 0.0000 assertions/s.

このように 3回 request が行われ sleep が 3sec * 2 = 6sec されているのが分かります。
直感的に retry 処理が書けて便利です。

コードリーディング

先ほどのサンプルコードを実行した場合のコードを追います。

retryable/retryable.rb at 7fd390d6d8a8098db8839f3c320bebfd3ff2a604 · nfedyashev/retryable · GitHub

この retryable メソッドを読むことで一通り理解することができます。

まずは引数で渡された option の hash の validation を行います。

    def check_for_invalid_options(custom_options, default_options)
      invalid_options = default_options.merge(custom_options).keys - default_options.keys
      return if invalid_options.empty?
      raise ArgumentError, "[Retryable] Invalid options: #{invalid_options.join(', ')}"
    end

retryable/retryable.rb at 7fd390d6d8a8098db8839f3c320bebfd3ff2a604 · nfedyashev/retryable · GitHub

肝になるのはこのコードで yield でブロックを呼び出して、例外が発生時は *on_exception でキャッチしています。
そして sleep を実行し、retry 回数を count しながら retry メソッドを呼び出してもう一度 begin から繰り返します。

      begin
        opts[:log_method].call(retries, retry_exception) if retries > 0
        return yield retries, retry_exception
      rescue *not_exception
        raise
      rescue *on_exception => exception
        raise unless configuration.enabled?
        raise unless matches?(exception.message, matching)

        infinite_retries = tries == :infinite || tries.respond_to?(:infinite?) && tries.infinite? == 1
        raise if !infinite_retries && retries + 1 >= tries

        # Interrupt Exception could be raised while sleeping
        begin
          seconds = opts[:sleep].respond_to?(:call) ? opts[:sleep].call(retries) : opts[:sleep]
          opts[:sleep_method].call(seconds)
        rescue *not_exception
          raise
        rescue *on_exception
        end

        retries += 1
        retry_exception = exception
        opts[:exception_cb].call(retry_exception)
        retry
      ensure
        opts[:ensure].call(retries)
      end

retryable/retryable.rb at 7fd390d6d8a8098db8839f3c320bebfd3ff2a604 · nfedyashev/retryable · GitHub

コードもシンプルなので扱いやすい Gem だと思いました。
特に yield の使い方として参考になる実装でした。
context なども使えばより便利そうなので機会があったら使ってみたいです。