Elixir 入門 ~ MIX AND OTP ~ 実際にOTPを使ってプロセス管理をする
- ElixirのGetting StartもMix/OTPが終わり、次に進むのですが、復習がてら前にElixirで書いたコードを
Mix and OTP
の章でやったことを使って改善してみます。 - 元ネタはQiitaの自分の投稿 ElixirでSlackのbotを作ってHerokuで動かしてみる - Qiita
やりたいこと
- とにかくElixirでなんか作ろうと思って書いたのが上の記事ですが、書いていた時に微妙だなーと思ってた点が2つあります。
改善してみる
- コードはこちらにあがっています。
アプリケーションの起動まわり
mix.exs
を以下のように編集してapplication起動時のモジュールを指定して、Application
ビヘイビアを実装したMagicBot
モジュールを作成してそこに起動用のコードを書けば解決。
def application do [applications: [:logger, :slack], mod: {MagicBot, []}] end
- これで
mix run --no-halt
でアプリケーションが起動するようになりました。
Supervision Tree を使ってプロセスを分離する
今、プロセス1本で動いてるところをこういう
Supervision Tree
に変える。アプリケーションの
supervisor
Slack
のソケットを張り続けるプロセス- Botに実行させたいコマンドを管理する
supervisor
- 各コマンド。命令を受け取った時点で
supevisor
経由で動的に作成される。
- 各コマンド。命令を受け取った時点で
- Slackからメッセージを受け取ったらパースして
supervisor
経由でやりたいことを実行させるという感じですね。
作ってみた
- まずはアプリケーションの起点となるモジュールで全体を管理するSupervisorを起動
lib/magic_bot.ex
defmodule MagicBot do use Application def start(_type, _args) do MagicBot.Supervisor.start_link end end
- 次にBotのsupervisor
lib/magic_bot/supervisor.ex
defmodule MagicBot.Supervisor do use Supervisor def start_link(opts \\ []) do Supervisor.start_link(__MODULE__, :ok, opts) end # Define alias of process name @bot_name MagicBot.Bot @action_sup_name BotAction.Supervisor def init(:ok) do # Get API Token of Slack. api_key = case System.get_env("MAGICBOT_API_KEY") do nil -> Application.get_env(:MagicBot, :api_key) s -> s end # Make child process children = [ supervisor(BotAction.Supervisor, [[name: @action_sup_name]]), worker(MagicBot.Bot, [api_key, [name: @bot_name, sup_action: @action_sup_name]]) ] # 戦略は `one_for_one`でSlackとアクションのsupervisorを起動 supervise(children, strategy: :one_for_one) end end
- MagicBot.Supervisorに管理されるSlackのコネクションを張り続けるモジュール
defmodule MagicBot.Bot do use Slack def handle_connect(slack, state) do # Slackとの接続成功時に呼ばれるコールバック IO.puts "Connected as #{slack.me.name}" {:ok, state} end def handle_message(message = %{type: "message", text: _}, slack, state) do # Slackからメッセージを受け取った時に呼ばれるコールバック trigger = String.split(message.text, ~r{ | }) case String.starts_with?(message.text, "<@#{slack.me.id}>: ") do # @bot名 ~ できたら :respond を渡してactionのプロセスを開始 true -> BotAction.Supervisor.start_action(state[:sup_action], :respond, Enum.fetch!(trigger, 1), message, slack) # それ以外は :hear を渡して actionのプロセスを開始 false -> BotAction.Supervisor.start_action(state[:sup_action], :hear, hd(trigger), message, slack) end {:ok, state} end def handle_message(_message, _slack, state) do {:ok, state} end end
- Botの行動を管理するSupervisor。
lib/bot_action/supervisor.ex
defmodule BotAction.Supervisor do def start_link(opts \\ []) do Task.Supervisor.start_link(opts) end def start_action(supervisor, command, trigger, message, slack) do # Slackのプロセスから呼ばれ、このSupervisor配下に新しいプロセスを作成する。 # Task.Supervisorの子プロセスの戦略は `simple_one_for_one` になり、動的に追加できる。 # クラッシュ時は再起動されない Task.Supervisor.start_child(supervisor, fn -> case command do :respond -> BotAction.Action.respond(trigger, message, slack) :hear -> BotAction.Action.hear(trigger, message, slack) end end) end end
lib/bot_action/action.ex
defmodule BotAction.Action do def hear("lgtm", message, slack) do HTTPoison.start case URI.encode("http://www.lgtm.in/g") |> HTTPoison.get do {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> body |> Floki.find("#imageUrl") |> Floki.attribute("value") |> hd |> send_message(message, slack) {_, _} -> nil end end def hear(_, _, _) do end def respond("エリクサーほしい?", message, slack) do send_message("エリクサーちょうだい!\nhttp://img.yaplog.jp/img/01/pc/2/5/2/25253/1/1354.jpg", message, slack) end def respond(_, _, _) do end defp send_message(text, message, slack) do Slack.send_message(text, message.channel, slack) end end
- これで完成。
意図した通りに動くか確認
- lib/bot_action/action.exに以下の関数を追加してみる。
def hear("test1", message, slack) do # 単純にメッセージを返す send_message("test1", message, slack) end def hear("test2", message, slack) do # 5秒待ってからメッセージを返す :timer.sleep(5000) send_message("test2", message, slack) end def hear("test3", message, slack) do # 意図的に例外を起こす raise "oops" send_message("test3", message, slack) end
- botを起動してSlackに"test2"を流したあとに"test1"を流す。
test1はtest2のsleepに待たされることなく、即座にメッセージが返ってきたので、意図した通りに動いていてますね。
次に"test2"を流したあとに"test3"を流して"test3"のプロセスをクラッシュさせてみます。
test2はtest3のクラッシュの影響を受けずにメッセージを返していますね。test3のプロセスはメッセージを送る前に
raise
しているのでメッセージは返ってきません。という感じで
OTP
を使ってプロセス管理するとなかなかElixirっぽいコードになったかなという印象ですね。