Elixir:ロギングプロセス-実践ガイド



Elixir



(およびもちろんもちろんErlang



)のプロセスは一意のプロセス識別子 -pidを使用して識別されます。

これらを使用してプロセスとやり取りします。 メッセージはpid



よう送信され、仮想マシン自体がこれらのメッセージを正しいプロセスに配信します。

ただし、場合によっては、 pid



に対する過度の信頼が重大な問題につながる可能性があります。

たとえば、死んだプロセスのpid



保存したり、 pid



作成を抽象化するSupervisor



を使用したりできます。したがって、どのpid



を持っているかさえわかりません( per :また、 Supervisor



では、別のpid



プロセスを再起動できます。これについては一切知りません)。

簡単なアプリケーションを作成して、直面する可能性のある問題と、これらの問題をどのように解決するかを見てみましょう。







レジストリなしで始める



最初の例では、簡単なチャットを作成します。 mix



プロジェクトを作成することから始めましょう。







 $ mix new chat
      
      





この記事のすべての例で使用する、絶対に標準的なGenServer



作成しましょう。







 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer # API def start_link do GenServer.start_link(__MODULE__, []) end def add_message(pid, message) do GenServer.cast(pid, {:add_message, message}) end def get_messages(pid) do GenServer.call(pid, :get_messages) end # SERVER def init(messages) do {:ok, messages} end def handle_cast({:add_message, new_message}, messages) do {:noreply, [new_message | messages]} end def handle_call(:get_messages, _from, messages) do {:reply, messages, messages} end end
      
      





そのようなコードがなじみのない、または理解できないと思われる場合は、OTPに関する優れたパラグラフが記載されているElixir



の作業開始をお読みください。


mix



環境でiex



セッションをiex



し、サーバーでの作業を試みましょう。







 $ iex -S mix iex> {:ok, pid} = Chat.Server.start_link {:ok, #PID<0.107.0>} iex> Chat.Server.add_message(pid, "foo") :ok iex> Chat.Server.add_message(pid, "bar") :ok iex> Chat.Server.get_messages(pid) ["bar", "foo"]
      
      





このステップのコードはこのコミットにあります。


この段階では、すべてが素晴らしく、素晴らしいだけです。 pid



を取得し、送信する各メッセージ( add_message/2



およびget_messages/1



)に対してこのpid



を渡します。すべてが予想get_messages/1



機能し、退屈さえします。

しかし、 Supervisor



を追加しようとすると楽しみが始まります...







とてもいい:私はSupervisor



です!



そのため、何らかの理由でChat.Server



プロセスChat.Server



死に絶えています。 私たちは空のコールドiex



セッションにiex



ており、新しいプロセスを開始し、そのpid



を取得し、この新しいpid



メッセージを書き込む以外に選択肢はありません。 Supervisor



作成しましょう-このような些細なことを心配する必要はありません!







 # ./lib/chat/supervisor.ex defmodule Chat.Supervisor do use Supervisor def start_link do Supervisor.start_link(__MODULE__, []) end def init(_) do children = [ worker(Chat.Server, []) ] supervise(children, strategy: :one_for_one) end end
      
      





さて、 Supervisor



作成Supervisor



非常に簡単です。 しかし、サーバーの動作モデルが変わらない場合、問題が発生します。 結局のところ、 Chat.Server



プロセスを自分で開始するのではなく、 Supervisor



がこれを行ってくれます。 したがって、 pid



プロセスにはアクセスできません。







これはバグではなく、 Supervisor



などのOTP



パターンの機能です。 子プロセスのpid



アクセスすることはできません。予期せず(ただし、もちろん必要な場合のみ)プロセスを再起動できますが、実際にはプロセスをpid



し、新しいpid



新しいpid



を作成します。







プロセス名を登録する



Chat.Server



プロセスChat.Server



アクセスするには、プロセスを指す方法を考え出す必要があります。もう一方はpid



はありません。 Supervisor



を介してプロセスが再起動された場合でも(つまり、 pid



が変更された場合でも)ポインターが保存されるように、ポインターが必要です。

そして、そのようなポインターは



と呼ばれます!







まず、 Chat.Server



変更しChat.Server









 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer def start_link do # We now start the GenServer with a `name` option. GenServer.start_link(__MODULE__, [], name: :chat_room) end # And our function doesn't need to receive the pid anymore, # as we can reference the process with its unique name. def add_message(message) do GenServer.cast(:chat_room, {:add_message, message}) end def get_messages do GenServer.call(:chat_room, :get_messages) end # ... end
      
      





変更-このコミットで


これですべてが同じように機能するはずですが、より良いだけです-このpid



どこにでも渡さないでください:







 $ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.94.0>} iex> Chat.Server.add_message("foo") :ok iex> Chat.Server.add_message("bar") :ok iex> Chat.Server.get_messages ["bar", "foo"]
      
      





