Skip to content

Instantly share code, notes, and snippets.

@mgwidmann
Last active September 25, 2022 19:58
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgwidmann/f0696034f2d73d116cd4547ac06c7f86 to your computer and use it in GitHub Desktop.
Save mgwidmann/f0696034f2d73d116cd4547ac06c7f86 to your computer and use it in GitHub Desktop.
Overview of the Supervision system
# The golden trinity of Erlang is the secret sauce behind what makes
# Elixir a strong choice for any backend application and/or system.
# https://tkowal.wordpress.com/2015/10/20/failing-fast-and-slow-in-erlang-and-elixir/
# The supervision and worker system, commonly referred to as OTP (Open
# Telecom Platform, which has nothing to do with telephony), is what
# gives Erlang/Elixir applications them a robust "self-healing" type
# property that makes them extremely fault tolerant.
# Erlang/Elixir is an Actor based system. Each "Process" runs within
# the virtual machine and is only similar to OS processes in design.
# They are concurrent, share nothing, and are cheap to make.
# Fire up iex to play around
$ iex
# The current actor's PID (the one waiting for input) can be retrieved
# from the `self` function, available everywhere.
iex> self
#PID<0.91.0>
# And we can kill ourself like so (equivalent of `kill -9`, no chance for recovery)
iex> Process.exit(self, :kill)
** (EXIT from #PID<0.91.0>) killed
Interactive Elixir (1.2.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
# The terminal starts back up as if for the first time, resets the command
# counter and our history (hitting up arrow) is gone also. A supervisor
# restarted the IEx process and it has "healed". We can see that by checking
# our own PID again to see that it has changed.
iex> self
#PID<0.106.0>
# Lets start a new mix project, passing `--sup` to tell mix to generate
# a supervision tree for us. Elixir applications heal themselves like
# the superhero Wolverine, so lets make a project based on him.
$ mix new wolverine --sup
# As we can see in mix.exs:5, our app is named `:wolverine`, and on line
# 18, we tell it to use the module `Wolverine` to start our application.
# In lib/wolverine.ex we have our application, where we start the children
# of our application (which currently happens to be empty). Lets pretend we're
# coding a robot version of Wolverine and we want to model out what that might
# look like.
# Lets start at the top and work our way down, create the head.
# In lib/wolverine/head.ex write the following:
################################## DISCLAIMER #########################################
# Supervision tree design is a whole different topic. The design presented here
# is in no way a typical supervision tree design. Typically you'd design as systems
# and subsystems, based upon their dependencies between each system they should be
# supervised accordingly with potentially different supervision strategies.
defmodule Wolverine.Head do
use Supervisor
def start_link() do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def init() do
children = [
]
IO.puts "#{__MODULE__} (#{inspect self}) started up from!"
supervise(children, strategy: :one_for_one)
end
end
# Copy that same file into lib/wolverine/body.ex and lib/wolverine/enemy.ex
# and rename the module name on line 1 to be Wolverine.Body and Wolverine.Enemy
# respectively. Also, in Woverine.Enemy, replace `Supervisor` with `GenServer` everywhere
# and delete everything in the `init/1` function except the print statement and return
# {:ok, nil} instead.
# And make sure it gets started up, modify lib/wolverine.ex:11
supervisor(Wolverine.Head, []),
worker(Wolverine.Enemy, [])
# And the head supervises the body... modify lib/head.ex:10
supervisor(Wolverine.Body, [])
# Add the final pieces to complete him. Copy lib/wolverine/enemy.ex into
# lib/wolverine/claw.ex and lib/wolverine/leg.ex. In those two files Change the `start_link/0` to
# accept a name parameter so that we can start up multiple of them:
def start_link(name) do
GenServer.start_link(__MODULE__, name: name)
end
# In lib/wolverine/body.ex add four workers on line 10. This will start multiple independent
# versions of the claw and leg. The second parameter is a list of arguments passed to `start_link`:
worker(Wolverine.Claw, [Wolverine.LeftClaw], id: Wolverine.LeftClaw),
worker(Wolverine.Claw, [Wolverine.RightClaw], id: Wolverine.RightClaw),
worker(Wolverine.Leg, [Wolverine.LeftLeg], id: Wolverine.LeftLeg),
worker(Wolverine.Leg, [Wolverine.RightLeg], id: Wolverine.RightLeg)
# Start up the system and see the printout of each supervisor/worker
$ iex -S mix
Erlang/OTP 18 [erts-7.3] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Elixir.Wolverine.Head (#PID<0.88.0>) started up from!
Elixir.Wolverine.Body (#PID<0.89.0>) started up from!
Elixir.Wolverine.Claw (#PID<0.90.0>) started up from!
Elixir.Wolverine.Claw (#PID<0.91.0>) started up from!
Elixir.Wolverine.Leg (#PID<0.92.0>) started up from!
Elixir.Wolverine.Leg (#PID<0.93.0>) started up from!
Elixir.Wolverine.Enemy (#PID<0.94.0>) started up from!
Interactive Elixir (1.2.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
# Take note of the PIDs there, we can get their PID and kill them
# If we kill the body, it will regenerate, but because it supervises
# the claws and legs, they also will be recreated.
iex> Process.whereis(Wolverine.Body) |> Process.exit(:kill)
Elixir.Wolverine.Body (#PID<0.97.0>) started up from!
Elixir.Wolverine.Claw (#PID<0.98.0>) started up from!
Elixir.Wolverine.Claw (#PID<0.99.0>) started up from!
Elixir.Wolverine.Leg (#PID<0.100.0>) started up from!
Elixir.Wolverine.Leg (#PID<0.101.0>) started up from!
true
iex(2)>
# Notice the actors for the Head and Enemy are isolated from the crash.
# Only the subsystems we designated are rebooted.
iex> Process.whereis(Wolverine.Head)
#PID<0.88.0>
iex> Process.whereis(Wolverine.Enemy)
#PID<0.94.0>
# We can visualize our supervision tree by opening up observer and
# looking at the applications tab
iex> :observer.start
# Finally lets make them able to fight! Add to both the lib/wolverine/claw.ex and
# lib/wolverine/leg.ex the below function:
@attack_power 1
def handle_cast({:attack, enemy}, _) do
IO.puts "Attacking #{inspect enemy} with #{@attack_power} damage"
:timer.sleep(1000) # Sleep one second
GenServer.cast(enemy, {:hit, @attack_power})
{:noreply, nil}
end
# To make the claw more powerful, change the attack power to 5.
# Lets make sure the enemy can respond to the message we sent him.
# In lib/wolverine/enemy.ex add a function to handle the incoming message:
def handle_cast({:hit, damage}, _) do
IO.puts "Ouch, hit with #{damage} damage!"
{:noreply, nil}
end
# Lastly, when any piece of code wants to make an attack, we need to make a function
# to make that easy to do. Typically this goes inside the GenServer module, but we can
# put it anywhere.
# In lib/wolverine.ex lets add the attack function
def attack(attack_with, enemy) do
GenServer.cast(attack_with, {:attack, enemy})
end
# Lets try it out
$ iex -S mix
iex> Wolverine.attack(Wolverine.LeftClaw, Wolverine.Enemy)
Attacking Wolverine.Enemy with 5 damage
:ok
iex> Ouch, hit with 5 damage!
# Notice the attack message and prompt shows up immediately but the ouch
# message is a second later
iex> Wolverine.attack(Wolverine.RightLeg, Wolverine.Enemy)
Attacking Wolverine.Enemy with 1 damage
:ok
iex> Ouch, hit with 1 damage!
# Another fun tool is a third party project visualixir
# https://github.com/koudelka/visualixir
# Clone the repo, run `mix deps.get` and start it up with
$ iex --name visualixir@127.0.0.1 -S mix phoenix.server
# Restart the wolverine application with
$ iex --name wolverine@127.0.0.1 -S mix
# Join the nodes together
iex> Node.connect :"visualixir@127.0.0.1"
# View at http://localhost:4000/, select the wolverine node and find among
# all the dots the ones labeled with the claws and legs as well as the enemy.
# Alt + click them to add tracing so we can see the messages coming in and
# out, then execute the attack functions again to see the messages flying
# back and forth.
@hopewise
Copy link

a typo in def init() do where it should be def init(state) do

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment