Mix.install([
{:kino, "~> 0.8.0"}
])
alias IEx.Helpers
### GENERIC SERVER####
defmodule GenericServer do
def start(application_module) do
spawn(fn ->
initial_state = application_module.init()
loop(application_module, initial_state)
end)
end
def call(server_pid, request) do
send(server_pid, {:call, request, self()})
receive do
{:response, response} -> response
after
5000 -> {:error, :timeout}
end
end
def cast(server_pid, request) do
send(server_pid, {:cast, request})
:ok
end
defp loop(application_module, current_state) do
receive do
{:call, request, caller} ->
{response, new_state} = application_module.handle_call(request, current_state)
send(caller, {:response, response})
loop(application_module, new_state)
{:cast, request} ->
new_state = application_module.handle_cast(request, current_state)
loop(application_module, new_state)
# Safeguard, ignore unexpected messages
{_any_message} ->
loop(application_module, current_state)
end
end
end
defmodule BrowserCounter do
# Interface functions
def start() do
GenericServer.start(BrowserCounter)
end
def request_browsers(pid) do
GenericServer.call(pid, {:retrieve_browsers})
end
def add_browser_hit(pid, browser) do
GenericServer.cast(pid, {:browser_hit, browser})
end
# Callback Modules
def init() do
%{}
end
def handle_call({:retrieve_browsers}, state) do
{state, state}
end
def handle_cast({:browser_hit, browser}, state) do
case Map.fetch(state, browser) do
:error -> Map.put_new(state, browser, 1)
{:ok, n} -> Map.put(state, browser, n + 1)
end
end
end
#### END GENERIC SERVER ####
#### START GENSERVER DEMO ####
defmodule GenBrowserCounter do
use GenServer
# Interface functions
def start() do
GenServer.start(__MODULE__, nil, name: __MODULE__)
end
def request_browsers(pid) do
GenServer.call(pid, :retrieve_browsers)
end
def add_browser_hit(pid, browser) do
GenServer.cast(pid, {:browser_hit, browser})
end
# Callback Modules
@impl GenServer
def init(_), do: {:ok, %{}}
@impl GenServer
def handle_call(:retrieve_browsers, _from, state) do
{:reply, state, state}
end
@impl GenServer
def handle_cast({:browser_hit, browser}, state) do
new_state =
case Map.fetch(state, browser) do
:error -> Map.put_new(state, browser, 1)
{:ok, n} -> Map.put(state, browser, n + 1)
end
{:noreply, new_state}
end
end
#### END GenServer####
#### DEMO LINKS AND MONITORS
defmodule EchoMon do
def init(next \\ nil) do
loop(next)
end
def loop(next \\ nil) do
receive do
{:echo, message, from} ->
shout_to(next, from, message)
loop(next)
{:update_next, from} ->
new_next = spawn(EchoMon, :init, [])
Process.monitor(new_next)
send(from, {new_next})
loop(new_next)
{:die} ->
raise("I died")
random_msg ->
IO.puts("Random message:")
IO.inspect(random_msg)
loop(next)
end
end
defp shout_to(nil, from, msg) do
shout(from, msg)
end
defp shout_to(pid, from, msg) do
echo_msg = shout(from, msg)
send(pid, {:echo, echo_msg, self()})
end
defp shout(from, msg) do
:timer.sleep(50)
echo_msg = "#{inspect(from)}: #{msg}"
IO.puts("\n" <> echo_msg)
echo_msg
end
end
defmodule EchoLink do
def init(next \\ nil) do
Process.flag(:trap_exit, true)
loop(next)
end
def loop(next \\ nil) do
receive do
{:echo, message, from} ->
shout_to(next, from, message)
loop(next)
{:update_next, from} ->
new_next = spawn_link(EchoLink, :init, [])
send(from, {new_next})
loop(new_next)
{:die} ->
raise("I died")
random_msg ->
IO.puts("Random message:")
IO.inspect(random_msg)
loop(next)
end
end
defp shout_to(nil, from, msg) do
shout(from, msg)
end
defp shout_to(pid, from, msg) do
echo_msg = shout(from, msg)
send(pid, {:echo, echo_msg, self()})
end
defp shout(from, msg) do
:timer.sleep(50)
echo_msg = "#{inspect(from)}: #{msg}"
IO.puts("\n" <> echo_msg)
echo_msg
end
end
defmodule EchoNaive do
def loop(next \\ nil) do
receive do
{:echo, message, from} ->
shout_to(next, from, message)
loop(next)
{:update_next, new_next} ->
loop(new_next)
end
end
defp shout_to(nil, from, msg) do
shout(from, msg)
end
defp shout_to(pid, from, msg) do
echo_msg = shout(from, msg)
send(pid, {:echo, echo_msg, self()})
end
defp shout(from, msg) do
:timer.sleep(50)
echo_msg = "#{inspect(from)}: #{msg}"
IO.puts("\n" <> echo_msg)
echo_msg
end
end
#### END DEMO LINKS AND MONITORS
{:module, EchoNaive, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:shout, 2}}
Dit is afgeleid van alle slides, met wat extra bewijs en interactiviteit. Don't @ me if it's unclear, het is niet 100% compleet!
Elixir is very suited for concurrent programming thanks to the immutable state. Everything is handled sequentonally which makes it predictable as well
Elixir is immutable, we essentially can't go outside of our process to adjust variables!
example (evaluate this codeblock, enter your own numbers):
name = Kino.Input.number("Whatever number you like")
x = Kino.Input.read(name)
# even elixir complains about this x
spawn(fn -> x = 42 end)
# The x in the parameters and the one inside the function are not the same
IO.puts(x)
# this will always be true
x == Kino.Input.read(name)
warning: variable "x" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)
distu/crash_course.livemd#cell:6jnxh5mgfdl6jbu3hbpnj352tb4f5mjv:3
420
true
spawn(/1)
has it's own callstack, so we're not about to go outside of that!
They can happen, however they're extremely easy to spot, see this article on the official website. Generally they won't happen if you're using GenServer.call/3
.
Check out this example.
y = 0
target = Kino.Input.read(name)
parent = self()
spawn(fn -> send(parent, {:set_y, 42}) end)
spawn(fn -> send(parent, {:set_y, 1}) end)
spawn(fn -> send(parent, {:set_y, target}) end)
# Livebook kinda messes with it though
y =
receive do
{:set_y, new_y} ->
IO.puts("set myself to #{new_y}")
new_y
end
y =
receive do
{:set_y, new_y} ->
IO.puts("set myself to #{new_y}")
new_y
end
# because elixir is funny
y =
receive do
{:set_y, new_y} ->
IO.puts("set myself to #{new_y}")
new_y
end
IO.puts(y)
# this will always be true
y == Kino.Input.read(name)
set myself to 42
set myself to 1
set myself to 420
420
true
Dit is een voorbeeld, schrijf nooit zo'n code
Doing things at the same time
Concurrency can be handled on two levels. Operating System and Language.
Note: Threads in the same process can read each other and code can be get into deadlocks (except Elixir)
- Every process has it's own memory
- A process has multiple threads
- Threads within the same process share memory
- Threads are the units of excution and are scheduled on the CPU core
Elixir has it's own Concurrency model used at Runtime, the unit being defined as a Process. This works with BEAM (which is basically a VM like Java), which handles and schedules the Runtime of Processes on it's own
- Your PC only sees one process running
- BEAM handles the scheduling
- BEAMVM Runs as a single OS process
- It creates # threads as CPU cores
- Every thread has a scheduler
- Elixir Processes are distributed among them
- Schedulers determine which run
- Every Process is completely isolated
- No memory sharing, no need for memory sync...
- Use messaging!
- Fault-tolerant by default
- Crashes cannot impact other processes (unless linking lol)
- Scalable by default
- Distrubuting can happen across multiple BEAM instances
- Requires nothing special code-wise
- Unit of concurrency
- Isolated and communicate using messages
- Used to model a Server Process
Yep, it's pretty easy to understand what's going on. Check back to Race Conditions to sort of see how that messaging thing works. It's a (very bad) implementation of what's essentially going on.
defmodule Process do
def start do #Start via function to encapsulate behavior
spawn(&loop/0)
end
defp loop do
receive do #handle calls here
_-> IO.puts("got something")
#...
end
loop() #loop this entire function, otherwise we exit!
end
It's worth noting that functions in this module run in different processes, there's no link between a module and a process!
Zie setup bovenaan voor de GenericServer. Reevaluate de codeblokken en geef uw eigen atoms mee voor een demo
# Resets the example! GenServers handle this better
pid = BrowserCounter.start()
BrowserCounter.request_browsers(pid)
BrowserCounter.add_browser_hit(pid, :firefox)
BrowserCounter.add_browser_hit(pid, :chrome)
BrowserCounter.request_browsers(pid)
%{chrome: 1, firefox: 1}
BrowserCounter.add_browser_hit(pid, :edge)
BrowserCounter.request_browsers(pid)
%{chrome: 1, edge: 1, firefox: 1}
Processes can register themselves under a local name, it has to:
- Be a atom (e.g
:thing
) - Only have one name
- Be unique
Process.register(self(), :whatever)
send(:whatever, :something)
- They have unlimited size
- Though we're limited in memory!
- The main way they impact other processes
- Best practice is to add a general match for unexpected stuff
Check out the setup on this notebook, you can find working code there
Well, there are a few requirements to make one, it needs to:
- Spawn a seperate process
- Run infinte loop to handle messages
- React to said messages
- Send out responses
- Maintain a state (see browser example!)
Makes sense to use the Template Pattern for this!
- Generic module
- It should be able to do callbacks to the app module
- This will accept a plug-in module as argument as atom (e.g
:functione
)- This is part of the generic state
- Invoke callbacks when needed
- Application module, which is your code for the app
Always remember that
- Call -> Sync -> Expect messages!!
- Cast -> Async -> Fire and forget!!
They literally do everything we described on top lol. You can use it like this
defmodule whatever do
use GenServer
end
This injects code during compilation, so there's a default implementation for all callback functions. Honestly, check out the hexdocs for GenServer.
- Stopping the server:
- return
{:stop, reason}
or{:ignore}
frominit/1
- return
{:stop, reason, new_state}
fromhandle_*
callbacks - Invoke
GenServer.stop/3
from a client process
- return
- Use
@impl GenServer
when implementing callbacks - Register GenServer under a
:name
by providing a third:name
argument toGenServer.start/3
- e.g
GenServer.start(mod, nil, name: hi)
- This allows calling by
:hi
rather then PID.
- e.g
- Use
__MODULE__
in a module to shorthand the name
Don't use plain processes, become OTP. Idk what he's trying to say here lol but this is from the powerpoint
- Use OTP-Compliant processes
- Can be used in Supervision Trees
- Error are logged in detail
- They play nice with BEAM/OTP
- What processes are compliant?
- GenServer
- Supervisors
- Task, Agent...
# gpid = GenBrowserCounter.start() #If we evaluate this twice, then it will say it's already started
{_what, prod} = GenBrowserCounter.start()
gpid =
case prod do
{_, pid} -> pid
_pid -> pid
end
# GenBrowserCounter.request_browsers(gpid)
# this is a cast, change the atom!
GenBrowserCounter.add_browser_hit(gpid, :firefox)
GenBrowserCounter.request_browsers(gpid)
# Find the relevant startup code in the setup!
%{firefox: 8}
These slides basically echo what we need for a Distributed App...
This is more interesting though
Essentially, if you've had Service Oriented Architecture, you know what it is. Processes have a message bus which they use to send and recieve stuff from everyone. It's handled FIFO wise.
Honestly this is very self explainatory
#Sending something
pid = spawn(fn -> ... end)
send(pid,:thing)
#Receiving
receive do
:thing -> do_stuff() #pattern match you know the deal
_ -> im_stuff()
after
5000 -> timed_out() # after 5seconds
end
Beam has three error types:
- Error; unexpected behaviour
raise("error")
- Exit; on purpose dying
exit("for no reason lol")
- Throw; allows non-local return*?*
throw(:value)
Well we know Elixir is really optimized, isolated and finecrained... just let it crash (also known as offensive programming!)
- Self-healing is easy
- Elixir is built for that
- Processes are isolated
- Focus on recovery
- Easier to manage
- We don't have to deal with every edge case
"When I die, you die"
That's the idea of links anyways, exit signals are propigated to linked processes. You can exit trap a process, which means you can deal with the crash.
Links are birectional!
Check out these examples:
# Link without trapping
pid_A = spawn(EchoLink, :init, [])
send(pid_A, {:update_next, self()})
pid_B =
receive do
{new_pid} -> new_pid
end
send(pid_B, {:update_next, self()})
pid_C =
receive do
{new_pid} -> new_pid
end
send(pid_A, {:echo, "Hello world", self()})
#
send(pid_A, {:die})
send(pid_A, {:echo, "Hello world", self()})
send(pid_B, {:echo, "Hello world", self()})
send(pid_C, {:echo, "Hello world", self()})
{:echo, "Hello world", #PID<0.273.0>}
# With trapping
pid_A = spawn(EchoLink, :init, [])
send(pid_A, {:update_next, self()})
pid_B =
receive do
{new_pid} -> new_pid
end
send(pid_B, {:update_next, self()})
pid_C =
receive do
{new_pid} -> new_pid
end
send(pid_A, {:echo, "Hello world", self()})
send(pid_C, {:die})
send(pid_C, {:echo, "Hello world", self()})
send(pid_A, {:echo, "Hello world", self()})
{:echo, "Hello world", #PID<0.273.0>}
"I'm watching you..."
Essentially the main difference between it and a link is that Monitors do not die when the monitored process dies. It receives a message instead.
It's unidirectional, you can monitor a process. Said process doesn't really know it's being monitored.
Check out these examples:
pid_A = spawn(EchoMon, :init, [])
send(pid_A, {:update_next, self()})
pid_B =
receive do
{new_pid} -> new_pid
end
send(pid_B, {:update_next, self()})
pid_C =
receive do
{new_pid} -> new_pid
end
send(pid_A, {:echo, "Hello world", self()})
send(pid_C, {:die})
send(pid_C, {:echo, "Hello world", self()})
send(pid_A, {:echo, "Hello world", self()})
Random message:
{:echo, "Hello world", #PID<0.273.0>}
{:DOWN, #Reference<0.150070124.794558473.148492>, :process, #PID<0.340.0>,
{%RuntimeError{message: "I died"},
[
{EchoMon, :loop, 1,
[
file: 'distu/crash_course.livemd#cell:setup',
line: 137,
error_info: %{module: Exception}
]}
]}}
14:52:31.361 [error] Process #PID<0.340.0> on node :"gucvgnk6-livebook_server@livebook" raised an exception
** (RuntimeError) I died
distu/crash_course.livemd#cell:setup:137: EchoMon.loop/1
#PID<0.273.0>: Hello world
#PID<0.273.0>: Hello world
#PID<0.338.0>: #PID<0.273.0>: Hello world
#PID<0.338.0>: #PID<0.273.0>: Hello world
# Extra Naive approach, idk why i have it
pid_A = spawn(EchoNaive, :loop, [])
pid_B = spawn(EchoNaive, :loop, [])
pid_C = spawn(EchoNaive, :loop, [])
send(pid_A, {:update_next, pid_B})
send(pid_B, {:update_next, pid_C})
send(pid_A, {:echo, "Hello world", self()})
{:echo, "Hello world", #PID<0.273.0>}
#PID<0.273.0>: Hello world
#PID<0.708.0>: #PID<0.273.0>: Hello world
#PID<0.709.0>: #PID<0.708.0>: #PID<0.273.0>: Hello world
Just like usual, check out the Setup code for implementation.
I can't really provide proper demo's in this book due to it being so massive :c.
tl:dr, Supervisors are the glue of the app
Basically a supervisor just manages other processes, using monitors, exit traps and links. Said processes are called it's children (which makes it hard to say 'kill the subprocess')
It provides fault-tolerance and encapsulates how stuff starts and stops, does what it needs to do when a child terminates and brings down all children when itself shuts down.
Processes should be name registerd so we can deal with invalidating passed PIDs
Supervisor starts and stops sequentially! Be mindful with blocking code
Finally, Supervisors should be OTP complient and can be a child of another Supervisor.
It has a bunch of strats:
- One for One
- Restart the stopped child
- One for all
- literally restart every child if one breaks
- Rest for one
- Restart all the younger children after a crash...
If the Supervisor cannot restart a child after some tries, it will exit itself...
Two flavors:
- 'generic' via
start_link/2
- Using a module-based supervisor
Both of them need a list of children to start, which defines how they should deal with them. It contains info like:
- How do I start my child?
- What should I do when it crashes?
- How do I distinguish my children?
what if she takes the kids???
Example of a child spec:
Supervisor.start_link(
[ #map of the kids and specs
%{
id: BrowserCounter #this is a kid
start: {BrowserCounter, :start_link,[nil]} #this is how to start the kid
}
],
strategy: :one_for_one) #strat
This is kinda bad tho! Calling from a hardcoded function is fragile. Do something like this instead
Supervisor.start_link(
[
{GenServerModule,[nil]}
],
strategy: :one_for_one)
%{id: GenServerModule, start:{GenServerModule, :start_link,[nil]}} #returns this
- Mandatory:
- :id
- :start - Tuple containing how to start stuff
- Optional:
- :restart - behaviour to restart
- :shutdown - shutdown behaviour for a kid (timeout or :brutal_kill👀)
- :type - worker or supervisor
There are defualt params here, check the hexdocs
defmodule MySuperVisor do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, nil)
end
def init(_) do
Supervisor.init([BrowserCounter], strategy: :one_for_one)
end
end
This is actually part of 8, but it's only 2 slides like bruh
Essentially it's a nested structure of supervisors, a registry and workers. Looks like this:
- The best way to make a fault tolerant app
- You can now control what gets restarted and stuff
- Error can be handled locally or propegated up.
Well if we're spawning mulitple things, we gotta keep track of stuff.
If a Supervisor has to restart a process, It gets a new PID, so we can't rely on those since they can be invalidated!
Solution? Use a registry
- We manually manage PIDs
- We should only allow one instance.
- It's a seperate process where processes can register itself under some name
- e.g name it
:jimbo
- e.g name it
- Processes just ask the Registry for the PID
Registry.start_link(name: :reg, keys: :unique) #or you can pick :duplicate!
Registry.register(:reg, {:jeff, 1},nil)#register under name(tuple in this case) and arb. val
[{jeff_pid, value}] = Registry.lookup(:reg, {:jeff,1}) #returns pid and value that was registered
- Registry should be linked to all registered processes
- If Registry dies, everything should aswell
This is something you can do:
GenServer.start_link(callback_module, arg, name: name)
Okay we're using a tuple in the form of {:via, module, arg}
, why? It turns out, we can let the module
to register itself. Essentially it runs module
, passing arg
. We can then use the entire tuple instead of a PID!
The specified module is usually a Registry Process and looks like this
{:via, reg_name, {process_name, process_key}}
Here's an example from the slides:
defmodule ExampleServer do
use GenServer
def start_link(id) do
GenServer.start_link(__MODULE__, nil, name: via_tuple(id))
end
def call(id, some_request) do
GenServer.call(via_tuple(id), some_request)
end
defp via_tuple(id) do
{:via, Registry, {:my_registry, {__MODULE__, id}}}
end
def handle_call(some_request, _, state) do
{:reply, some_request, state}
end
end
# if someone wants to expand this, go ahead
warning: function init/1 required by behaviour GenServer is not implemented (in module ExampleServer).
We will inject a default implementation for now:
def init(init_arg) do
{:ok, init_arg}
end
You can copy the implementation above or define your own that converts the arguments given to GenServer.start_link/3 to the server state.
distu/crash_course.livemd#cell:rufgmf5cewqwi2qbpefxgmm3w5soqfi5:1: ExampleServer (module)
{:module, ExampleServer, <<70, 79, 82, 49, 0, 0, 19, ...>>, {:handle_call, 3}}
Due to the nature of nodes, i can't really demo this as neatly
Nodes are analog to BEAM instances! This means that one machine can run multiple BEAM Machines. Every Node has multiple processes...
Run iex --sname peko
or iex --name peko@localhost
to name a named IEx shell
# gets the current node.
c_node = node()
# gets all connected node, but this will always be empty!
node_list = Node.list()
IO.puts(c_node)
# running on another node
Node.spawn(c_node, fn -> IO.puts("I sent this from #{c_node}, probably myself") end)
# can't really demo this, but this sends it to another node
Node.spawn(c_node, fn -> send(self(), {:response, "talking to myself"}) end)
Helpers.flush()
xbqo5gth-livebook_server@livebook
I sent this from xbqo5gth-livebook_server@livebook, probably myself
:ok
There are two ways to do this, locally and globally. Due to how nodes work, I can't really demo global registry, sorry :c
u_text = Kino.Input.text("What should we put")
some_func = spawn(fn -> IO.puts("i had to put '#{Kino.Input.read(u_text)}'") end)
# gives us :ok
:global.register_name({:something, "thingy"}, some_func)
IO.inspect(:global.whereis_name({:something, "thingy"}))
:global.registered_names()
#PID<0.711.0>
i had to put 'my name jeff'
[something: "thingy", jeff: "jeff"]
some_func =
spawn(fn -> IO.puts("i had to put '#{Kino.Input.read(u_text)}', which is stored locally") end)
# registering
Process.register(some_func, :r_name)
# sending to the process
send(:r_name, "sending you good vibes")
IO.inspect(Process.whereis(:r_name))
#PID<0.712.0>
i had to put 'my name jeff', which is stored locally
#PID<0.712.0>
You can also register multiple processes under the same name like this, according to the powerpoint i based this on (it doesn't work)
# Node 1
:pg2.start()
:pg2.create({:process_type, "name"})
# Node 2
:pg2.start()
:pg2.which_groups()
:pg2.join({:process_type, "name"}, self())
:pg2.get_members({:process_type, "name"})
:pg2.join({:process_type, "name"}, self())
Okay for the people who had IP Major, you already know what mix
does. If you need a reminder; it's basically the NPM of Elixir. Essentially, a toolchain that lets us use dependancies and start our applications correctly. I can't properly show this in a book, sorry :c
Otherwise, this is about all of the knowledge you'd need for the exam (according to all the powerpoints i've seen). Best advice I can give you is to try make sense of it yourself. Write your own summary! LiveBook is really easy to install and is what I'm using to write this up.
# Case matching
u_input = {:error, {:already_exists, "something"}}
case u_input do
{:error, {:doesnt_exist, x}} ->
"There's tuple containing :error and a tuple containing :doesnt_exist. x got bound to #{x}"
{:error, {:already_exists, nil}} ->
"Almost there, you shouldn't see this unless you set the last thing to 'nil'"
{_, {:already_exists, x}} ->
"Okay, this matches. The '_' catches everything, :already_exists matches with the atom and we bound x to #{x}"
{:error, {_, x}} ->
"This doesn't match because the top case already got matched! Change :already_exists in the input to something else, you should see this and get x bound to #{x}"
_ ->
"this is a catchall"
end
"Okay, this matches. The '_' catches everything, :already_exists matches with the atom and we bound x to something"
# Cond stuff, it's basically the same as case matching but more with true/false
cond do
2 + 2 == 5 ->
"can never happen"
2 * 2 == 5 ->
"still can't"
3 + 3 == 6 ->
"this works because 3 plus 3 is 6 :)"
end
"this works because 3 plus 3 is 6 :)"
# Note that every value except nil and false is considered true, example
thing = nil
cond do
thing -> "you won't see this"
false -> "and neither this"
420 -> "but you will see this"
true -> "this as well if 420 didn't exist"
end
"but you will see this"
defmodule Demo do
# Variable rules
# Generally if you have a unused variable, prefix it with a _
def thing(x, y) do
# we complain about y, because we never use it here!
x
end
# Pattern matching in functions
# if x=x, then run this one. The thing is that we can't underscore this because we check in our
# function definition. Neat
def is_same(x, x) do
true
end
# otherwise run this one. We should prefix these because we don't use them
def is_same(_x, _y) do
false
end
end
IO.inspect(Demo.is_same(1, 2))
IO.inspect(Demo.is_same(2, 2))
warning: variable "y" is unused (if the variable is not meant to be used, prefix it with an underscore)
distu/crash_course.livemd#cell:s6palduwxoqqcdexrsh62qmjd74f7hyv:4: Demo.thing/2
false
true
true
Traditionally, programming languages work with thread pools because creating kernel level threads is pretty intense on the system. Having pre-made threads increase cut down on those costs.
Elixir doesn't need those thanks to BEAM. BEAM is analog to the JVM, it's a virtual enviroment for Elixir which runs as a single Process. It essentially takes a kernel level thread per CPU core and multiplexes that thread for itself. Basically we're forgoing the OS's threading and making our own.
BEAM now schedules and decides when to context switch to the specific process. It's efficient with handling so many threads, even on lightweight machines.
Question 2: What's the dangers of using a variable to keep track of state? Does Elixir have this issue too?
Essentially it introduces a factor of unpredictability. Elixir doesn't have mutatable variables, meaning that the state changing for no reason is hard to accomplish. Every process has it's own heap, it cannot influence another stack. The only way to change states is to create an entire new state.
Because of that, Elixir works with a internal mailbox system. It makes it easier to reason when something changes.
Draw a fictional Supervision Tree of 3 (or more) levels deep with each node having at least 2 children (you will thus need at least 6 processes excluding the application/root supervisor). Indicate what type of process each node is (supervisor, genserver, …). Choose your own possible start-up order and explain it.
Start-up:
- Root supervisor starts up
- Supervisor A starts up (depends on Root)
- Supervisor B starts up (depends on A)
- Gen B1 and B2 start up (Depends on B)
- Gen A1 and A2 start up (Depends on A)
- Supervisor C starts up (Depends on Root)
- Gen C1 and C2 start up (Depends on C)
Note that it might change depending on how the child specs are written!
- Default (code injection) using GenServer/Agent
- Easy but can lead to unused code
- Passing it as a map
- Full control
- Lots of manual work tho
- Using Child_Specs
- Full control again
- Less work