View Source Graceful Startup
A client that gracefully handles errors in configuration at start-up time.
As the documentation for GenServer.init/1
details, a GenServer may return
:ignore
instead of {:ok, initial_state}
in its GenServer.init/1
callback. This will cause the client to
exit normally without entering the loop or calling
GenServer.terminate/2
. If used when part of a supervision tree the parent supervisor will not fail to start nor immediately try to restart theGenServer
.
This makes it a good option for handling failures to start the client.
Slipstream.init/1
is mostly a wrapper around GenServer.init/1
, so we
may return :ignore
and see the same behavior.
Tutorial
We start with an empty client module
(c5a08fd
)
defmodule MyApp.GracefulStartupClient do
end
And immediately fill out the Slipstream basics like a
start_link/1
implementation (so the module may be
supervised), and an invocation of use Slipstream
(8d44dd0
)
defmodule MyApp.GracefulStartupClient do
use Slipstream
@moduledoc "..."
def start_link(opts) do
Slipstream.start_link(__MODULE__, opts, name: __MODULE__)
end
end
Then we add a basic implementation of the Slipstream.init/1
callback
(a51e77c
)
@impl Slipstream
def init(_args) do
config = Application.fetch_env!(:my_app, __MODULE__)
{:ok, connect!(config)}
end
But note that this introduces two potential raise
ing errors in our
Slipstream.init/1
callback:
Application.fetch_env!/2
will raise of the key-value pair is not defined in configuration (config/*.exs
)Slipstream.connect!/2
will raise if the configuration passed as the first argument is not valid according toSlipstream.Configuration
.
And also note that a raise
in an Slipstream.init/1
callback will fail
the start-up of the supervisor process. If this client is started in the
Application supervision tree (lib/my_app/application.ex
), it will take
down the entire application on error.
So let's refactor this to make it a bit more safe! First, we switch
Application.fetch_env!/2
to its more graceful counterpart:
Application.fetch_env/2
, which returns {:ok, config}
when
the configuration is defined and :error
when it is not
(90ceb4a
)
@impl Slipstream
def init(_args) do
with {:ok, config} <- Application.fetch_env(:slipstream, __MODULE__) do
{:ok, connect!(config)}
else
:error -> :ignore
end
end
Now the client will attempt to connect only if the configuration
is defined. But it can still fail, as we use the raising
Slipstream.connect!/2
. We refactor that to Slipstream.connect/2
in
b3be445
:
@impl Slipstream
def init(_args) do
with {:ok, config} <- Application.fetch_env(:slipstream, __MODULE__),
{:ok, socket} <- connect(config) do
{:ok, socket}
else
:error -> :ignore
{:error, _reason} -> :ignore
end
end
So now in either of our failure-cases, the client will simply not start-up instead of potentially crashing the entire application. This is an application of graceful degradation: in cases of system failure, we degrade our performance instead of entirely giving up.
In this example, we go on to add helpful Logger
messages that declare when a failure case has been met
(09d1a83
).
To see the full example code, open up
examples/graceful_startup/client.ex
.
A small test suite can be found at
test/slipstream/examples/graceful_startup_test.exs
.