GenServerのタイムアウトの挙動

ElixirGenServer のタイムアウトを中心に例外処理の構文を実験した

言語 バージョン
erlang 20.2
elixir 1.6.6

GenServer.call/3 でのタイムアウトには exit が使われる。これは rescue では拾えないので catch が必要になる。 GenServer のタイムアウト設定に :infinity を与えることで待ち続けることが可能になっている。 これは Task, Agent など応用的なモジュールでもそのまま利用できる。 タイムアウト設定で不正な値(:invalid)を与えると %ErlangError{} が投げられるため rescue で拾うことができる。

Agentモジュールの確認

適当にスクリプトを書いて実験した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
Agent.start_link(fn -> 0 end, name: Sample)
|> IO.inspect()

defmodule ExampleCode do
  def agent_timeout(update_timeout_msec, sleep_time \\ 1000) do
    try do
      result = Agent.update(
        Sample,
        fn state ->
          :timer.sleep(sleep_time)
          state + 1
        end,
        update_timeout_msec
      )
      {:ok, update_timeout_msec, sleep_time, result}
    catch
      :exit, e ->
        {:exit, update_timeout_msec, sleep_time, e}
    end
  end
end
1
iex ./agent-timeout.exs -s IEx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
iex(1)> ExampleCode.agent_timeout(:infinity, 1)
{:ok, :infinity, 1, :ok}
iex(2)> ExampleCode.agent_timeout(:infinity, 10)
{:ok, :infinity, 10, :ok}
iex(3)> ExampleCode.agent_timeout(:infinity, 100)
{:ok, :infinity, 100, :ok}
iex(4)> ExampleCode.agent_timeout(:infinity, 10000)
{:ok, :infinity, 10000, :ok}
iex(5)> ExampleCode.agent_timeout(2000, 10000)
{:exit, 2000, 10000,
 {:timeout,
  {GenServer, :call,
   [
     Sample,
     {:update, #Function<0.57866094/1 in ExampleCode.agent_timeout/2>},
     2000
   ]}}}

Task

同じく適当にスクリプトを書いて実験した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
defmodule ExampleCode do

  def example_await(timeout, sleep_time \\ 1000) do
    try do
      result = Task.async(fn ->
        :timer.sleep(sleep_time)
        :ok
      end)
      |> Task.await(timeout)
      {:ok, timeout, sleep_time, result}
    catch
      :exit, e -> {:exit, timeout, sleep_time, e}
    end
  end

  def example_yield(timeout, sleep_time \\ 1000) do
    try do
      result = Task.async(fn ->
        :timer.sleep(sleep_time)
        :ok
      end)
      |> Task.yield(timeout)
      {:ok, timeout, sleep_time, result}
    catch
      :exit, e -> {:exit, timeout, sleep_time, e}
    end
  end
end
1
iex ./task-timeout.exs -s IEx
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
iex(1)> ExampleCode.example_await(1000, 500)
{:ok, 1000, 500, :ok}
iex(2)> ExampleCode.example_await(100, 500)
{:exit, 100, 500,
 {:timeout,
  {Task, :await,
   [
     %Task{
       owner: #PID<0.90.0>,
       pid: #PID<0.94.0>,
       ref: #Reference<0.1796741474.2735472644.117839>
     },
     100
   ]}}}
iex(3)> ExampleCode.example_await(:infinity, 500)
{:ok, :infinity, 500, :ok}
iex(4)> ExampleCode.example_yield(:infinity, 500)
{:ok, :infinity, 500, {:ok, :ok}}
iex(5)> ExampleCode.example_yield(100, 500)
{:ok, 100, 500, nil}
iex(6)> ExampleCode.example_yield(1, 500)
{:ok, 1, 500, nil}
iex(7)> try do
...(7)>   ExampleCode.example_yield(:invalid, 500)
...(7)> rescue
...(7)>   e in ErlangError -> e
...(7)> end
%ErlangError{original: :timeout_value}

timeout として不正な値を入れると :timeout_value が投げられる。 Elixir からは ErlangError にラップされている。

補足: Elixirの例外捕捉

構文として try/rescue/catch/else/after がある。 after は制御構文としての実行はされるがプロセス自身の異常終了に対応はしないので注意が必要。 下の通り、 rescuecatch に優先して評価される。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
iex(11)> try do
...(11)>   ExampleCode.example_yield(:invalid, 500)
...(11)> rescue
...(11)>   e -> {:rescue, e}
...(11)> catch
...(11)>   :error, e -> {:catch, e}
...(11)> end
{:rescue, %ErlangError{original: :timeout_value}}
iex(12)> try do
...(12)>   ExampleCode.example_yield(:invalid, 500)
...(12)> catch
...(12)>   :error, e -> {:catch, e}
...(12)> rescue
...(12)>   e -> {:rescue, e}
...(12)> end
warning: "catch" should always come after "rescue" in try
  iex:12

{:rescue, %ErlangError{original: :timeout_value}}
comments powered by Disqus