Last active
September 25, 2022 19:58
-
-
Save mgwidmann/f0696034f2d73d116cd4547ac06c7f86 to your computer and use it in GitHub Desktop.
Overview of the Supervision system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 [email protected] -S mix phoenix.server | |
# Restart the wolverine application with | |
$ iex --name [email protected] -S mix | |
# Join the nodes together | |
iex> Node.connect :"[email protected]" | |
# 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. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
a typo in
def init() do
where it should bedef init(state) do