Skip to content

Instantly share code, notes, and snippets.

@JesseStorms
Last active January 9, 2023 18:32
Show Gist options
  • Save JesseStorms/1b73a517fd5a9d8ac4f27f02a3e9e371 to your computer and use it in GitHub Desktop.
Save JesseStorms/1b73a517fd5a9d8ac4f27f02a3e9e371 to your computer and use it in GitHub Desktop.
LiveBook van Distributed

The CrashCourse

Run in Livebook

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}}

Foreword

Dit is afgeleid van alle slides, met wat extra bewijs en interactiviteit. Don't @ me if it's unclear, het is niet 100% compleet!

1_Processes

TL:DR

Elixir is very suited for concurrent programming thanks to the immutable state. Everything is handled sequentonally which makes it predictable as well

Check out:

Are there sync issues?

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!

Race conditions?

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

1_Concurrency

Doing things at the same time

Concurrency can be handled on two levels. Operating System and Language.

OS Concurrency

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

Language Level (in Elixir)

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

This means that
  • Your PC only sees one process running
  • BEAM handles the scheduling

BEAM
  • 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

Perks of BEAM
  • 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

5_GenServers

TL:DR - Processes

  • Unit of concurrency
  • Isolated and communicate using messages
  • Used to model a Server Process

Server Processes?

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.

Process - Server Process
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!

Self-made Server example

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}

Registering

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)

Mailbox

  • 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

Generic Server Process

Check out the setup on this notebook, you can find working code there

How?

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!

Template pattern?
  • 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

Terminology

Always remember that

  • Call -> Sync -> Expect messages!!
  • Cast -> Async -> Fire and forget!!

GenServers

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.

Feature highlights

  • Stopping the server:
    • return {:stop, reason} or {:ignore} from init/1
    • return {:stop, reason, new_state} from handle_* callbacks
    • Invoke GenServer.stop/3 from a client process
  • Use @impl GenServer when implementing callbacks
  • Register GenServer under a :name by providing a third :name argument to GenServer.start/3
    • e.g GenServer.start(mod, nil, name: hi)
    • This allows calling by :hi rather then PID.
  • Use __MODULE__ in a module to shorthand the name
Lifecycle of a GenServer

OTP-Compliant

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...

Example actually using use GenServer

# 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}

6_Links and monitors

These slides basically echo what we need for a Distributed App...

Communicating between processes

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.

Mailboxes

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

Erroring out

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)
How do we deal with them?

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

Links

"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>}

Monitors

"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.

7_Supervisors

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.

Restart stuff

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...

Working with supervisors

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

Details:

  • 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

Module-based Supervisor example:

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

Supervision tree

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.

8_Registy

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.

So what's a Registry exacly?

  • It's a seperate process where processes can register itself under some name
    • e.g name it :jimbo
  • Processes just ask the Registry for the PID

Example code

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

Via Tuple

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}}

8_Nodes

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...

Working with nodes

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

Process Discovery

There are two ways to do this, locally and globally. Due to how nodes work, I can't really demo global registry, sorry :c

Globally

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"]

Locally:

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>

Process groups

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())

8_Mix and closing stuff

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.

Some snippets to understand some elixir stuff:

# 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

Public examdoc

Question 1, Why isn't Elixir not concerned with Thread pools?

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.

Question 3: draw a Supervision tree

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!

Question 4: Where to define child specs, explain pros and cons

  • 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment