今年に入ってまた Ruby をメインで書くようになりました。
少しずつ Ruby 力を高めたいので、今回は Retryable という Gem をコードリーディングします。
コード量も少なくて読みやすいです。
概要
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 なども使えばより便利そうなので機会があったら使ってみたいです。