Current section
Files
Jump to
Current section
Files
lib/nldoc/stream/consumer_server.ex
defmodule NLdoc.Stream.ConsumerServer do
@moduledoc """
A GenServer responsible for managing message consumption and forwarding events
to the calling Plug process.
"""
use GenServer
require Logger
alias NLdoc.Stream.{Message, Message.Header}
@type state() :: %{
caller: pid(),
id: String.t(),
channel: AMQP.Channel.t(),
queue: String.t(),
consumer_tag: String.t()
}
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: opts[:name])
end
@impl true
@spec init(%{
caller: pid(),
id: String.t(),
offset: :first | number(),
queue: String.t(),
channel: AMQP.Channel.t()
}) :: {:ok, state()} | {:stop, term()}
def init(state = %{}) do
case state.channel
|> consume(state.queue, self(), offset: state.offset, filter_value: state.id) do
{:ok, consumer_tag} ->
{:ok, %{state | consumer_tag: consumer_tag}}
{:error, reason} ->
{:stop, reason}
end
end
@impl true
def handle_info({:basic_consume_ok, _info}, state) do
{:noreply, state}
end
def handle_info({:basic_cancel, %{consumer_tag: tag}}, state = %{caller: caller}) do
caller |> send({:unsubscribe, tag})
{:stop, :normal, state}
end
def handle_info(
{:basic_deliver, payload, msg},
state = %{caller: caller, id: id, channel: channel}
) do
if msg |> Message.matches_stream_filter?(id) do
caller |> send({:message, payload})
end
channel |> AMQP.Basic.ack(msg.delivery_tag)
{:noreply, state}
end
def handle_info(:unsubscribe, state = %{channel: channel, consumer_tag: tag}) do
{:ok, _} = channel |> AMQP.Basic.cancel(tag)
{:noreply, state}
end
@impl true
def terminate(_reason, %{caller: caller, channel: channel, consumer_tag: tag}) do
caller |> send({:unsubscribe, tag})
channel |> AMQP.Queue.unsubscribe(tag)
:ok
end
@type offset_opt() :: {:offset, integer() | :first | nil}
@type filter_value_opt() :: {:filter_value, String.t() | nil}
@type opt :: offset_opt() | filter_value_opt()
@spec consume(
channel :: AMQP.Channel.t(),
queue :: String.t(),
consumer_pid :: pid(),
opts :: [opt()]
) :: {:ok, String.t()} | AMQP.Basic.error()
defp consume(channel, queue, consumer_pid, opts),
do: channel |> AMQP.Basic.consume(queue, consumer_pid, arguments: opts |> amqp_arguments())
@spec amqp_arguments(arguments :: [opt()]) :: [Header.t()]
defp amqp_arguments([{:filter_value, value} | xs]) when is_binary(value),
do: [{"x-stream-filter-value", :longstr, value} | xs |> arguments()]
defp arguments([{:offset, offset} | xs]) when is_integer(offset),
do: [{"x-stream-offset", :long, offset} | xs |> arguments()]
defp arguments([{:offset, :first} | xs]),
do: [{"x-stream-offset", :longstr, "first"} | xs |> arguments()]
defp arguments([_ | xs]),
do: xs |> arguments()
defp arguments([]),
do: []
end