Skip to content

Commit

Permalink
Merge 58d2f3b into a5392e9
Browse files Browse the repository at this point in the history
  • Loading branch information
gausby committed Jul 18, 2018
2 parents a5392e9 + 58d2f3b commit 800712b
Show file tree
Hide file tree
Showing 37 changed files with 1,115 additions and 105 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ transport can be used like this:

``` elixir
Tortoise.Supervisor.start_child(
client_id: "my_client_id",
client_id: "smart-spoon",
handler: {Tortoise.Handler.Logger, []},
server: {Tortoise.Transport.SSL, host: host, port: port, key: key, cert: cert},
subscriptions: [{"foo/bar", 0}])
Expand Down
202 changes: 202 additions & 0 deletions docs/connecting_to_a_mqtt_broker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Connecting to a MQTT Broker

Tortoise is capable of connecting to any MQTT broker that implements
the 3.1.1 version of the MQTT protocol (support for MQTT 5 is
planned). It does so by taking a connection specification, and with it
will do its best to connect to the broker and keeping the connection
open.

A minimal connection specification looks like this:

``` elixir
{ok, _pid} =
Tortoise.Connection.start_link(
client_id: HelloWorld,
server: {Tortoise.Transport.Tcp, host: "localhost", port: 1883},
handler: {Tortoise.Handler.Logger, []}
)
```

This will establish a TPC connection to a broker running on
*localhost* port *1883*. The connection takes a module that implements
the `Tortoise.Handler`-behaviour; In this case the
`Tortoise.Handler.Logger` callback module, which will print a log
statement on events happening during the connection life cycle.

Furthermore, we specify that the `client_id` of the connection is
`HelloWorld`. The client id can later be used to interact with the
connection, such as publishing messages and subscribing to topics.

Notice that this example expect a server configured to allow anonymous
connections. Not all MQTT brokers are configured the same, so
depending on the server more configuration options might be needed for
a successful connection. This document aims to give an overview.

## Network Transport

Tortoise has an abstraction for the network transport, and comes
with two official implementations included in Tortoise itself:

- `Tortoise.Transport.Tcp` used to connect to a broker via
TCP. While this transport is the simplest to use it is also the
least secure, and should only be used on trusted networks. It is
based on `:gen_tcp` found in the Erlang/OTP distribution.

- `Tortoise.Transport.SSL` used to create a secure connection via
secure socket layer. This option takes a bit more work to setup,
but it will prevent people from eavesdropping on the data being
sent between the client and the broker.

The transports are given with the `server` field in the connection
specification as a tuple, containing a the transport type and an
options list specific to the given transport.

`Tortoise.Transport.Tcp` takes two options; the host name as a string,
such as `"localhost"` or a four-tuple describing an IP-network
address, such as `{127, 0, 0, 1}`. An example where the TCP transport
is used can be seen in the introduction to this article.

The `Tortoise.Transport.SSL` is a bit more versatile in its
configuration options. It is based on the `:ssl` module from the
Erlang distribution, so be sure to check the documentation for the
`:ssl` module for detailed information on the possible configuration
options.

*[todo, describe SSL connection better when I have internet]*

Information on creating a custom transport can be found in the
`Tortoise.Transport` module, but for most cases the TCP and SSL module
should suffice.

## Connection Handler

A handful of events are possible during a client life cycle. Tortoise
aim to expose the interesting events as callback functions, defined in
the `Tortoise.Handler`-behaviour, making it possible to implement
custom behavior for the client. The exposed events are:

- The client is initialized, or terminated allowing for
initialization and tear down of subsystems
- A connection to the server is establish
- The connection to the server is dropped
- The subscription status to a given topic filter is changed
- A message is received on one of the subscribed topic filters

Read more about defining custom behavior for a connection in the
documentation for the `Tortoise.Handler`-module.

## The `client_id`

In MQTT the clients announce themselves to the broker with what is
referred to as a *client id*. Two clients cannot share the same client
id on a broker, and depending on the implementation (or configuration)
the server will either kick the first client, or deny the new client
if it specifies a client id already in use.

The protocol specifies that a valid client id is between 1 and 23
UTF-8 encoded bytes in length, but some server configurations may
allow for longer ids; thus tortoise will allow for client identifiers
longer than 23 bytes but some MQTT brokers might reject the
connection.

Allowed values are a string, or an atom. If an atom is specified it
will be converted to a string when the connection message is send on
the wire, but it will be possible to refer to the connection using the
atom, which can be more convenient. Notice that the client id can
easily reach the 23 bytes when converted from an atom because atoms
starting with an uppercase letter will be prefixed with *Elixir.*;
therefore `MyClientId` will be 17 bytes instead of the 10 one could
expect.

``` elixir
iex(1)> client_id = Atom.to_string(MyClientId)
"Elixir.MyClientId"
iex(2)> byte_size(client_id)
17
```

The specified client identifier is used to identify a connection when
publishing messages, subscribing to topics, or otherwise interacting
with the named connection.

``` elixir
{ok, _pid} =
Tortoise.Connection.start_link(
client_id: MyClient,
server: {Tortoise.Transport.Tcp, host: "localhost", port: 1883},
handler: {Tortoise.Handler.Logger, []}
)

