defmodule Tortoise.Connection.Controller do @moduledoc false require Logger alias Tortoise.{Package, Connection, Handler} alias Tortoise.Connection.Inflight alias Tortoise.Package.{ Connect, Connack, Disconnect, Publish, Puback, Pubrec, Pubrel, Pubcomp, Subscribe, Suback, Unsubscribe, Unsuback, Pingreq, Pingresp } use GenServer @enforce_keys [:client_id, :handler] defstruct client_id: nil, ping: :queue.new(), status: :down, awaiting: %{}, handler: %Handler{module: Handler.Default, initial_args: []} alias __MODULE__, as: State # Client API def start_link(opts) do client_id = Keyword.fetch!(opts, :client_id) handler = Handler.new(Keyword.fetch!(opts, :handler)) init_state = %State{ client_id: client_id, handler: handler } GenServer.start_link(__MODULE__, init_state, name: via_name(client_id)) end defp via_name(client_id) do Tortoise.Registry.via_name(__MODULE__, client_id) end def stop(client_id) do GenServer.stop(via_name(client_id)) end def info(client_id) do GenServer.call(via_name(client_id), :info) end @spec ping(Tortoise.client_id()) :: {:ok, reference()} def ping(client_id) do ref = make_ref() :ok = GenServer.cast(via_name(client_id), {:ping, {self(), ref}}) {:ok, ref} end @spec ping_sync(Tortoise.client_id(), timeout()) :: {:ok, reference()} | {:error, :timeout} def ping_sync(client_id, timeout \\ :infinity) do {:ok, ref} = ping(client_id) receive do {Tortoise, {:ping_response, ^ref, round_trip_time}} -> {:ok, round_trip_time} after timeout -> {:error, :timeout} end end @doc false def handle_incoming(client_id, package) do GenServer.cast(via_name(client_id), {:incoming, package}) end @doc false def handle_result(client_id, {{pid, ref}, Package.Publish, result}) do send(pid, {{Tortoise, client_id}, ref, result}) :ok end def handle_result(client_id, {{pid, ref}, type, result}) do send(pid, {{Tortoise, client_id}, ref, result}) GenServer.cast(via_name(client_id), {:result, {type, result}}) end @doc false def handle_onward(client_id, %Package.Publish{} = publish) do GenServer.cast(via_name(client_id), {:onward, publish}) end # Server callbacks @impl true def init(%State{handler: handler} = opts) do {:ok, _} = Tortoise.Events.register(opts.client_id, :status) case Handler.execute(handler, :init) do {:ok, %Handler{} = updated_handler} -> {:ok, %State{opts | handler: updated_handler}} end end @impl true def terminate(reason, %State{handler: handler}) do _ignored = Handler.execute(handler, {:terminate, reason}) :ok end @impl true def handle_call(:info, _from, state) do {:reply, state, state} end @impl true def handle_cast({:incoming, <>}, state) do package |> Package.decode() |> handle_package(state) end # allow for passing in already decoded packages into the controller, # this allow us to test the controller without having to pass in # binaries def handle_cast({:incoming, %{:__META__ => _} = package}, state) do handle_package(package, state) end def handle_cast({:ping, caller}, state) do with {:ok, {transport, socket}} <- Connection.connection(state.client_id) do time = System.monotonic_time(:microsecond) apply(transport, :send, [socket, Package.encode(%Package.Pingreq{})]) ping = :queue.in({caller, time}, state.ping) {:noreply, %State{state | ping: ping}} else {:error, :unknown_connection} -> {:stop, :unknown_connection, state} end end def handle_cast( {:result, {Package.Subscribe, subacks}}, %State{handler: handler} = state ) do case Handler.execute(handler, {:subscribe, subacks}) do {:ok, updated_handler} -> {:noreply, %State{state | handler: updated_handler}} end end def handle_cast( {:result, {Package.Unsubscribe, unsubacks}}, %State{handler: handler} = state ) do case Handler.execute(handler, {:unsubscribe, unsubacks}) do {:ok, updated_handler} -> {:noreply, %State{state | handler: updated_handler}} end end # an incoming publish with QoS=2 will get parked in the inflight # manager process, which will onward it to the controller, making # sure we will only dispatch it once to the publish-handler. def handle_cast( {:onward, %Package.Publish{qos: 2, dup: false} = publish}, %State{handler: handler} = state ) do case Handler.execute(handler, {:publish, publish}) do {:ok, updated_handler} -> {:noreply, %State{state | handler: updated_handler}} end end @impl true def handle_info({:next_action, {:subscribe, topic, opts} = action}, state) do {qos, opts} = Keyword.pop_first(opts, :qos, 0) case Tortoise.Connection.subscribe(state.client_id, [{topic, qos}], opts) do {:ok, ref} -> updated_awaiting = Map.put_new(state.awaiting, ref, action) {:noreply, %State{state | awaiting: updated_awaiting}} end end def handle_info({:next_action, {:unsubscribe, topic} = action}, state) do case Tortoise.Connection.unsubscribe(state.client_id, topic) do {:ok, ref} -> updated_awaiting = Map.put_new(state.awaiting, ref, action) {:noreply, %State{state | awaiting: updated_awaiting}} end end # connection changes def handle_info( {{Tortoise, client_id}, :status, same}, %State{client_id: client_id, status: same} = state ) do {:noreply, state} end def handle_info( {{Tortoise, client_id}, :status, new_status}, %State{client_id: client_id, handler: handler} = state ) do case Handler.execute(handler, {:connection, new_status}) do {:ok, updated_handler} -> {:noreply, %State{state | handler: updated_handler, status: new_status}} end end def handle_info({{Tortoise, client_id}, ref, result}, %{client_id: client_id} = state) do case {result, Map.pop(state.awaiting, ref)} do {_, {nil, _}} -> Logger.warn("Unexpected async result") {:noreply, state} {:ok, {_action, updated_awaiting}} -> {:noreply, %State{state | awaiting: updated_awaiting}} end end # QoS LEVEL 0 ======================================================== # commands ----------------------------------------------------------- defp handle_package( %Publish{qos: 0, dup: false} = publish, %State{handler: handler} = state ) do case Handler.execute(handler, {:publish, publish}) do {:ok, updated_handler} -> {:noreply, %State{state | handler: updated_handler}} # handle stop end end # QoS LEVEL 1 ======================================================== # commands ----------------------------------------------------------- defp handle_package( %Publish{qos: 1} = publish, %State{handler: handler} = state ) do :ok = Inflight.track(state.client_id, {:incoming, publish}) case Handler.execute(handler, {:publish, publish}) do {:ok, updated_handler} -> {:noreply, %State{state | handler: updated_handler}} end end # response ----------------------------------------------------------- defp handle_package(%Puback{} = puback, state) do :ok = Inflight.update(state.client_id, {:received, puback}) {:noreply, state} end # QoS LEVEL 2 ======================================================== # commands ----------------------------------------------------------- defp handle_package(%Publish{qos: 2} = publish, %State{} = state) do :ok = Inflight.track(state.client_id, {:incoming, publish}) {:noreply, state} end defp handle_package(%Pubrel{} = pubrel, state) do :ok = Inflight.update(state.client_id, {:received, pubrel}) {:noreply, state} end # response ----------------------------------------------------------- defp handle_package(%Pubrec{} = pubrec, state) do :ok = Inflight.update(state.client_id, {:received, pubrec}) {:noreply, state} end defp handle_package(%Pubcomp{} = pubcomp, state) do :ok = Inflight.update(state.client_id, {:received, pubcomp}) {:noreply, state} end # SUBSCRIBING ======================================================== # command ------------------------------------------------------------ defp handle_package(%Subscribe{} = subscribe, state) do # not a server! (yet) {:stop, {:protocol_violation, {:unexpected_package_from_remote, subscribe}}, state} end # response ----------------------------------------------------------- defp handle_package(%Suback{} = suback, state) do :ok = Inflight.update(state.client_id, {:received, suback}) {:noreply, state} end # UNSUBSCRIBING ====================================================== # command ------------------------------------------------------------ defp handle_package(%Unsubscribe{} = unsubscribe, state) do # not a server {:stop, {:protocol_violation, {:unexpected_package_from_remote, unsubscribe}}, state} end # response ----------------------------------------------------------- defp handle_package(%Unsuback{} = unsuback, state) do :ok = Inflight.update(state.client_id, {:received, unsuback}) {:noreply, state} end # PING MESSAGES ====================================================== # command ------------------------------------------------------------ defp handle_package(%Pingresp{}, %State{ping: ping} = state) when is_nil(ping) or ping == {[], []} do {:noreply, state} end defp handle_package(%Pingresp{}, %State{ping: ping} = state) do {{:value, {{caller, ref}, start_time}}, ping} = :queue.out(ping) round_trip_time = System.monotonic_time(:microsecond) - start_time send(caller, {Tortoise, {:ping_response, ref, round_trip_time}}) {:noreply, %State{state | ping: ping}} end # response ----------------------------------------------------------- defp handle_package(%Pingreq{} = pingreq, state) do # not a server! {:stop, {:protocol_violation, {:unexpected_package_from_remote, pingreq}}, state} end # CONNECTING ========================================================= # command ------------------------------------------------------------ defp handle_package(%Connect{} = connect, state) do # not a server! {:stop, {:protocol_violation, {:unexpected_package_from_remote, connect}}, state} end # response ----------------------------------------------------------- defp handle_package(%Connack{} = connack, state) do # receiving a connack at this point would be a protocol violation {:stop, {:protocol_violation, {:unexpected_package_from_remote, connack}}, state} end # DISCONNECTING ====================================================== # command ------------------------------------------------------------ defp handle_package(%Disconnect{} = disconnect, state) do # This should be allowed when we implement MQTT 5. Remember there # is a test that assert this as a protocol violation! {:stop, {:protocol_violation, {:unexpected_package_from_remote, disconnect}}, state} end end