Current section
Files
Jump to
Current section
Files
lib/nats/nats_pubsub.ex
defmodule Nats.NatsPubsub do
@moduledoc """
Nats handler for PubSub
"""
use GenServer
require Logger
defmodule State do
defstruct id: nil, pubsub_name: nil, gnat: nil, subscription: nil, serializer: nil
@type t :: %__MODULE__{
id: Node.t(),
pubsub_name: String.t(),
gnat: any(),
subscription: String.t(),
serializer: module()
}
end
@main_topic "nodes.pubsub.*"
@doc false
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: opts[:adapter_name])
end
@doc false
def node_name(_adapter_name), do: node()
@doc false
def broadcast(adapter_name, topic, message, dispatcher) do
do_broadcast(adapter_name, topic, message, dispatcher)
end
@doc false
def direct_broadcast(adapter_name, node_name, topic, message, dispatcher) do
metadata = Keyword.put([], :target, node_name)
do_broadcast(adapter_name, topic, message, dispatcher, metadata)
end
defp do_broadcast(adapter_name, topic, message, dispatcher, metadata \\ []) do
metadata = Keyword.put(metadata, :from, node())
metadata = Keyword.put(metadata, :dispatcher, dispatcher)
GenServer.call(adapter_name, {:broadcast, topic, message, metadata})
end
## GenServer Callbacks
@impl true
@doc false
def init(state) do
Process.flag(:trap_exit, true)
nats_conn = Keyword.fetch!(state, :connection)
pubsub_name = Keyword.get(state, :adapter_name, __MODULE__)
serializer = Keyword.get(state, :serializer, Nats.Serializer.Native)
{:ok, %State{pubsub_name: pubsub_name}, {:continue, {:setup, nats_conn, serializer}}}
end
@impl true
def handle_continue({:setup, nats_conn, serializer}, state) do
{gnat, subscription} =
case Gnat.start_link(nats_conn) do
{:ok, gnat} ->
subscription = sub(gnat)
{gnat, subscription}
_ ->
raise RuntimeError, "Could not connect to Nats with options #{inspect(nats_conn)}"
end
{:noreply,
%State{state | id: node(), gnat: gnat, subscription: subscription, serializer: serializer}}
end
defp sub(gnat) do
case Gnat.sub(gnat, self(), @main_topic) do
{:ok, subscription} ->
subscription
_ ->
raise RuntimeError,
"Unable to subscribe Node #{inspect(node())} to channel #{@main_topic}"
end
end
@impl true
def handle_call(
{:broadcast, topic, message, metadata},
_from,
%State{gnat: gnat, serializer: serializer} = state
) do
headers = build_headers(metadata)
res =
case serializer.encode(message) do
{:ok, payload} ->
Gnat.pub(gnat, "nodes.pubsub.#{topic}", payload, headers: headers)
_ ->
:error
end
{:reply, res, state}
end
@impl true
def handle_info(
{:msg, %{topic: topic, reply_to: _to, headers: headers} = event},
%State{pubsub_name: pubsub_name, serializer: serializer} = state
) do
current_node = to_string(node())
case conver_headers_to_map(headers) do
%{target: target}
when not is_nil(target) and target != current_node ->
# Direct broadcast and this is not the destination node.
:ok
%{from: ^current_node} ->
# This node is the source, nothing to do, because local dispatch already
# happened.
:ok
%{dispatcher: dispatcher} ->
real_topic = get_real_topic(topic)
payload = decode(serializer, event)
Phoenix.PubSub.local_broadcast(
pubsub_name,
real_topic,
payload,
maybe_convert_to_existing_atom(dispatcher)
)
end
{:noreply, state}
end
def handle_info(event, state) do
Logger.warning("An unexpected event has occurred. Event: #{inspect(event)}")
{:noreply, state}
end
@impl true
def terminate(reason, _state) do
Logger.warning("#{inspect(__MODULE__)} terminate with reason: #{inspect(reason)}")
end
defp build_headers([]), do: []
defp build_headers(metadata) when is_list(metadata) do
Enum.map(metadata, fn {key, value} ->
key = if is_atom(key), do: Atom.to_string(key), else: key
value = if is_atom(value), do: Atom.to_string(value), else: value
{key, value}
end)
end
defp build_headers(_), do: []
defp decode(serializer, %{body: body} = event) do
case serializer.decode(body) do
{:ok, payload} ->
payload
_ ->
raise ArgumentError, "Could not deserialize event: #{inspect(event)}"
end
end
defp get_real_topic(topic) do
String.replace(topic, "nodes.pubsub.", "")
end
defp conver_headers_to_map(headers), do: Enum.into(headers, %{})
defp maybe_convert_to_existing_atom(string) when is_binary(string),
do: String.to_existing_atom(string)
defp maybe_convert_to_existing_atom(atom) when is_atom(atom), do: atom
end