Packages
cloister
0.2.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.0
0.14.0
0.13.0
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.3
0.10.2
0.10.1
0.10.0
0.9.0
0.8.0
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.5
0.6.4
0.6.3
retired
0.6.2
retired
0.6.1
retired
0.6.0
retired
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
The helper application to manage cluster, that uses hash ring to route requests to nodes. Automatically keeps track of connected nodes, provides helpers to determine where the term is to be executed, to multicast to all the nodes in the cluster and to retrieve current state of the cluster.
Current section
Files
Jump to
Current section
Files
lib/cloister/monitor.ex
defmodule Cloister.Monitor do
@moduledoc false
use GenServer
require Logger
@type status :: :down | :starting | :up | :stopping | :rehashing | :panic
@type t :: %{
__struct__: Cloister.Monitor,
otp_app: atom(),
listener: module(),
started_at: DateTime.t(),
status: status(),
alive?: boolean(),
clustered?: boolean(),
sentry?: boolean(),
ring: atom()
}
defstruct otp_app: :cloister,
listener: nil,
started_at: nil,
status: :down,
alive?: false,
clustered?: false,
sentry?: false,
ring: nil
alias Cloister.Monitor, as: Mon
# millis
@refresh_rate 1_000
@spec start_link(opts :: keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
{state, opts} = Keyword.pop(opts, :state, [])
GenServer.start_link(
__MODULE__,
state,
Keyword.put_new(opts, :name, __MODULE__)
)
end
@impl GenServer
@doc false
def init(state) do
[{top_app, _, _} | _] = Application.loaded_applications()
otp_app = Keyword.get(state, :otp_app, top_app)
unless Keyword.has_key?(state, :ring),
do: HashRing.Managed.new(otp_app)
state =
state
|> Keyword.put_new(:otp_app, otp_app)
|> Keyword.put_new(:started_at, DateTime.utc_now())
|> Keyword.put_new(:status, :starting)
|> Keyword.put_new(:ring, otp_app)
:ok = :net_kernel.monitor_nodes(true, node_type: :all)
{:ok, struct(__MODULE__, state), {:continue, :quorum}}
end
@impl GenServer
def terminate(reason, %Mon{} = state) do
Logger.warn("[đ¸ī¸ #{node()}] âšī¸ reason: [" <> inspect(reason) <> "], state: [" <> inspect(state) <> "]")
state = notify(:stopping, state)
notify(:down, state)
end
@impl GenServer
@doc false
def handle_continue(:quorum, %Mon{} = state),
do: do_handle_quorum(Node.alive?(), state)
@spec do_handle_quorum(boolean(), state :: t()) ::
{:noreply, new_state} | {:noreply, new_state, {:continue, :quorum}}
when new_state: t()
@doc false
defp do_handle_quorum(true, %Mon{otp_app: otp_app} = state) do
active_sentry =
for sentry <- Application.get_env(otp_app, :sentry, [node()]),
Node.connect(sentry),
do: sentry
if active_sentry != [] do
case Code.ensure_compiled(Cloister.Monitor.Info) do
{:module, _} ->
:ok
_ ->
ast =
quote do
@spec whois(term :: term()) :: node()
def whois(term) do
case HashRing.Managed.key_to_node(unquote(state.ring), term) do
{:error, {:invalid_ring, :no_nodes}} ->
Cloister.Monitor.nodes!()
whois(term)
node ->
node
end
end
@spec nodes :: [term()]
def nodes do
HashRing.Managed.nodes(unquote(state.ring))
end
end
Module.create(Cloister.Monitor.Info, ast, Macro.Env.location(__ENV__))
end
state = %Mon{
state
| alive?: true,
sentry?: Enum.member?(active_sentry, node()),
clustered?: true
}
{:noreply, notify(:up, state)}
else
{:noreply, state, {:continue, :quorum}}
end
end
@doc false
defp do_handle_quorum(false, %Mon{} = state),
do: {:noreply, notify(:up, %Mon{state | sentry?: true, clustered?: false})}
##############################################################################
@spec state :: t()
@doc "Returns an internal state of the Node"
def state, do: GenServer.call(__MODULE__, :state)
@spec siblings :: [node()]
@doc "Returns whether the requested amount of nodes in the cluster are connected"
def siblings, do: GenServer.call(__MODULE__, :siblings)
@spec nodes! :: t()
@doc "Rehashes the ring and returns the current state"
def nodes!, do: GenServer.call(__MODULE__, :nodes!)
##############################################################################
@impl GenServer
def handle_info(:update_node_list, state) do
# Logger.debug("[đ¸ī¸ #{node()}] đ state: [" <> inspect(state) <> "]")
{:noreply, update_state(state)}
end
@impl GenServer
def handle_info({:nodeup, node, info}, state) do
Logger.info(
"[đ¸ī¸ #{node()}] #{node} âŦī¸: [" <> inspect(info) <> "], state: [" <> inspect(state) <> "]"
)
{:noreply, update_state(state)}
end
@impl GenServer
def handle_info({:nodedown, node, info}, state) do
Logger.info(
"[đ¸ī¸ #{node()}] #{node} âŦī¸ info: [" <>
inspect(info) <> "], state: [" <> inspect(state) <> "]"
)
{:noreply, update_state(state)}
end
##############################################################################
@impl GenServer
@doc false
def handle_call(:state, _from, state), do: {:reply, state, state}
@impl GenServer
@doc false
def handle_call(:siblings, _from, state),
do: {:reply, [node() | Node.list()], state}
@impl GenServer
@doc false
def handle_call(:nodes!, _from, state) do
state = update_state(state)
{:reply, state, state}
end
@spec update_state(state :: t()) :: t()
defp update_state(%Mon{} = state) do
# consensus = Application.get_env(state.otp_app, :consensus, 1)
ring = HashRing.Managed.nodes(state.ring)
nodes = [node() | Node.list()]
status =
case {ring -- nodes, nodes -- ring} do
{[], []} ->
:up
{[], to_ring} ->
Enum.each(to_ring, &HashRing.Managed.add_node(state.ring, &1))
:rehashing
{from_ring, []} ->
Enum.each(from_ring, &HashRing.Managed.remove_node(state.ring, &1))
:rehashing
{from_ring, to_ring} ->
Enum.each(from_ring, &HashRing.Managed.remove_node(state.ring, &1))
Enum.each(to_ring, &HashRing.Managed.add_node(state.ring, &1))
:panic
end
notify(status, state)
end
@spec notify(to :: status(), state :: t()) :: t()
defp notify(to, %{status: to} = state), do: reschedule(state)
defp notify(to, %{status: from} = state) do
state = %Mon{state | status: to}
apply(state.listener, :on_state_change, [from, state])
reschedule(state)
end
@spec reschedule(state :: t()) :: t()
defp reschedule(state) do
Process.send_after(self(), :update_node_list, @refresh_rate)
state
end
end