Current section

Files

Jump to
libcluster lib events.ex
Raw

lib/events.ex

defmodule Cluster.Events do
@moduledoc """
This module implements a publish/subscribe mechanism for cluster events.
"""
import Cluster.Logger
# Public API
@doc """
Subscribes a process (by pid) to cluster events.
"""
@spec subscribe(pid) :: :ok
def subscribe(pid) do
send(__MODULE__, {:subscribe, pid})
:ok
end
@doc """
Unsubscribes a process (by pid) from cluster events.
"""
@spec unsubscribe(pid) :: :ok
def unsubscribe(pid) do
send(__MODULE__, {:unsubscribe, pid})
:ok
end
@doc """
Publishes an event to all subscribers.
Published messages are delivered via `send/2`, so if subscribing
a gen_* process, you will receive them in the `handle_info/2` callback.
"""
@spec publish(event :: term()) :: :ok
def publish(event) do
send(__MODULE__, {:publish, event})
:ok
end
# Helpers
defmacrop handle_debug(debug, msg) do
quote do
:sys.handle_debug(unquote(debug), &write_debug/3, nil, unquote(msg))
end
end
## Process implementation
def start_link() do
:proc_lib.start_link(__MODULE__, :init, [self()], :infinity, [:link])
end
def init(parent) do
# register as Cluster.Events
Process.register(self(), __MODULE__)
# Trap exits
Process.flag(:trap_exit, true)
# Tell the supervisor we've started
:proc_lib.init_ack(parent, {:ok, self()})
debug = :sys.debug_options([])
# Monitor node events
:ok = :net_kernel.monitor_nodes(true, [node_type: :all])
loop(%{}, parent, debug)
end
defp loop(subscribers, parent, debug) do
receive do
# System messages take precedence, as does the parent process exiting
{:system, from, request} ->
:sys.handle_system_msg(request, from, parent, __MODULE__, debug, nil)
# Parent supervisor is telling us to exit
{:EXIT, ^parent, reason} ->
warn "[events] exiting: #{inspect reason}"
exit(reason)
{:nodeup, node, _info} = msg ->
debug = handle_debug(debug, {:in, msg})
for {pid, _} <- subscribers, do: send(pid, {:nodeup, node})
loop(subscribers, parent, debug)
{:nodedown, node, _info} = msg ->
debug = handle_debug(debug, {:in, msg})
for {pid, _} <- subscribers, do: send(pid, {:nodedown, node})
loop(subscribers, parent, debug)
{:subscribe, pid} = msg ->
debug = handle_debug(debug, {:in, msg})
case Map.get(subscribers, pid) do
nil ->
ref = Process.monitor(pid)
loop(Map.put(subscribers, pid, ref), parent, debug)
_ ->
loop(subscribers, parent, debug)
end
{:unsubscribe, pid} = msg ->
debug = handle_debug(debug, {:in, msg})
case Map.pop(subscribers, pid) do
{nil, _} ->
loop(subscribers, parent, debug)
{ref, subscribers} ->
Process.demonitor(ref, [:flush])
loop(subscribers, parent, debug)
end
{:publish, event} = msg ->
debug = handle_debug(debug, {:in, msg})
for {pid, _} <- subscribers, do: send(pid, event)
loop(subscribers, parent, debug)
{:DOWN, ref, _type, pid, _info} = msg ->
debug = handle_debug(debug, {:in, msg})
case Map.pop(subscribers, pid) do
{^ref, subscribers} ->
loop(subscribers, parent, debug)
{nil, _} ->
loop(subscribers, parent, debug)
end
end
end
# Sys module callbacks
# Handle resuming this process after it's suspended by :sys
# We're making a bit of an assumption here that it won't be suspended
# prior to entering the receive loop. This is something we (probably) could
# fix by storing the current phase of the startup the process is in, but I'm not sure.
def system_continue(parent, debug, state),
do: loop(state, parent, debug)
# Handle system shutdown gracefully
def system_terminate(_reason, :application_controller, _debug, _state) do
# OTP-5811 Don't send an error report if it's the system process
# application_controller which is terminating - let init take care
# of it instead
:ok
end
def system_terminate(:normal, _parent, _debug, _state) do
exit(:normal)
end
def system_terminate(reason, _parent, debug, state) do
:error_logger.format('** ~p terminating~n
** Server state was: ~p~n
** Reason: ~n** ~p~n', [__MODULE__, state, reason])
:sys.print_log(debug)
exit(reason)
end
# Used for fetching the current process state
def system_get_state(state), do: {:ok, state}
# Called when someone asks to replace the current process state
# Required, but you really really shouldn't do this.
def system_replace_state(state_fun, state) do
new_state = state_fun.(state)
{:ok, new_state, new_state}
end
# Called when the system is upgrading this process
def system_code_change(misc, _module, _old, _extra) do
{:ok, misc}
end
defp write_debug(_dev, {:in, msg, from}, _ctx) do
Cluster.Logger.debug("[events] <== #{inspect msg} from #{inspect from}")
end
defp write_debug(_dev, {:in, msg}, _ctx) do
Cluster.Logger.debug("[events] <== #{inspect msg}")
end
defp write_debug(_dev, {:out, msg, to}, _ctx) do
Cluster.Logger.debug("[events] ==> #{inspect msg} to #{inspect to}")
end
defp write_debug(_dev, {:out, msg}, _ctx) do
Cluster.Logger.debug("[events] ==> #{inspect msg}")
end
defp write_debug(_dev, event, _ctx) do
Cluster.Logger.debug("[events] #{inspect event}")
end
end