エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

gen_event でイベントを通知する/イベントをハンドリングする

Logger の backend を書いたときに利用した gen_event について調べたので、その覚書。

gen_event とは

Erlang が標準で提供しているモジュールです。 イベントをハンドリングする仕組みを提供してくれます。

複数のハンドラを登録しておくと、イベントがそれらのハンドラに通知されます。

と、いうわけで。書いてみます。

プロジェクトを用意する

gen_event を試すプロジェクトを用意します。 アプリケーションの起動時に gen_event のプロセスを起動したいので --sup オプションをつけて supervision tree の雛形を生成しておきます。

$ mix new notification --sup

gen_event のプロセスを起動するコードを追加する

lib/notification/application.ex を編集します。 children の内容を編集して gen_event を起動する設定を記述します。

defmodule Notification.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    children = [
      %{
        id: :gen_event,
        start: {
          :gen_event,
          :start_link,
          [{:local, Notification}]
        }
      }
    ]

    opts = [strategy: :one_for_one, name: Notification.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

start の値のタプルは、gen_event のプロセスを起動する関数の呼び出し

:gen_event.start_link({:local, Notification})

を表しています。

iex を起動して、Notification.Supervisor が監視しているプロセスの情報を取得すると、gen_event が起動していることがわかります。

$ iex -S mix
iex(1)> Supervisor.which_children(Notification.Supervisor)
[{:gen_event, #PID<0.136.0>, :worker, [:gen_event]}]

ハンドラを書く

ハンドラのファイル lib/notification/handler.ex を追加して gen_event のコールバックを実装したモジュールを記述していきます。

モジュールのふるまい @behaviour:gen_event を指定します。

defmodule Notification.Handler do
  @behaviour :gen_event
end

この状態でコンパイルすると必要なコールバック関数が実装されていないと警告が表示されます。

$ mix compile
Compiling 1 file (.ex)
warning: function handle_call/2 required by behaviour :gen_event is not implemented (in module Notification.Handler)
  lib/notification/handler.ex:1

warning: function handle_event/2 required by behaviour :gen_event is not implemented (in module Notification.Handler)
  lib/notification/handler.ex:1

warning: function init/1 required by behaviour :gen_event is not implemented (in module Notification.Handler)
  lib/notification/handler.ex:1

Generated notification app

handle_call/2, handle_event/2, init/1 の 3 つの関数が必須なことがわかります。 それ以外には、プロセスの終了時に呼び出される terminate/2 や、 gen_event 以外の要因のメッセージが発生したときに呼び出される handle_info/2 、コードが更新されたときに呼び出される code_change/3 があります。が、今回は最小限で実装します。

defmodule Notification.Handler do
  @behaviour :gen_event

  require Logger

  def init(args) do
    name = get_in(args, [:name])
    Logger.info("#{name} initialized")
    {:ok, %{name: name}}
  end

  def handle_call(request, state) do
    Logger.info("#{state.name} called with #{request}")
    {:ok, {:ok, request}, state}
  end

  def handle_event(event, state) do
    Logger.info("#{state.name} received #{event}")
    {:ok, state}
  end
end

ログを出力するだけの実装です。

ハンドラを登録する

iex でアプリケーションを起動します。

$ iex -S mix
iex(1)>

gen_event のプロセスは Notification という名前ですでに起動しているので、ハンドラを登録してみます。

iex(1)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler1")

22:14:30.726 [info]  Handler1 initialized
:ok

Handler1 という名前をつけたハンドラが登録されました。

ハンドラに通知する

この状態で通知を送ってみます。

iex(2)> :gen_event.notify(Notification, "Hi")
:ok

22:15:24.201 [info]  Handler1 received Hi

:gen_event.notify/2 で通知を送ると、handle_event/2 が呼び出されたことがわかります。

:gen_event.call/3 で呼び出すと、handle_call/2 が呼び出されます。 こちらの呼び出しはハンドラのモジュールを指定する必要があります。 同期呼び出しになるので、handle_call/2 が返した値が :gen_event.call/3 の戻り値になります。

:gen_event.call(Notification, Notification.Handler, "Hi")

22:19:38.486 [info]  Handler1 called with Hi
{:ok, "Hi"}

ハンドラのモジュールの関数を呼び出しているだけのようにも見えますが、ハンドラが登録されていない状態で呼び出すとエラーになります。ハンドラが登録されていないと呼び出せないことがわかります。

$ iex -S mix
iex(1)> :gen_event.call(Notification, Notification.Handler, "Hi")
{:error, :bad_module}

複数のハンドラを登録し通知する

iex を起動しなおして、ハンドラを 3 つ名前を変えて登録してみます。

$ iex -S mix
iex(1)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler1")

22:24:54.356 [info]  Handler1 initialized
:ok
iex(2)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler2")

22:24:56.649 [info]  Handler2 initialized
:ok
iex(3)> :gen_event.add_handler(Notification, Notification.Handler, name: "Handler3")

22:24:58.466 [info]  Handler3 initialized
:ok

通知を送ります。

iex(4)> :gen_event.notify(Notification, "Hello!")

22:25:47.489 [info]  Handler3 received Hello!
:ok

22:25:47.489 [info]  Handler2 received Hello!

22:25:47.489 [info]  Handler1 received Hello!

登録した 3 つのハンドラが呼び出されたことがわかります。ハンドラの実行は非同期ですので、ハンドラのログの出力と :gen_event.notify/2 の戻り値の表示が混ざって表示されています。

イベント源のコードを書く

ハンドラの登録や通知を簡単にするためのコードを書きます。

lib/notification.ex を編集して :gen_event の関数の呼び出しを隠す関数を書きます。:gen_event のプロセスはこのファイルで記述するモジュールの名前 Notification で登録しているので、__MODULE__ マクロで指定しています。

defmodule Notification do
  def add_handler(name) do
    :gen_event.add_handler(__MODULE__, Notification.Handler, name: name)
  end

  def notify(event) do
    :gen_event.notify(__MODULE__, event)
  end
end

実行します。

$ iex -S mix
iex(1)> Notification.add_handler("Handler1")

22:30:42.760 [info]  Handler1 initialized
:ok
iex(2)> Notification.add_handler("Handler2")

22:30:44.872 [info]  Handler2 initialized
:ok
iex(3)> Notification.add_handler("Handler3")

22:30:46.296 [info]  Handler3 initialized
:ok
iex(4)> Notification.notify("Hello!")

22:30:59.376 [info]  Handler3 received Hello!
:ok

22:30:59.376 [info]  Handler2 received Hello!

22:30:59.376 [info]  Handler1 received Hello!

いつか読むはずっと読まない:ソラリスの陽のもとに

ポーランド語原典からの翻訳版として 国書刊行会 が 2004 年に刊行した単行本を早川書房 が 2015 年に文庫化したもの。

有名な作品ですが、それまではロシア語訳版の翻訳だったんですね。ようやく手にしました。