ErlangVMの落としかた初級編

## 背景

ErlangVM は堅牢だといわれています。 
実際社内で運用している Erlang 製のゲーム課金・認証基盤は弊社の全ゲームからのアクセスを数年間落ちずにさばいています。
ここまで頑丈だと逆にどうやれば落とすことができるのかに興味が行くのは自然なことです。

今回は ErlangVMの落とし方を学ぶため、実際に落とすためのアプリケーションを Elixir で書いてみます。

## Supervisorの再起動制限

Supervisor の起動時オプションには max_restarts と max_seconds というオプションがあります。


https://hexdocs.pm/elixir/Supervisor.html#module-start_link-2-init-2-and-strategies

```
 max_restarts: 制限時間内に再起動する回数の上限　デフォルトは3
 max_seconds: max_restartsを指定する制限時間 デフォルトは5 
 ```

どうやら再起動回数には上限があり、デフォルトでは5秒以内に3回再起動したら Supervisor はそれ以上再起動せず沈黙するということのようです。
これを使えば ErlangVM を落とせそうです。


# サンプルアプリケーション

実際にErlangVMを落とすためのアプリケーションを書いてみます。

```elixir:application.ex
defmodule SupRestartLimit.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    worker_childlen = [
      %{id: :worker1, start: {SupRestartLimit.Worker, :start_link, [500]}},
      %{id: :worker2, start: {SupRestartLimit.Worker, :start_link, [700]}},
      %{id: :worker3, start: {SupRestartLimit.Worker, :start_link, [800]}},
    ]

    children = [
      %{
        id: SupRestartLimit.Worker.Supervisor,
        start: {Supervisor, :start_link, [worker_childlen, [strategy: :one_for_one, restart: :transient, max_restarts: 1, max_seconds: 10]]}
      }
    ]

    opts = [strategy: :one_for_one, name: SupRestartLimit.Supervisor, max_restarts: 3, max_seconds: 60]
    Supervisor.start_link(children, opts)
  end
end
```

ApplicationとリンクしたトップレベルのSupervisor, その下にWorker用のSupervisor(one_for_one)、その下に Worker(one_for_one)が 3 つあります。

```elixir:worker.ex
defmodule SupRestartLimit.Worker do
  use GenServer
  require Logger

  @counter_limit 5

  def start_link(interval) do
    GenServer.start_link(__MODULE__, [interval])
  end

  @impl GenServer
  def init([interval]) do
    state = %{counter: 0, interval: interval}
    Process.send_after(self(), :countup, interval)
    {:ok, state}
  end

  @impl GenServer
  def handle_info(:countup, %{counter: counter, interval: interval}=state) do
    Logger.debug "count_up: #{counter}"

    if counter >= @counter_limit do
      raise "counter limit exceeded"
    else
      Process.send_after(self(), :countup, interval)
    end
    new_state = put_in(state.counter, counter + 1)
    {:noreply, new_state}
  end
  def handle_info(_, state), do: {:noreply, state}
end
```

Worker は start_link の引数で指定された interval ごとに自身のstateの`:counter`をカウントアップし、5回カウントアップすると例外を投げます。
再起動戦略は `:transient` なので Supervisor による再起動がかかります。

## 実行

ちゃんとVMごと落とすために `iex -S mix` による実行ではなくパッケージして実行します。

