Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
defmodule Highlander do
@moduledoc """
Highlander allows you to run a single globally unique process in a cluster.
Highlander uses erlang's `:global` module to ensure uniqueness, and uses `child_spec.id` as the uniqueness key.
Highlander will start its child process just once in a cluster. The first Highlander process will start its child, all other Highlander processes will monitor the first process and attempt to take over when it goes down.
_Note: You can also use Highlander to start a globally unique supervision tree._
## Usage
Simply wrap a child process with `{Highlander, child}`.
Before:
```
children = [
child_spec
]
Supervisor.init(children, strategy: :one_for_one)
```
After:
```
children = [
{Highlander, child_spec}
]
Supervisor.init(children, strategy: :one_for_one)
```
See the [documentation on Supervisor.child_spec/1](https://hexdocs.pm/elixir/Supervisor.html#module-child_spec-1) for more information.
## `child_spec.id` is used to determine global uniqueness
Ensure that `child_spec.id` has the correct value! Check the debug logs if you are unsure what is being used.
## Globally unique supervisors
You can also have Highlander run a supervisor:
```
children = [
{Highlander, {MySupervisor, arg}},
]
```
## Handling netsplits
If there is a netsplit in your cluster, then Highlander will think that the other process has died, and start a new one. When the split heals, `:global` will recognize that there is a naming conflict, and will take action to rectify that. To deal with this, Highlander simply terminates one of the two child processes with reason `:shutdown`.
To catch this, simply trap exits in your process and add a `terminate/2` callback.
Note: The `terminate/2` callback will also run when your application is terminating.
```
def init(arg) do
Process.flag(:trap_exit, true)
{:ok, initial_state(arg)}
end
def terminate(_reason, _state) do
# this will run when the process receives an exit signal from its parent
end
```
"""
use GenServer
require Logger
def child_spec(child_child_spec) do
child_child_spec = Supervisor.child_spec(child_child_spec, [])
Logger.debug("Starting Highlander with #{inspect(child_child_spec.id)} as uniqueness key")
%{
id: child_child_spec.id,
start: {GenServer, :start_link, [__MODULE__, child_child_spec, []]}
}
end
@impl true
def init(child_spec) do
Process.flag(:trap_exit, true)
{:ok, register(%{child_spec: child_spec})}
end
@impl true
def handle_info({:DOWN, ref, :process, _, _}, %{ref: ref} = state) do
{:noreply, register(state)}
end
def handle_info({:EXIT, _pid, :name_conflict}, %{pid: pid} = state) do
:ok = Supervisor.stop(pid, :shutdown)
{:stop, {:shutdown, :name_conflict}, Map.delete(state, :pid)}
end
@impl true
def terminate(reason, %{pid: pid}) do
:ok = Supervisor.stop(pid, reason)
end
def terminate(_, _), do: nil
defp name(%{child_spec: %{id: global_name}}) do
{__MODULE__, global_name}
end
defp handle_conflict(_name, pid1, pid2) do
Process.exit(pid2, :name_conflict)
pid1
end
defp register(state) do
case :global.register_name(name(state), self(), &handle_conflict/3) do
:yes -> start(state)
:no -> monitor(state)
end
end
defp start(state) do
{:ok, pid} = Supervisor.start_link([state.child_spec], strategy: :one_for_one)
Map.put(state, :pid, pid)
end
defp monitor(state) do
case :global.whereis_name(name(state)) do
:undefined ->
register(state)
pid ->
ref = Process.monitor(pid)
%{child_spec: state.child_spec, ref: ref}
end
end
end