Tortoise.publish(MyClient, "foo/bar", "hello")
```

**Notice**: Though the MQTT 3.1.1 protocol allow for a zero-byte
client id—in which case the server should assign a random `client_id`
for the connection—a client id is enforced in Tortoise. This is
done so the connection has an identifier that can be used when
interacting with the connection.

## User Name and Password

Some brokers are configured to require some basic authentication,
which will determine whether a user is allowed to subscribe or publish
to a given topic, and some set limitations to what quality of service
a particular user, or group of users, are allowed to subscribe or
publish with.

To specify a user name and password for a connection the aptly named
*user_name* and *password* comes in handy. Both of them take UTF-8
encoded strings, or `nil` as their value, in which case an anonymous
connection is attempted. Depending on the broker configuration it is
allowed to specify a user name and omit the password, but the user
name has to be specified if a password is specified.

Both default to `nil` if left blank.

## The keep alive interval

When connected a MQTT client should ping the server on a set interval
to let the broker know that it is still alive. The keep alive value is
given as an integer, describing time in seconds between keep alive
messages, and should be set depending on factors such as power
consumption, network bandwidth, etc. Per default Tortoise will sent a
keep alive message every 60 seconds, which is a reasonable value for
most installations. The allowed maximum value is `65_535`, which is 18
hours, 12 minutes, and 15 seconds; most would consider this a bit too
extreme, and some brokers might reject connections specifying a too
long `keep_alive` interval.

Some brokers allow disabling the keep alive interval by setting it to
zero, so `Tortoise` allow for a `keep_alive` specified as `0`. Note
that the broker can still choose to disconnect a given client on the
grounds of inactivity. When `keep_alive` is disabled the broker
implementation will decide its own measure of inactivity, so to avoid
unspecified behavior it is advised to use a keep alive value.

## Last will message

It is possible to specify a message which should be dispatched by the
broker if the client is disconnected from the broker abruptly. This
message is known as the last will message, and allow for other
connected clients to act on other clients leaving the broker.

The last will message is specified as part of the connection, and for
Tortoise it is possible to configure a last will message by passing in
a `Tortoise.Package.Publish` struct to the *will* connection
configuration field.

``` elixir
{:ok, pid} =
Tortoise.Connection.start_link(
client_id: William,
server: {Tortoise.Transport.Tcp, host: 'localhost', port: 1883},
handler: {Tortoise.Handler.Logger, []},
will: %Tortoise.Package.Publish{topic: "foo/bar", payload: "goodbye"}
)
```

If we have another client connected to the broker, subscribing to
*foo/bar*, we should now receive a message containing the message
*goodbye* on that topic, should the client called *William* disconnect
abruptly from the broker. We can simulate this by terminating the *pid*
using `Process.exit(pid, :ouch)`.
178 changes: 178 additions & 0 deletions docs/connection_supervision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Connection Supervision

An important aspect of building an Elixir application is setting up a
supervision structure that ensure the application will continue
working if parts of the system should reach an erroneous state and
need to get restarted into a known working state. To do this one need
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.

``` elixir
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
mean that any stored state in the process that own 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 run
it inside of a supervisor process. When properly supervised connection
terminations 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.

``` elixir
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 depend
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 along 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`.

``` elixir
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
need 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.

``` elixir
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 and specifying the name
that was given to the supervisor, in this case
*MyApp.Connection.Supervisor*.

``` elixir
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 best practice to supervise a connection to ensure it
remains up. Different approaches can be taken depending on the
situation:

* If a fixed amount 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`

* If a dynamic set of connections are needed the recommended way is
to spawn a named `Tortoise.Supervisor` as part of a supervisor,
which hold the processes that depend on the connections, and spawn
the connections on the dynamic supervisor.

Supervising the connections along the processes that rely on the
connection ensure that the application can be started and stopped as a
whole, and makes it possible to recover from faulty state.

0 comments on commit 800712b

Please sign in to comment.