Home

Awesome

Horde.Process

An opinionated but configurable means of quickly creating GenServer modules that are intended to be managed and distributed via Horde.

I found myself re-writing the same boilerplate code to create Horde-compatible processes; calling GenServer.start_link the same way every time, handling common return values, generating a "via tuple" for the process name, etc. Rather than having to copy/paste each change, I've put that encapsulation of common functionality into a single (albiet very small) library. Maybe it can make it's way into the Horde library at some point just so people have an easy time jumping into (what I think is) the common use case.

It may not fit your particular needs, despite me trying to make it configurable and also able to handle the straightforward usage out-of-the-box. If that's the case, you can open an issue or submit a PR.

Installation

Add it to your mix.exs dependencies.

def deps do
  [
    {:horde_process, "~> 0.1.0"},
  ]
end

You can read the full documentation at HexDocs.

Usage

Every Horde Process is assumed to be a GenServer. At some point I might remove this particular detail so that it can be mix and matched in other ways, but for now they are assumed to be GenServer processes.

To make a custom Horde Process simply use Horde.Process and pass in the required supervisor and registry modules.

defmodule MyApp.User.Process do
  use Horde.Process, supervisor: MyApp.User.HordeSupervisor, registry: MyApp.User.HordeRegistry

  @impl Horde.Process
  def process_id(%{"user_id" => user_id}), do: user_id
  def process_id(%{user_id: user_id}), do: user_id
  def process_id(user_id) when is_binary(user_id), do: user_id

  @impl Horde.Process
  def child_spec(user_id) do
    %{
      id: user_id,
      start: {__MODULE__, :start_link, [user_id]},
      restart: :transient,
      shutdown: 10_000
    }
  end

  @impl GenServer
  # Set up the process state quickly and have `handle_continue/2` do the rest.
  def init(user_id) do
    Process.flag(:trap_exit, true)
    {:ok, user_id, {:continue, :init}}
  end

  @impl GenServer
  def handle_continue(:init, user_id) do
    {:ok, user} = MyApp.User.fetch(user_id)
    {:noreply, user}
  end
end

Read the documentation for Horde.Process to learn more about why using {:continue, term} is actually an important detail of having efficient Horde Processes.

In the above example we implement the two required Horde Process callbacks, process_id/1 and child_spec/1, and then also implement our GenServer callbacks, init/1 and handle_continue/2.

The custom module now has some additional functions which get generated by use Horde.Process; fetch/1, call/2, etc. If you wanted to get a user process, or start it if one was not already running, you could do the following:

{:ok, pid} = MyApp.User.Process.get(user_id)

Now you can call the process as you normally would with any other PID. But let's assume you wanted to use cast or call functions against the process. Rather than fetching the PID and then passing that through to GenServer.cast or something, Horde.Process imports some additional helper functions.

{:ok, reply} = MyApp.User.Process.call!(user_id, :do_something)

Or maybe you don't want to force a user process to start up if one isn't already running:

{:ok, reply} = MyApp.User.Process.call(user_id, :do_something)

Because the reply from the process is wrapped by call!/2, if GenServer.call would normally reply with something like {:ok, reply} then the return value of call!/2 would be {:ok, {:ok, reply}}. The first term of the tuple is just specifying whether a process was registered and could receive the message. This means that you could receive error replies that should be matched on {:ok, {:error, err}}. If the first term in the tuple is not :ok then it means something went wrong with Horde, not with the request itself.