Julia - retry 機構

環境

  • Julia 1.3.0

retry

ある処理がエラーになってしまったとき、時間を置いて再度実行したらエラーが出ないことはよく有ることです。例えばサーバーにうまく接続できないとか。 そんなときに使うのが retry 機構です。retry 機構を使うことでエラー時の再実行を容易にすることが出来ます。

Julia の retry 機構は以下のようなインターフェースになっています。

retry(f;  delays=ExponentialBackOff(), check=nothing) -> Function

関数 f が実際に実行される処理、delays が再実行する間隔と回数、check はどのエラーによって再実行するかをコントロールするときに指定するもの(っぽい)です。

https://docs.julialang.org/en/v1/base/base/#Base.retry

retry() の返り値は第1引数の関数 f に retry 機構を付与したものと考えればよいと思います。

以下の例は1秒間隔で最大3回関数 f を実行

julia> x = 0
0

julia> function f()
           global x += 1
           if x < 3
               println("x = ", x)
               error()
           else
               return println("Hello World")
           end
       end
f (generic function with 1 method)

julia> retry(f, delays=[1, 1, 1])()
x = 1
x = 2
Hello World

do ... end ブロックを使って以下のようにも書けます。ある処理の一部分に retry 機構を付けたい場合はこちらのほうが便利かもしれません。

julia> retry(delays=[1, 1, 1]) do
           global x += 1
           if x < 3
               println("x = ", x)
               error()
           else
               return println("Hello World")
           end
       end()   # retry の返り値は関数なので最後に `([args...])` を忘れずに
x = 1
x = 2
Hello World

実用的にはこんな感じでしょうか。

retry(delays=[1, 1, 1]) do url
    HTTP.request("GET", url)
end("http://httpbin.org/ip")

Exponential BackOff

待つ間隔を指数関数的に増やす場合は ExponentialBackOff を使います。

ExponentialBackOff(; n=1, first_delay=0.05, max_delay=10.0, factor=5.0, jitter=0.1)

https://docs.julialang.org/en/v1/base/base/#Base.ExponentialBackOff

n が retry する回数で、k回目の retry 時の待ち時間は以下の式で与えられます。

{ \displaystyle
t = \min
\left(
  \text{first_delay} \times \text{factor}^{( (k-1) + \mathrm{uniform}(-\text{jitter},~\text{jitter}) )}, \text{max_delay} \right)
}

ここで {\mathrm{uniform}(a,b)}区間 {[a, b)} の一様乱数 { \displaystyle \mathrm{uniform}(a,b) = (b - a) \times \mathrm{rand}() + a}

AWS でのエラーの再試行とエクスポネンシャルバックオフ - AWS 全般のリファレンス

julia> ExponentialBackOff(n=5, first_delay=0.05, max_delay=10, jitter=0) |> collect
5-element Array{Float64,1}:
  0.05
  0.25
  1.25
  6.25
 10.0 

julia> [0.05 * 5^(i-1) for i in 1:5]
5-element Array{Float64,1}:
  0.05
  0.25
  1.25
  6.25
 31.25

Document の但し書きについて

公式 Document を見ると

Before Julia 1.2 this signature was restricted to f::Function.

という但し書きがあります。ようするに retry の定義が 1.2 より前では

retry(f::Function;  delays=ExponentialBackOff(), check=nothing) -> Function

だったということですが、

Return an anonymous function that calls function f.

とあるのだから、f::Function で一見問題ない気がします。

issue や PR をちゃんと見ていないので正確ではありませんが、おそらく Function-like objects などを取れるようになったということを言いたいのかなと私は思いました。

https://docs.julialang.org/en/v1/manual/methods/#Function-like-objects-1

julia> struct Polynomial{R}
           coeffs::Vector{R}
       end

julia> function (p::Polynomial)(x)
           v = p.coeffs[end]
           for i = (length(p.coeffs)-1):-1:1
               v = v*x + p.coeffs[i]
           end
           return v
       end

julia> (p::Polynomial)() = p(5)

julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])

# Julia 1.2 未満
julia> retry(p)
ERROR: MethodError: no method matching retry(::Polynomial{Int64})
Closest candidates are:
  retry(::Function; delays, check) at error.jl:198
Stacktrace:
 [1] top-level scope at none:0

# Julia 1.2 以降
julia> retry(p)
#50 (generic function with 1 method)