プロセスが再起動しても、同じ方法でアクセスできます。







 iex> Process.whereis(:chat_room) #PID<0.111.0> iex> Process.whereis(:chat_room) |> Process.exit(:kill) true iex> Process.whereis(:chat_room) #PID<0.114.0> iex> Chat.Server.add_message "foo" :ok iex> Chat.Server.get_messages ["foo"]
      
      





さて、私たちの現在のタスクについては、問題は解決されているように見えますが、もっと複雑な(そして実際のタスクにより近い)ことを試してみましょう。







動的プロセス作成



複数のチャットルームをサポートする必要があるとします。 クライアントは名前を持つ新しいルームを作成でき、希望するルームにメッセージを送信できることを期待しています。 その場合、インターフェースは次のようになります。







 iex> Chat.Supervisor.start_room("first room") iex> Chat.Supervisor.start_room("second room") iex> Chat.Server.add_message("first room", "foo") iex> Chat.Server.add_message("second room", "bar") iex> Chat.Server.get_messages("first room") ["foo"] iex> Chat.Server.get_messages("second room") ["bar"]
      
      





上から始めて、これをすべてサポートするようにSupervisor



を変更しましょう。







 # ./lib/chat/supervisor.ex defmodule Chat.Supervisor do use Supervisor def start_link do # We are now registering our supervisor process with a name # so we can reference it in the `start_room/1` function Supervisor.start_link(__MODULE__, [], name: :chat_supervisor) end def start_room(name) do # And we use `start_child/2` to start a new Chat.Server process Supervisor.start_child(:chat_supervisor, [name]) end def init(_) do children = [ worker(Chat.Server, []) ] # We also changed the `strategty` to `simple_one_for_one`. # With this strategy, we define just a "template" for a child, # no process is started during the Supervisor initialization, # just when we call `start_child/2` supervise(children, strategy: :simple_one_for_one) end end
      
      





そして、 start_link



関数で名前を受け入れるようにしましょう:







 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer # Just accept a `name` parameter here for now def start_link(name) do GenServer.start_link(__MODULE__, [], name: :chat_room) end #... end
      
      





変更-このコミットで


そして、ここに問題があります! 複数のChat.Server



プロセスを使用できますが、それらすべてに:chat_room



という名前を:chat_room