```
-> mix release                                                                                                                                                                                                                           
Compiling 1 file (.ex)
==> Assembling release..
==> Building release sup_restart_limit:0.1.0 using environment dev
==> You have set dev_mode to true, skipping archival phase
==> Release successfully built!
    You can run it in one of the following ways:
      Interactive: _build/dev/rel/sup_restart_limit/bin/sup_restart_limit console
      Foreground: _build/dev/rel/sup_restart_limit/bin/sup_restart_limit foreground
      Daemon: _build/dev/rel/sup_restart_limit/bin/sup_restart_limit start

-> _build/dev/rel/sup_restart_limit/bin/sup_restart_limit foreground 

15:28:46.101 [debug] count_up: 0
15:28:46.301 [debug] count_up: 0
15:28:46.401 [debug] count_up: 0
15:28:46.602 [debug] count_up: 1
15:28:47.002 [debug] count_up: 1

-- 中略 --- 

15:29:02.228 [debug] count_up: 1                                   
                                                                   
15:29:02.425 [debug] count_up: 5                                   
                                                                   
15:28:48.607 [error] GenServer #PID<0.564.0> terminating                                                                               
** (RuntimeError) counter limit exceeded                           
    (sup_restart_limit) lib/sup_restart_limit/worker.ex:23: SupRestartLimit.Worker.handle_info/2           
    (stdlib) gen_server.erl:616: :gen_server.try_dispatch/4                                                                                                                                                                                                                   
    (stdlib) gen_server.erl:686: :gen_server.handle_msg/6                                                                                                                                                                                                                     
    (stdlib) proc_lib.erl:247: :proc_lib.init_p_do_apply/3
Last message: :countup
State: %{counter: 5, interval: 500}

=CRASH REPORT==== 3-Dec-2017::15:28:48 ===
  crasher:
    initial call: Elixir.SupRestartLimit.Worker:init/1
    pid: <0.564.0>
    registered_name: []
    exception error: #{'__exception__' => true,
                       '__struct__' => 'Elixir.RuntimeError',
                       message => <<"counter limit exceeded">>}
      in function  'Elixir.SupRestartLimit.Worker':handle_info/2 (lib/sup_restart_limit/worker.ex, line 23)
      in call from gen_server:try_dispatch/4 (gen_server.erl, line 616)
      in call from gen_server:handle_msg/6 (gen_server.erl, line 686)
    ancestors: [<0.563.0>,'Elixir.SupRestartLimit.Supervisor',<0.561.0>]
    message_queue_len: 0
    messages: []
    links: [<0.563.0>]
    dictionary: []
    trap_exit: false
    status: running
    heap_size: 610
    stack_size: 27
    reductions: 808
  neighbours:

=SUPERVISOR REPORT==== 3-Dec-2017::15:28:49 ===                    
     Supervisor: {<0.563.0>,'Elixir.Supervisor.Default'}           
     Context:    child_terminated                                  
     Reason:     {#{'__exception__' => true,                       
                    '__struct__' => 'Elixir.RuntimeError',         
                    message => <<"counter limit exceeded">>},      
                  [{'Elixir.SupRestartLimit.Worker',handle_info,2, 
                       [{file,"lib/sup_restart_limit/worker.ex"},{line,23}]},                                                          
                   {gen_server,try_dispatch,4,                     
                       [{file,"gen_server.erl"},{line,616}]},      
                   {gen_server,handle_msg,6,                       
                       [{file,"gen_server.erl"},{line,686}]},      
                   {proc_lib,init_p_do_apply,3,                    
                       [{file,"proc_lib.erl"},{line,247}]}]}       
     Offender:   [{pid,<0.565.0>},                                 
                  {id,worker2},                                    
                  {mfargs,{'Elixir.SupRestartLimit.Worker',start_link,[700]}},                                                         
                  {restart_type,permanent},                        
                  {shutdown,5000},                                 
                  {child_type,worker}]                             


=SUPERVISOR REPORT==== 3-Dec-2017::15:28:49 ===                    
     Supervisor: {<0.563.0>,'Elixir.Supervisor.Default'}           
     Context:    shutdown                                          
     Reason:     reached_max_restart_intensity                     
     Offender:   [{pid,<0.565.0>},                                 
                  {id,worker2},                                    
                  {mfargs,{'Elixir.SupRestartLimit.Worker',start_link,[700]}},                                                         
                  {restart_type,permanent},                        
                  {shutdown,5000},                                 
                  {child_type,worker}]  

=SUPERVISOR REPORT==== 3-Dec-2017::15:28:49 ===                    
     Supervisor: {local,'Elixir.SupRestartLimit.Supervisor'}       
     Context:    child_terminated                                  
     Reason:     shutdown                                          
     Offender:   [{pid,<0.563.0>},                                 
                  {id,'Elixir.SupRestartLimit.Worker.Supervisor'}, 
                  {mfargs,                                         
                      {'Elixir.Supervisor',start_link,             
                          [[#{id => worker1,                       
                              start =>                             
                                  {'Elixir.SupRestartLimit.Worker',
                                      start_link,                  
                                      [500]}},                     
                            #{id => worker2,                       
                              start =>                             
                                  {'Elixir.SupRestartLimit.Worker',
                                      start_link,
                                      [700]}},
                            #{id => worker3,
                              start =>
                                  {'Elixir.SupRestartLimit.Worker',
                                      start_link,
                                      [800]}}],
                           [{strategy,one_for_one},
                            {restart,transient},
                            {max_restarts,1},
                            {max_seconds,10}]]}},
                  {restart_type,permanent},
                  {shutdown,5000},
                  {child_type,worker}]
```

子の SupRestartLimit.Worker の再起動制限を超え、親である SupRestartLimit.Worker.Supervisor が shutdownされました。 その結果トップレベルの SupRestartLimit.Supervisor によって再起動されました。
さらに続けます。 

```
=SUPERVISOR REPORT==== 3-Dec-2017::15:29:40 ===
     Supervisor: {local,'Elixir.SupRestartLimit.Supervisor'}
     Context:    shutdown
     Reason:     reached_max_restart_intensity
     Offender:   [{pid,<0.599.0>},
                  {id,'Elixir.SupRestartLimit.Worker.Supervisor'},
                  {mfargs,
                      {'Elixir.Supervisor',start_link,
                          [[#{id => worker1,
                              start =>
                                  {'Elixir.SupRestartLimit.Worker',
                                      start_link,
                                      [500]}},
                            #{id => worker2,
                              start =>
                                  {'Elixir.SupRestartLimit.Worker',
                                      start_link,
                                      [700]}},
                            #{id => worker3,
                              start =>
                                  {'Elixir.SupRestartLimit.Worker',
                                      start_link,
                                      [800]}}],
                           [{strategy,one_for_one},
                            {restart,transient},
                            {max_restarts,1},
                            {max_seconds,10}]]}},
                  {restart_type,permanent},
                  {shutdown,5000},
                  {child_type,worker}]


15:29:40.020 [info]  Application sup_restart_limit exited: shutdown
{"Kernel pid terminated",application_controller,"{application_terminated,sup_restart_limit,shutdown}"}
Kernel pid terminated (application_controller) ({application_terminated,sup_restart_limit,shutdown})

Crash dump is being written to: erl_crash.dump...done

```
成功です。

ついにトップレベルSupervisorの再起動制限も超え、アプリケーションが終了し、ErlangVMもerl_crush.dumpを吐いてクラッシュしました。

# まとめ

- Supervisor は万能ではありません　専用の Supervisor の下にいるからといって無制限にプロセスをクラッシュしていいわけではありません。
再起動回数には上限があり、トップレベルの Supervisor の再起動制限を超えれば ErlangVM ごと落ちます。