Current section

Files

Jump to
tortoise lib tortoise connection inflight.ex
Raw

lib/tortoise/connection/inflight.ex

defmodule Tortoise.Connection.Inflight do
@moduledoc false
alias Tortoise.{Package, Connection}
alias Tortoise.Connection.Controller
alias Tortoise.Connection.Inflight.Track
use GenStateMachine
@enforce_keys [:client_id]
defstruct client_id: nil, pending: %{}, order: []
alias __MODULE__, as: State
# Client API
def start_link(opts) do
client_id = Keyword.fetch!(opts, :client_id)
GenStateMachine.start_link(__MODULE__, opts, 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
GenStateMachine.stop(via_name(client_id))
end
@doc false
def drain(client_id) do
GenStateMachine.call(via_name(client_id), :drain)
end
@doc false
def track(client_id, {:incoming, %Package.Publish{qos: qos} = publish})
when qos in 1..2 do
:ok = GenStateMachine.cast(via_name(client_id), {:incoming, publish})
end
def track(client_id, {:outgoing, package}) do
caller = {_, ref} = {self(), make_ref()}
case package do
%Package.Publish{qos: qos} when qos in 1..2 ->
:ok = GenStateMachine.cast(via_name(client_id), {:outgoing, caller, package})
{:ok, ref}
%Package.Subscribe{} ->
:ok = GenStateMachine.cast(via_name(client_id), {:outgoing, caller, package})
{:ok, ref}
%Package.Unsubscribe{} ->
:ok = GenStateMachine.cast(via_name(client_id), {:outgoing, caller, package})
{:ok, ref}
end
end
@doc false
def track_sync(client_id, {:outgoing, _} = command, timeout \\ :infinity) do
{:ok, ref} = track(client_id, command)
receive do
{{Tortoise, ^client_id}, ^ref, result} ->
result
after
timeout -> {:error, :timeout}
end
end
@doc false
def update(client_id, {_, %{__struct__: _, identifier: _identifier}} = event) do
:ok = GenStateMachine.cast(via_name(client_id), {:update, event})
end
@doc false
def reset(client_id) do
:ok = GenStateMachine.cast(via_name(client_id), :reset)
end
# Server callbacks
@impl true
def init(opts) do
client_id = Keyword.fetch!(opts, :client_id)
initial_data = %State{client_id: client_id}
next_actions = [
{:next_event, :internal, :post_init}
]
{:ok, :disconnected, initial_data, next_actions}
end
@impl true
def handle_event(:internal, :post_init, :disconnected, data) do
case Connection.connection(data.client_id, active: true) do
{:ok, {_transport, _socket} = connection} ->
{:ok, _} = Tortoise.Events.register(data.client_id, :status)
{:next_state, {:connected, connection}, data}
{:error, :timeout} ->
{:stop, :connection_timeout}
{:error, :unknown_connection} ->
{:stop, :unknown_connection}
end
end
# When we receive a new connection we will use that for our future
# transmissions.
def handle_event(
:info,
{{Tortoise, client_id}, :connection, connection},
_current_state,
%State{client_id: client_id, pending: pending} = data
) do
next_actions =
for identifier <- Enum.reverse(data.order) do
case Map.get(pending, identifier, :unknown) do
%Track{pending: [[{:dispatch, %Package.Publish{} = publish} | action] | pending]} =
track ->
publish = %Package.Publish{publish | dup: true}
track = %Track{track | pending: [[{:dispatch, publish} | action] | pending]}
{:next_event, :internal, {:execute, track}}
%Track{} = track ->
{:next_event, :internal, {:execute, track}}
end
end
{:next_state, {:connected, connection}, data, next_actions}
end
# Connection status events; when we go offline we should transition
# into the disconnected state. Everything else will get ignored.
def handle_event(
:info,
{{Tortoise, client_id}, :status, :down},
_current_state,
%State{client_id: client_id} = data
) do
{:next_state, :disconnected, data}
end
def handle_event(:info, {{Tortoise, _}, :status, _}, _, %State{}) do
:keep_state_and_data
end
# Create. Notice: we will only receive publish packages from the
# remote; everything else is something we initiate
def handle_event(
:cast,
{:incoming, %Package.Publish{dup: false} = package},
_state,
%State{pending: pending} = data
) do
track = Track.create(:positive, package)
data = %State{
data
| pending: Map.put_new(pending, track.identifier, track),
order: [track.identifier | data.order]
}
next_actions = [
{:next_event, :internal, {:onward_publish, package}},
{:next_event, :internal, {:execute, track}}
]
{:keep_state, data, next_actions}
end
# possible duplicate
def handle_event(:cast, {:incoming, _}, :draining, %State{}) do
:keep_state_and_data
end
def handle_event(
:cast,
{:incoming, %Package.Publish{identifier: identifier, dup: true} = publish},
_state,
%State{pending: pending} = data
) do
case Map.get(pending, identifier) do
nil ->
next_actions = [
{:next_event, :cast, {:incoming, %Package.Publish{publish | dup: false}}}
]
{:keep_state_and_data, next_actions}
%Track{polarity: :positive, status: [{:received, %{__struct__: Package.Publish}}]} ->
:keep_state_and_data
_otherwise ->
{:stop, :state_out_of_sync, data}
end
end
def handle_event(:cast, {:outgoing, {pid, ref}, _}, :draining, data) do
send(pid, {{Tortoise, data.client_id}, ref, {:error, :terminating}})
:keep_state_and_data
end
def handle_event(:cast, {:outgoing, caller, package}, _state, data) do
{:ok, package} = assign_identifier(package, data.pending)
track = Track.create({:negative, caller}, package)
next_actions = [
{:next_event, :internal, {:execute, track}}
]
data = %State{
data
| pending: Map.put_new(data.pending, track.identifier, track),
order: [track.identifier | data.order]
}
{:keep_state, data, next_actions}
end
# update
def handle_event(:cast, {:update, _}, :draining, _data) do
:keep_state_and_data
end
def handle_event(
:cast,
{:update, {_, %{identifier: identifier}} = update},
_state,
%State{pending: pending} = data
) do
with {:ok, track} <- Map.fetch(pending, identifier),
{:ok, track} <- Track.resolve(track, update) do
next_actions = [
{:next_event, :internal, {:execute, track}}
]
data = %State{
data
| pending: Map.put(pending, identifier, track),
order: [identifier | data.order -- [identifier]]
}
{:keep_state, data, next_actions}
else
:error ->
{:stop, {:protocol_violation, :unknown_identifier}, data}
{:error, reason} ->
{:stop, reason, data}
end
end
def handle_event(:cast, :reset, _, %State{pending: pending} = data) do
# cancel all currently outgoing messages
for {_, %Track{polarity: :negative, caller: {pid, ref}}} <- pending do
send(pid, {{Tortoise, data.client_id}, ref, {:error, :canceled}})
end
{:keep_state, %State{data | pending: %{}, order: []}}
end
# We trap the incoming QoS 2 packages in the inflight manager so we
# can make sure we will not onward them to the connection handler
# more than once.
def handle_event(
:internal,
{:onward_publish, %Package.Publish{qos: 2} = publish},
_,
%State{} = data
) do
:ok = Controller.handle_onward(data.client_id, publish)
:keep_state_and_data
end
# The other package types should not get onwarded to the controller
# handler
def handle_event(:internal, {:onward_publish, _}, _, %State{}) do
:keep_state_and_data
end
def handle_event(:internal, {:execute, _}, :draining, _) do
:keep_state_and_data
end
def handle_event(
:internal,
{:execute, %Track{pending: [[{:dispatch, package}, _] | _]} = track},
{:connected, {transport, socket}},
%State{} = data
) do
case apply(transport, :send, [socket, Package.encode(package)]) do
:ok ->
{:keep_state, handle_next(track, data)}
end
end
def handle_event(
:internal,
{:execute, %Track{pending: [[{:dispatch, _}, _] | _]}},
:disconnected,
%State{}
) do
# the dispatch will get re-queued when we regain the connection
:keep_state_and_data
end
def handle_event(
:internal,
{:execute, %Track{pending: [[{:respond, caller}, _] | _]} = track},
_state,
%State{client_id: client_id} = data
) do
case Track.result(track) do
{:ok, result} ->
:ok = Controller.handle_result(client_id, {caller, track.type, result})
{:keep_state, handle_next(track, data)}
end
end
def handle_event({:call, from}, :drain, {:connected, {transport, socket}}, %State{} = data) do
for {_, %Track{polarity: :negative, caller: {pid, ref}}} <- data.pending do
send(pid, {{Tortoise, data.client_id}, ref, {:error, :canceled}})
end
data = %State{data | pending: %{}, order: []}
disconnect = %Package.Disconnect{}
case apply(transport, :send, [socket, Package.encode(disconnect)]) do
:ok ->
:ok = transport.close(socket)
reply = {:reply, from, :ok}
{:next_state, :draining, data, reply}
end
end
# helpers ------------------------------------------------------------
defp handle_next(
%Track{pending: [[_, :cleanup]], identifier: identifier},
%State{pending: pending} = state
) do
order = state.order -- [identifier]
%State{state | pending: Map.delete(pending, identifier), order: order}
end
defp handle_next(_track, %State{} = state) do
state
end
# Assign a random identifier to the tracked package; this will make
# sure we pick a random number that is not in use
defp assign_identifier(%{identifier: nil} = package, pending) do
case :crypto.strong_rand_bytes(2) do
<<0, 0>> ->
# an identifier cannot be zero
assign_identifier(package, pending)
<<identifier::integer-size(16)>> ->
unless Map.has_key?(pending, identifier) do
{:ok, %{package | identifier: identifier}}
else
assign_identifier(package, pending)
end
end
end
# ...as such we should let the in-flight process assign identifiers,
# but the possibility to pass one in has been kept so we can make
# deterministic unit tests
defp assign_identifier(%{identifier: identifier} = package, pending)
when identifier in 0x0001..0xFFFF do
unless Map.has_key?(pending, identifier) do
{:ok, package}
else
{:error, {:identifier_already_in_use, identifier}}
end
end
end