Connection Supervision
An important aspect of building an Elixir application is setting up a supervision structure that ensures the application will continue to work if parts of the system should reach an erroneous state and need to get restarted into a known working state. To do this one needs to group the processes the application consist of in a manner such that processes belonging together will start and terminate together.
Tortoise
offers multiple ways of supervising one or multiple
connections; by using the provided dynamic Tortoise.Supervisor
or
starting a dynamic supervisor belonging to the application using the
connections; or by starting the connections needed directly in an
application supervisor. This document will describe the ways of
supervision, and give an overview for when to use a given supervision
strategy.
Linked Connection
A connection can be started and linked to the current process by using
the Tortoise.Connection.start_link/1
function.
Tortoise.Connection.start_link(
client_id: HeartOfGold,
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883},
handler: {Tortoise.Handler.Logger, []}
)
As with any other linked process both process will terminate if either
terminate, as described in the Process.link/1
documentation. This
means that any stored state in the process that owns the MQTT connection
will disappear with the process if the connection process
terminates. Therefore it is not recommended to link a connection
process like this outside of experimenting in IEx
, but instead to run
it inside of a supervisor process. When properly supervised connections
terminate, the crash will be contained, allowing the other processes
to keep their state.
Supervising a connection
The Tortoise.Connection
module provides a child_spec/1
which makes
it easier to start a Tortoise.Connection
as part of a supervisor by
simply passing a {Tortoise.Connection, connection_specification}
to
the supervisor child list.
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{Tortoise.Connection,
[
client_id: WombatTaskForce,
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883},
handler: {Tortoise.Handler.Logger, []}
]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
The great thing about this approach is that the connection can live in the same supervision tree as the rest of the application that depends on that connection. The connection is started, restarted, and stopped with the application as a whole, ensuring the connection is closed with the processes that depend on it.
Be sure to set a reasonable connection strategy for the
supervisor. Refer to the Supervisor
documentation for more
information on usage and configuration.
The Tortoise.Supervisor
When Tortoise
is included as a dependency in the mix.exs file of
an application, Tortoise
will automatically get started alongside the
application. During the application start up a dynamic supervisor will
spawn and register itself under the name Tortoise.Supervisor
. This
can be used to start supervised connections that will get restarted if
they are terminated with an abnormal reason.
To start a connection on the Tortoise.Supervisor
one can use the
Tortoise.Supervisor.start_child/2
function, which defaults to using
the dynamic supervisor registered under the name
Tortoise.Supervisor
.
Tortoise.Supervisor.start_child(
client_id: "heart-of-gold",
handler: {Tortoise.Handler.Logger, []},
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883}
)
This is an easy and convenient way of getting started, as everything
needed to supervise a connection is there when the Tortoise
application has been initialized. One downside is that while the
children are supervised they are not grouped with the application that
uses the connections; they are grouped with the Tortoise
application. To mitigate this, a Tortoise.Supervisor.child_spec/1
function is available, which can be used to start the
Tortoise.Supervisor
as part of another supervisor.
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{Tortoise.Supervisor,
[
name: MyApp.Connection.Supervisor,
strategy: :one_for_one
]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
Connections can now, dynamically, be attached to the supervised
Tortoise.Supervisor
by calling the
Tortoise.Supervisor.start_child/2
function with the name
that was given to the supervisor, in this case
MyApp.Connection.Supervisor.
Tortoise.Supervisor.start_child(
MyApp.Connection.Supervisor,
client_id: SmartHose,
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883},
handler: {Tortoise.Handler.Logger, []}
)
This is the best way of supervising a dynamic set of connections, but might be overkill if only one static connection is needed for the application.
Summary
Tortoise
makes it possible to spawn connections and supervise them,
and it is always a best practice to supervise a connection to ensure it
remains up. Different approaches can be taken depending on the
situation:
If a fixed number of connections are needed, the recommended way is to attach them directly to a supervision tree, along with the processes that depend on said connections using the
Tortoise.Connection.child_spec/1
function.If a dynamic set of connections is needed, the recommended way is to spawn a named
Tortoise.Supervisor
as part of a supervisor, which holds the processes that depend on the connections, and spawn the connections on the dynamic supervisor.
Supervising the connections along the processes that rely on them ensures that the application can be started and stopped as a whole, and makes it possible to recover from a faulty state.