- この記事はElikir Advent Calendar 2015の7日目の記事です。
ETSとは
- Erlang Term Storage の略で、etsモジュールを経由してin memoryなテーブルにtupleを保存できる仕組みです。
- 子プロセス間でデータを共有できますが、ネットワーク越しの共有はできません。
- ドキュメントはこちら。 http://www.erlang.org/doc/man/ets.html
REPLから使ってみる
-
iex
でREPLを起動しデータを登録したり取り出してみたりします。
新しいテーブルを作成
iex(1)> table = :ets.new(:temp_table, [:set, :protected])
8211
-
:ets.new/2
で新しいテーブルを作成してETSのPIDが返ります。1個目の引数はテーブル名のatom、2個目はテーブルの定義をatomのlistで設定します。 -
:set
はテーブルの定義を指定していて、同一のキーは登録不可になります。他だと:bag
はキーの重複ありなど種類に応じて使い分けられます。 -
:protected
はアクセスの範囲を指定しています。この場合だと書き込みはテーブルを作成したプロセスのみ、読み取りはオープンという具合です。他にはpublic
、private
があります。
読み書き
iex(2)> :ets.insert(table, {:foo, "hoge"})
true
iex(3)> :ets.lookup(table, :foo)
[{:foo, "hoge"}]
iex(4)> :ets.lookup(table, :bar)
[]
iex(5)> :ets.insert(table, {:foo, "hogehoge"})
[{:foo, "hogehoge"}]
iex(6)> :ets.insert_new(table, {:foo, "hogehogehoge"})
false
iex(7)> :ets.insert_new(table, {:bar, "fuga"})
{:bar, "fuga"}
-
insert
で"foo"を登録し、lookup
で"foo"を探して存在するので取得できました。一方、存在しない"bar"のlookup
は空のlistが返ってきています。 - 2回目の
insert
ではすでに同じキー名が存在する場合は上書きを、insert_new
の場合はすでにキーが存在している場合はfalseが帰り、登録はできていません。新規登録であれば成功します。 - テーブルの登録時に形式を
ordered_set
にするとlast/1
やfirst/1
などが使えるようになります。
何かつくってみる
- 以前、ElixirでSlackのBotを作るというのを書いて遊んでいたのですが、これに手をいれてみます。
- lgtmが流れたらlgtm.inからランダムで画像を拾って流すというbotに、呼ばれた回数をETSに保存する処理を追加します。
コード
- まずはBotのSupervisorを起動するところでETSのテーブルを作成してPIDを取得します。取得したIDはBotのstateで持ち続けるようにします。
lib/magic_bot/supervisor.ex
# ETSを作成。アクセスは各Botの子プロセスから読み書きされることを想定しているのでPublic。
table = :ets.new(:work_bot, [:set, :public])
# BotのWorkerの引数のlistに作成したETSのプロセスを追加
children = [
supervisor(BotAction.Supervisor, [[name: @action_sup_name]]),
worker(MagicBot.Bot, [api_key, [name: @bot_name, sup_action: @action_sup_name, ets: table]])
]
supervise(children, strategy: :one_for_one)
- Botの子プロセスにETSのPIDを渡す
lib/lib/bot_action/supervisor.ex
def start_action(state, command, trigger, message, slack) do
Task.Supervisor.start_child(state[:sup_action], fn ->
# stateからetsを取り出して引数に追加
case command do
:respond -> BotAction.Action.respond(trigger, message, state[:ets], slack)
:hear -> BotAction.Action.hear(trigger, message, state[:ets], slack)
end
end)
end
- Botの行動を修正
lib/bot_action/action.ex
def hear("lgtm", message, ets, slack) do
# lgtm が呼ばれたらETSに呼ばれた回数をカウントアップして保存
:ets.insert(ets, {:lgtm_called_count, get_lgtm_called_count(ets) + 1})
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
# lgtmcountを話しかけたらETSからlgtmが呼ばれた回数を取得してslackに流すactionを追加
def respond("lgtmcount", message, ets, slack) do
send_message("いまのとこ #{get_lgtm_called_count(ets)} 回のlgtm", message, slack)
end
# ETSからlgtmの呼ばれた回数を取得する関数を追加
defp get_lgtm_called_count(ets) do
case :ets.lookup(ets, :lgtm_called_count) do
[] -> 0
x -> x[:lgtm_called_count]
end
end
動きを確認してみる
- Herokuにデプロイして確認します。ローカルでも
mix run --no-halt
で確認できます。
- 最初は0回と言われていて、lgtmを読んだあとは1回と表示されていますね。意図したとおりにカウントアップしているようです。
- 最初に書いたとおりETSはin memoryなストレージなのでbotが再起動されたら、ストレージもリセットされます。永続的にしたいのであればそもそもDBに保存するようにするか、プロセスが死んだときにETSから取り出して保存みたいなコードを書くことになるかと思います。
- コードはこちら https://github.com/rei-m/magic_bot まだまだElixir触り始めたばかりですが盛り上がっていくといいですね!