ことはできません。 トラブル...







 $ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.107.0>} iex> Chat.Supervisor.start_room "foo" {:ok, #PID<0.109.0>} iex> Chat.Supervisor.start_room "bar" {:error, {:already_started, #PID<0.109.0>}}
      
      





正直なところ、 VM



非常に雄弁です。 2番目のプロセスを作成しようとしていますが、同じ名前のプロセスが既に存在しているため、環境から非常に悪意があります。 他の方法を考え出す必要がありますが、どれですか?..







残念ながら、 name



引数のタイプは明確に定義されています。 {:chat_room, "room name"}



ようなものは使用できません。 ドキュメントを見てみましょう:







サポートされる値:

GenServer



この場合、 GenServer



Process.register/2



を使用して、指定されたatom



名でローカルに登録されます。

{:global, term}



-この場合、 GenServer



:global



モジュールの関数を使用して、指定されたterm



名でグローバルに登録されます。

{:via, module, term}



-この場合、 GenServer



module



で定義されたメカニズムと名前「term」を使用して登録されmodule





英語のオリジナル
サポートされている値は次のとおりです。

atom



GenServer



は、 Process.register/2



を使用して、指定された名前でローカルに登録されます。

{:global, term}



GenServer



は、 :global



モジュールの関数を使用して、指定された用語でグローバルに登録されます。

{:via, module, term}



GenServer



は指定されたメカニズムと名前で登録されます。


最初のオプションはatom



、すでに使用しています。トリッキーなケースでは収まらないことは確かです。

2番目のオプションは、プロセスをノードクラスターにグローバルに登録するために使用されます。 ローカルETS



テーブルを使用します。 さらに、クラスター内のノード内で一定の同期が必要になるため、プログラムの速度が低下します。 したがって、 本当に必要な場合にのみ使用してください。

3番目の最後のオプションは、 :via



をパラメーターとして使用するタプルを使用します。これは、まさに問題を解決するために必要なものです。 これについてはドキュメントに次のように書かれています:







オプション:via



は、 register_name/2



unregister_name/1



whereis_name/1



およびsend/2



インターフェースを持つモジュールをパラメーターとして受け入れます。


英語のオリジナル
:viaオプションでは、register_name / 2、unregister_name / 1、whereis_name / 1、send / 2をエクスポートするモジュールが必要です。


何もはっきりしていませんか? 私も! それでは、このメソッドの動作を見てみましょう。







タプルの使用:via





したがって、タプル:via



は、 Elixir



プロセスを登録するために別のモジュールを使用することを伝える方法です。 このモジュールは次のことを行う必要があります。









これが機能するためには、上記の関数はOTP



定義された特定の形式で応答を送信する必要がありhandle_call/3



およびhandle_cast/2



特定のルールに従うように。







これをすべて知っているモジュールを定義してみましょう:







 # ./lib/chat/registry.ex defmodule Chat.Registry do use GenServer # API def start_link do # We register our registry (yeah, I know) with a simple name, # just so we can reference it in the other functions. GenServer.start_link(__MODULE__, nil, name: :registry) end def whereis_name(room_name) do GenServer.call(:registry, {:whereis_name, room_name}) end def register_name(room_name, pid) do GenServer.call(:registry, {:register_name, room_name, pid}) end def unregister_name(room_name) do GenServer.cast(:registry, {:unregister_name, room_name}) end def send(room_name, message) do # If we try to send a message to a process # that is not registered, we return a tuple in the format # {:badarg, {process_name, error_message}}. # Otherwise, we just forward the message to the pid of this # room. case whereis_name(room_name) do :undefined -> {:badarg, {room_name, message}} pid -> Kernel.send(pid, message) pid end end # SERVER def init(_) do # We will use a simple Map to store our processes in # the format %{"room name" => pid} {:ok, Map.new} end def handle_call({:whereis_name, room_name}, _from, state) do {:reply, Map.get(state, room_name, :undefined), state} end def handle_call({:register_name, room_name, pid}, _from, state) do # Registering a name is just a matter of putting it in our Map. # Our response tuple include a `:no` or `:yes` indicating if # the process was included or if it was already present. case Map.get(state, room_name) do nil -> {:reply, :yes, Map.put(state, room_name, pid)} _ -> {:reply, :no, state} end end def handle_cast({:unregister_name, room_name}, state) do # And unregistering is as simple as deleting an entry # from our Map {:noreply, Map.delete(state, room_name)} end end
      
      





繰り返しますが、レジストリが内部でどのように機能するかを選択してください。 ここでは、単純なMap



を使用して、名前とpid



を関連付けます。 このコードは、 GenServer



どのようにGenServer



するかをよく知っている場合は特に、絶対にシンプルで簡単です。 関数によって返される値のみが不慣れに見える場合があります。







iex



セッションでレジストリを試す時がiex



ました:







 $ iex -S mix iex> {:ok, pid} = Chat.Server.start_link("room1") {:ok, #PID<0.107.0>} iex> Chat.Registry.start_link {:ok, #PID<0.109.0>} iex> Chat.Registry.whereis_name("room1") :undefined iex> Chat.Registry.register_name("room1", pid) :yes iex> Chat.Registry.register_name("room1", pid) :no iex> Chat.Registry.whereis_name("room1") #PID<0.107.0> iex> Chat.Registry.unregister_name("room1") :ok iex> Chat.Registry.whereis_name("room1") :undefined
      
      





5秒-素晴らしいフライト! レジストリは正常に機能します。レジストリを登録および削除します。 チャットで使用してみましょう。







問題は、複数のChat.Server



サーバーがChat.Server



Supervisor



で初期化されたことでした。 特定のルームにメッセージを送信するには、 Chat.Server.add_message(“room1”, “my message”)



を呼び出したいので、サーバー名を{:chat_room, “room1”}



および{:chat_room, “room2”}



として登録する必要があります{:chat_room, “room2”}



タプルを介して行う方法は次のとおりです。







 # ./lib/chat/server.ex defmodule Chat.Server do use GenServer # API def start_link(name) do # Instead of passing an atom to the `name` option, we send # a tuple. Here we extract this tuple to a private method # called `via_tuple` that can be reused in every function GenServer.start_link(__MODULE__, [], name: via_tuple(name)) end def add_message(room_name, message) do # And the `GenServer` callbacks will accept this tuple the # same way it accepts a pid or an atom. GenServer.cast(via_tuple(room_name), {:add_message, message}) end def get_messages(room_name) do GenServer.call(via_tuple(room_name), :get_messages) end defp via_tuple(room_name) do # And the tuple always follow the same format: # {:via, module_name, term} {:via, Chat.Registry, {:chat_room, room_name}} end # SERVER (no changes required here) # ... end
      
      





変更-このコミットで


ここで何が起こるかです: Chat.Server



にメッセージを送信してルームの名前を渡すChat.Server



に、タプルで渡されたモジュールを使用してpid



このpid



見つけます:via



(この場合はChat.Registry



) 。

これで問題が解決しますChat.Server



プロセスをいくつでも使用できるようになり(名前の空想が終わるまで)、そのpid



を知る必要がなくなりました。 絶対に。







ただし、このソリューションには別の問題があります。 推測?

まさに! レジストリは、クラッシュしたプロセスを認識していないため、 Supervisor



使用して再起動する必要があります。 これは、これが発生すると、レジストリは同じ名前のレコードを再作成することを許可せずpid



デッドプロセスのpid



保存することを意味します。







理論的には、この問題の解決策はそれほど複雑ではありません。 レジストリに、 pid



が保存されているすべてのプロセスを監視するよう強制します。 そのような「観察可能な」プロセスが落ちたらすぐに、レジストリから削除します。







 # in lib/chat/registry.ex defmodule Chat.Registry do # ... def handle_call({:register_name, room_name, pid}, _from, state) do case Map.get(state, room_name) do nil -> # When a new process is registered, we start monitoring it. Process.monitor(pid) {:reply, :yes, Map.put(state, room_name, pid)} _ -> {:reply, :no, state} end end def handle_info({:DOWN, _, :process, pid, _}, state) do # When a monitored process dies, we will receive a # `:DOWN` message that we can use to remove the # dead pid from our registry. {:noreply, remove_pid(state, pid)} end def remove_pid(state, pid_to_remove) do # And here we just filter out the dead pid remove = fn {_key, pid} -> pid != pid_to_remove end Enum.filter(state, remove) |> Enum.into(%{}) end end
      
      





変更-このコミットで


すべてが機能することを確認します。







 $ iex -S mix iex> Chat.Registry.start_link {:ok, #PID<0.107.0>} iex> Chat.Supervisor.start_link {:ok, #PID<0.109.0>} iex> Chat.Supervisor.start_room("room1") {:ok, #PID<0.111.0>} iex> Chat.Server.add_message("room1", "message") :ok iex> Chat.Server.get_messages("room1") ["message"] iex> Chat.Registry.whereis_name({:chat_room, "room1"}) |> Process.exit(:kill) true iex> Chat.Server.add_message("room1", "message") :ok iex> Chat.Server.get_messages("room1") ["message"]
      
      





さて、 Supervisor



Chat.Server



プロセスを再起動する回数はまったくChat.Server



ルームにメッセージを送信するとすぐに、正しいpid



配信されます。







gproc



簡素化する



原則として、チャットは終了しますが、タプルを使用して登録を簡素化するもう1つの機能について説明:via



ます。 これはgproc



ライブラリであるgproc



です。

そして、 gproc



代わりにChat.Server



使用するようgproc



に教えてChat.Server



Chat.Server



完全にChat.Registry



ます。







依存関係から始めましょう。 これを行うには、 gproc



mix.exs



に追加しmix.exs









 # ./mix.exs defmodule Chat.Mixfile do # ... def application do [applications: [:logger, :gproc]] end defp deps do [{:gproc, "0.3.1"}] end end
      
      





次に、依存関係を次のようにプルアップします。







 $ mix deps.get
      
      





タプルを使用して登録を変更できます:via



- gproc



ではなくChat.Registry



使用します:







 # ./lib/chat/server.ex defmodule Chat.Server do # ... # The only thing we need to change is the `via_tuple/1` function, # to make it use `gproc` instead of `Chat.Registry` defp via_tuple(room_name) do {:via, :gproc, {:n, :l, {:chat_room, room_name}}} end # ... end
      
      





gproc



{type, scope, key}



の3つの値で構成されるgproc



使用します。







この場合、以下を使用します。









可能なgproc



設定の詳細については、 こちらをご覧ください







このような変更の後、 iex



し、 iex



セッションですべてが引き続き機能することを確認します。







 $ iex -S mix iex> Chat.Supervisor.start_link {:ok, #PID<0.190.0>} iex> Chat.Supervisor.start_room("room1") {:ok, #PID<0.192.0>} iex> Chat.Supervisor.start_room("room2") {:ok, #PID<0.194.0>} iex> Chat.Server.add_message("room1", "first message") :ok iex> Chat.Server.add_message("room2", "second message") :ok iex> Chat.Server.get_messages("room1") ["first message"] iex> Chat.Server.get_messages("room2") ["second message"] iex> :gproc.where({:n, :l, {:chat_room, "room1"}}) |> Process.exit(:kill) true iex> Chat.Server.add_message("room1", "first message") :ok iex> Chat.Server.get_messages("room1") ["first message"]
      
      





変更-このコミットで


次はどこ?



あなたと私はたくさんの複雑な質問を見つけました。 主な調査結果:









もちろん、これだけではありません。 クラスタ内のすべてのノードでグローバル登録が必要な場合は、他のツールも有効です。 Erlang



は、グローバル登録用のグローバルモジュール、プロセスグループ用のgprc



があり、同じgprc



が役立ちます。







この記事に興味がある場合は、 Saša Jurić. Elixir in Action



をお読みくださいSaša Jurić. Elixir in Action



Saša Jurić. Elixir in Action









そして、ここにチーズカブがあります)








All Articles