Supervised Mix Projects
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Navigation
Home Report An Issue Monster SpawnerSupervisor And GenServer DrillsReview Questions
- What is the benefit of using a supervised mix project?
- How do you configure a mix project with a supervisor?
Supervised Mix Projects
Supervised mix projects use an Application module that defines application callbacks.
The application module is configured in mix.exs
in the application/0
function.
def application do
[
extra_applications: [:logger],
mod: {MyApp.Application, []}
]
end
The application module typically lives in the project folder in a my_app/application.ex
file and contains the logic to start the application. In a supervised application, this module starts a supervision tree.
defmodule MyApp.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: RockPaperScissors.Worker.start_link(arg)
# {RockPaperScissors.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
We can generate a mix project with all of the above scaffolding using the --sup
flag, or add it manually to an existing project.
mix new my_app --sup
Old
We can start mix projects under a supervisor.
Use the --sup
flag to generate a supervised mix project.
To demonstrate, we’re going to create a supervised rock paper scissors application.
$ mix new rock_paper_scissors --sup
Which creates the following files.
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/rock_paper_scissors.ex
* creating lib/rock_paper_scissors/application.ex
* creating test
* creating test/test_helper.exs
* creating test/rock_paper_scissors_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd rock_paper_scissors
mix test
Run "mix help" for more commands.
This creates a standard mix project, with two main differences.
First, there is a application.ex
file. This file defines a start/2
function which starts a named supervisor RockPaperScissors.Supervisor
.
defmodule RockPaperScissors.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: RockPaperScissors.Worker.start_link(arg)
# {RockPaperScissors.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: RockPaperScissors.Supervisor]
Supervisor.start_link(children, opts)
end
end
Next, the mix.exs
file’s application/0
function has changed to include a :mod
value.
This :mod
key defines the args value when the RockPaperScissors.Application.start/2
function is called.
def application do
[
extra_applications: [:logger],
mod: {RockPaperScissors.Application, []}
]
end
Starting Our Application
The RockPaperScissors.Application.start/2
function is called when we run the project, and
the :mod
value in application/0
defines the args
argument value.
Let’s prove that. Add an IO.puts/2 message in the start/2
function. We’ll also remove the comments
for the sake of conciseness, but you can leave them in if you prefer.
def start(_type, args) do
IO.puts(args)
children = []
opts = [strategy: :one_for_one, name: RockPaperScissors.Supervisor]
Supervisor.start_link(children, opts)
end
We’ll also modify args
to prove the connection.
def application do
[
extra_applications: [:logger],
mod: {RockPaperScissors.Application, "Starting Application"}
]
end
Now start the project in the IEx shell and you should see "Starting Application"
in the logs.
$ iex -S mix
Erlang/OTP 24 [erts-12.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Starting Application
Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
Since the RockPaperScissors.Application.start/2
function starts the RockPaperScissors.Supervisor
it
should be alive. We can use Supervisor.count_children/1 to see that the RockPaperScissors.Supervisor
has
started with no child workers.
iex(1)> Supervisor.count_children(RockPaperScissors.Supervisor)
%{active: 0, specs: 0, supervisors: 0, workers: 0}
Starting Child Workers
We’re going to create a RockPaperScissors
GenServer in the lib/rock_paper_scissors.ex
file which handles the logic for playing our game.
We’ll create a very basic named GenServer for now and an IO.puts/2 message to ensure it starts correctly.
defmodule RockPaperScissors do
use GenServer
def start_link(opts) do
IO.puts("RockPaperScissors started")
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(state) do
{:ok, state}
end
end
We then have to put the RockPaperScissors
as one of the children
for our supervisor in application.ex
.
def start(_type, args) do
IO.puts(args)
children = [
{RockPaperScissors, []}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: RockPaperScissors.Supervisor]
Supervisor.start_link(children, opts)
end
Quit and restart the IEx shell. You should see the Started RockPaperScissors
message in the output.
Erlang/OTP 24 [erts-12.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Starting Application
RockPaperScissors started
Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
We can confirm the RockPaperScissors
worker is being supervised by the RockPaperScissors.Supervisor
using
Supervisor.which_children/1.
iex(1)> Supervisor.which_children(RockPaperScissors.Supervisor)
[{RockPaperScissors, PID<0.163.0>, :worker, [RockPaperScissors]}]
RockPaperScissors Game
Now that we have the RockPaperScissors
worker started and supervised, we can create the logic
for the game.
The RockPaperScissors game will prompt the player to enter either rock
, paper
, or scissors
as a string.
We want to repeatedly prompt the user to enter a choice for rock, paper, or scissors.
So we’ll send the RockPaperScissors
module a :prompt
message over and over again.
defmodule RockPaperScissors do
use GenServer
def start_link(state) do
IO.puts("RockPaperScissors started")
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
send(self(), :prompt)
{:ok, state}
end
def handle_info(:prompt, state) do
choice = "Please enter rock, paper, or scissors.\n" |> IO.gets() |> String.trim()
send(self(), :prompt)
{:noreply, state}
end
end
Now, you’ll notice that if we try to start the project in the IEx shell, we can’t actually interact with the console to provide the player’s choice.
$ iex -S mix
Erlang/OTP 24 [erts-12.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
Compiling 1 file (.ex)
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.Interactive Elixir (1.13.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
We can instead run a mix project with mix run
. However, mix run
will exit once the RockPaperScissors.start/2
callback
completes.
$ mix run
mix run
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
$
We can use the --no-halt
flag to avoid this issue.
Now we can keep entering a choice over and over.
$ mix run --no-halt
mix run
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
rock
Please enter rock, paper, or scissors.
paper
Please enter rock, paper, or scissors.
Now we’ve got everything in place to implement the actual rock paper scissors game.
defmodule RockPaperScissors do
use GenServer
def start_link(state) do
IO.puts("RockPaperScissors started")
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
def init(state) do
send(self(), :prompt)
{:ok, state}
end
def handle_info(:prompt, state) do
choice = "Please enter rock, paper, or scissors.\n" |> IO.gets() |> String.trim()
answer = Enum.random(["rock", "paper", "scissors"])
result =
case {choice, answer} do
{"rock", "scissors"} -> "You win!"
{"paper", "rock"} -> "You win!"
{"scissors", "paper"} -> "You win!"
{"rock", "paper"} -> "You Lose!"
{"paper", "scissors"} -> "You Lose!"
{"scissors", "rock"} -> "You Lose!"
{same, same} -> "Draw!"
end
IO.puts(result)
send(self(), :prompt)
{:noreply, state}
end
end
Now we can play the game!
$ mix run --no-halt
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
rock
You Win!
Please enter rock, paper, or scissors.
However, to demonstrate the benefit of our supervisor, we’ve purposely left a bug in the code above. The case
statement doesn’t handle input
other than "rock"
, "paper"
, or "scissors"
. Even "Rock"
, "Paper"
, and "Scissors"
are not valid answers.
$ mix run --no-halt
Starting Application
RockPaperScissors started
Please enter rock, paper, or scissors.
Rock
19:34:09.995 [error] GenServer RockPaperScissors terminating
** (CaseClauseError) no case clause matching: {"Rock", "scissors"}
(rock_paper_scissors 0.1.0) lib/rock_paper_scissors.ex:20: RockPaperScissors.handle_info/2
(stdlib 3.17.1) gen_server.erl:695: :gen_server.try_dispatch/4
(stdlib 3.17.1) gen_server.erl:771: :gen_server.handle_msg/6
(stdlib 3.17.1) proc_lib.erl:226: :proc_lib.init_p_do_apply/3
Last message: :prompt
State: []
Please enter rock, paper, or scissors.
Notice that even though our game crashes, the supervisor restarts the RockPaperScissors
process so we can keep playing. That’s fault-tolerance in action!
Commit Your Progress
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Supervised Mix Projects reading"
$ git push
We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.