LoginSignup
23
21

More than 5 years have passed since last update.

ElixirからETSを使う

Posted at
  • この記事は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はアクセスの範囲を指定しています。この場合だと書き込みはテーブルを作成したプロセスのみ、読み取りはオープンという具合です。他にはpublicprivateがあります。

読み書き

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/1first/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で確認できます。

スクリーンショット 2015-12-05 16.28.10.png

  • 最初は0回と言われていて、lgtmを読んだあとは1回と表示されていますね。意図したとおりにカウントアップしているようです。
  • 最初に書いたとおりETSはin memoryなストレージなのでbotが再起動されたら、ストレージもリセットされます。永続的にしたいのであればそもそもDBに保存するようにするか、プロセスが死んだときにETSから取り出して保存みたいなコードを書くことになるかと思います。
  • コードはこちら https://github.com/rei-m/magic_bot まだまだElixir触り始めたばかりですが盛り上がっていくといいですね!
23
21
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
21