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

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

LiveViewでTaskの結果はhandle_infoで受ければよいという話

LiveViewでTaskの結果はhandle_infoで受ければよいという話

ElixirWeekly の何号だったか失念してしまったのですが。

LiveView のプロセスで、非同期処理を Task.async/1 で実行したならば、Task.async/1 が終了時に送信するメッセージを受け取ればよい、というお話。

プロセスのふるまいを学んだときに、どのようなメッセージが送られるか問いことを覚えたはずなのに、文脈が変わっただけでこうも気付けなくなるものかと思い知った次第。

おさらい Task.async/1

Task.async/1 を実行すると、Task 構造体が返ります。

構造体の中のキー :ref にタスクを識別するリファレンスが格納されています。

iex(1)> Task.async(fn -> :timer.sleep(100); 123 end)
%Task{
  owner: #PID<0.109.0>,
  pid: #PID<0.111.0>,
  ref: #Reference<0.1023354925.2953576457.207757>
}

タスクが終了すると、リファレンスとタスクの最後の値のペアがメッセージとして送られてきます。

最後にタスクのプロセスが終了したことの通知 :DOWN が送られます。

iex(2)> flush
{#Reference<0.1023354925.2953576457.207757>, 123}
{:DOWN, #Reference<0.1023354925.2953576457.207757>, :process, #PID<0.111.0>,
 :normal}
:ok

これをふまえて

初期状態 stopping のときに START ボタンを押すと、starting と表示されのち1秒後に running の表示に換わるサンプルです。

Task.async/2 の戻り値に含まれるリファレンスを保存しておきます。 受信したメッセージにタスクのリファンレンスが含まれていれば、先に起動したタスクの結果のメッセージなので、処理をします。

プロセス終了時の :DOWN を含むメッセージも送られてくるので、そのメッセージにマッチする handle_info/2 を用意しておかなければなりません。 ここでは他のすべてのメッセージは無視するようにしています。

defmodule MyAppWeb.PageLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, status: "stopped", ref: nil)}
  end

  def render(assigns) do
    ~H"""
    <button phx-click="start">start</button>
    <div><%= @status %></div>
    """
  end

  # ボタンの phx-click="start" のイベントハンドラ
  def handle_event("start", _, socket) do
    task = Task.async(fn ->
      # (時間のかかる処理の代わり)
      Process.sleep(100)
      123
    end)

    {:noreply, assign(socket, status: "starting", ref: task.ref)}
  end

  # タスクの結果を受け取る
  # (socket に格納したリファレンスと受信したメッセージに含まれるリファレンスが一致をチェックする)
  def handle_info({ref, _result}, socket) when socket.assigns.ref == ref do
    {:noreply, assign(socket, status: "running")}
  end

  # :DOWN のメッセージ等を無視する
  def handle_info(_, socket) do
    {:noreply, socket}
  end
end