~udia/servy

Final OTP Application
Fault Recovery with OTP Supervisors

refs

master
browse  log 

clone

read-only
https://git.sr.ht/~udia/servy
read/write
git@git.sr.ht:~udia/servy

You can also use your local clone with git send-email.

#Servy

TODO: Add description

#Installation

If available in Hex, the package can be installed by adding servy to your list of dependencies in mix.exs:

def deps do
  [
    {:servy, "~> 0.1.0"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/servy.

#Notes

#Create Mix Project

# run elixir, passing in relative path to elixir file
$ elixir lib/servy.ex
# interactive elixir, use `c` helper to compile and run file
$ iex

iex> c "lib/servy.ex"

# alternatively
$ iex lib/servy.ex
$ iex -S mix  # makes interactive elixir project aware

# to recompile while in interactive session, use `r` helper
iex> r Servy

#High-Level Transformations

def handle(request) do
  conv = parse(request)
  conv = route(conv)
  format_response(conv)
end

# equivalent
def handle(request) do
  request
  |> parse
  |> route
  |> format_response
end

#Simple Pattern Matching

[1, 2, 3] = [1, 2, 3]
[first, 2, last] = [1, 2, 3]
# first is 1, last is 3
[first, 4, last] = [1, 2, 3]
# MatchError
[first, last] = [1, 2, 3]
# Match Error

#Immutable Data

All objects in Elixir are immutable.

conv = %{ method: "GET", path: "/wildthings" }

# can access using get square bracket with atom key
"GET" = conv[:method]
"/wildthings" = conv[:path]
nil = conv[:request_body]

# for atom keys, can also use dot notation, but will raise error if not exist
"GET" = conv.method
"/wildthings" = conv.path
conv.request_body # KeyError

#Function Clauses

Rather than using conditional statements (if/elif/else), it is more idiomatic in Elixir to write function clauses.

def route(conv) do
  if conv.path == "/wildthings" do
    %{conv | resp_body: "Bears, Liöns, Tigers"}
  else
    if conv.path == "/bears" do
      %{conv | resp_body: "Teddy, Smokey, Paddington"}
    else
      %{conv | resp_body: "idk"}
    end
  end
end

can be rewritten as

def route(conv), do: route(conv, conv.path)

def route(conv, "/wildthings") do
  %{conv | resp_body: "Bears, Liöns, Tigers"}
end

def route(conv, "/bears") do
  %{conv | resp_body: "Teddy, Smokey, Paddington"}
end

def route(conv, _path) do
  %{conv | resp_body: "idk"}
end

#Additional Pattern Matching

All related function clauses should be grouped together. They are evaluated in the order defined in the source code, so putting the catch all at the top will effectively make the other function clauses inaccessible.

Additionally, match operators can be used within a function clause.

# will match all paths containing "/bears/anyvalue/here"
def route(conv, "GET", "/bears/" <> id) do
  %{conv | status: 200, resp_body: "Bear #{id}"}
end

#Advanced Pattern Matching

Rather than transforming route/1 into route/3 function clauses, we can modify route/1 to accept a map and perform pattern matching on the individual keys.

# def route(conv, "GET", "/wildthings") do # becomes
def route(%{method: "GET", path: "/wildthings"} = conv) do
  %{conv | status: 200, resp_body: "Bears, Liöns, Tigers"}
end

Cannot do string concatenation match with dynamic length variable in non tail position. Example overcoming this limitation using Regexp:

# def rewrite_path(%{path: "/" <> thing <> "?id=" <> id} = conv) do # error
def rewrite_path(%{path: path} = conv) do
  regex = ~r{\/(?<thing>\w+)\?id=(?<id>\d+)}
  captures = Regex.named_captures(regex, path)
  rewrite_path_captures(conv, captures)
end

def rewrite_path_captures(conv, %{"thing" => thing, "id" => id}) do
  %{conv | path: "/#{thing}/#{id}"}
end

def rewrite_path_captures(conv, nil), do: conv

#Reading a File, Case Operator

Function clauses can be written as a case conditional.

def route(%{method: "GET", path: "/about"} = conv) do
  # Get the absolute path of the pages file, relative to current file's directory
  Path.expand("../../pages", __DIR__)
  |> Path.join("about.html")
  |> File.read()
  |> handle_file(conv)
end

defp handle_file({:ok, content}, conv) do
  %{conv | status: 200, resp_body: content}
end

defp handle_file({:error, :enoent}, conv) do
  %{conv | status: 404, resp_body: "File not found!"}
end

defp handle_file({:error, reason}, conv) do
  %{conv | status: 500, resp_body: "File error: #{reason}"}
end

Using case instead of function clauses.

def route(%{method: "GET", path: "/about"} = conv) do
  # Get the absolute path of the pages file, relative to current file's directory
  file_path =
    Path.expand("../../pages", __DIR__)
    |> Path.join("about.html")

  case File.read(file_path) do
    {:ok, content} ->
      %{conv | status: 200, resp_body: content}

    {:error, :enoent} ->
      %{conv | status: 404, resp_body: "File not found!"}

    {:error, reason} ->
      %{conv | status: 500, resp_body: "File error: #{reason}"}
  end
end

The __DIR__ variable holds the existing file's directory path, relative to where the program session was started.

#Module Attributes

Elixir modules have two built in module attributes, @moduledoc for the module-level documentation string, and @doc for the function level documentation.

defmodule Sample.Module do
  @moduledoc """
  My module level documentation.
  """

  @hello_output "world"

  @doc "outputs 'world', module-level attributes set on compile"
  def hello, do: IO.puts(@hello_output)

  @hello_output "is valid"

  @doc """
  outputs 'This is valid'.
  """
  def wow do
    IO.puts("This #{@hello_output}")
  end
end

#Code Reorganization

An elixir project spanning multiple files can be run in interactive mode using iex -S mix. Calling individual modules will not correctly compile the dependent modules.

The module naming convention does not imply hierarchy. To reference functions in other modules, you can call the function using {module name}.{function} or by importing the function into the current scope.

defmodule Sample.Plugins do
  def foo, do: "whoa"
  def bar, do: "bruh"
end

defmodule Sample.Module do
  import Sample.Plugins, only: [foo: 0]
  def hello do
    IO.puts("#{foo()}, #{Sample.Plugins.bar()}")
  end
end

When importing modules, the only option specifies specific functions (provide the function arity) instead of importing everything. Alternative imports:

# import all of the functions only
import SomeModule, only: :functions

# import all of the macros only (`defmacro`)
import SomeModule, only: :macros

#Struct Usage

Rather than using a map (%{}) it can be useful to define the keys beforehand to ensure more strict semantics. Structs are defined within their own module- a module cannot have more than one struct. Structs are maps that allow default values for keys and compile time assertions.

defmodule Servy.Conv do
  defstruct method: "", path: "", resp_body: "", status: nil

  def full_status(conv) do
    "#{conv.status} #{status_reason(conv.status)}"
  end

  defp status_reason(code) do
    %{
      200 => "OK",
      201 => "Created",
      401 => "Unauthorized",
      403 => "Forbidden",
      404 => "Not Found",
      500 => "Internal Server Error"
    }[code]
  end
end

The defstruct macro takes in a list of fields which are the atom keys. If a list of atoms are provided, they will all default to nil.

defmodule Post do
  defstruct [:title, :content, :author]
end

See h defstruct for more information.

#Matching Heads and Tails

Using the | operator on a list will separate out the first element from the rest of the list.

nums = [1, 2, 3, 4, 5]
[head | tail] = nums
head == 1
tail == [2, 3, 4, 5]

[head | tail] = tail
head == 2
tail == [3, 4, 5]

[head | tail] = tail
head == 3
tail == [4, 5]

[head | tail] = tail
head == 4
tail == [5]

[head | tail] = tail
head == 5
tail == []

[head | tail] = tail
# MatchError

Additionally access to the head and the tail can also be done using the functions hd and tl.

nums = [1, 2, 3]
hd(nums)
# 1
tl(nums)
# [2, 3]

#Recursion

Elixir does not have looping, instead traversal of iterables is done through recursion.

defmodule Recurse do
  def loopy([head | tail]) do
    IO.puts "Head: #{head} Tail: #{inspect(tail)}"
    loopy(tail)
  end
  def loopy([]), do: IO.puts "Done!"
end

Recurse.loopy([1, 2, 3, 4, 5])
# Head: 1 Tail: [2, 3, 4, 5]
# Head: 2 Tail: [3, 4, 5]
# Head: 3 Tail: [4, 5]
# Head: 4 Tail: [5]
# Head: 5 Tail: []
# Done!

Or with state, like summing the numbers together:

# no additional state
defmodule Recurse do
  def sum([head | tail]) do
    head + sum(tail)
  end
  def sum([]), do: 0
end

Recurse.sum([1, 2, 3, 4, 5])

# keep track of a running total (more efficient! uses tail-call optimization)
defmodule Recurse do
  def sum([head | tail], total) do
    IO.puts "Total: #{total} Head: #{head} Tail: #{inspect(tail)}"
    sum(tail, total + head)
  end

  def sum([], total), do: total
end

IO.puts Recurse.sum([1, 2, 3, 4, 5], 0)

Triple all the numbers in a list:

# stacking function calls
defmodule Recurse do
  def triple([head | tail]) do
    [3 * head | triple(tail)]
  end
  def triple([]), do: []
end

Recurse.triple([1, 2, 3, 4, 5])

# more efficient tail-call optimization approach
defmodule Recurse do
  def triple([head | tail], partial) do
    triple(tail, Enum.concat(partial, [head * 3]))
  end
  def triple([], partial), do: partial
end

Recurse.triple([1, 2, 3, 4, 5], [])

# what the course suggested to do
defmodule Recurse do
  def triple(list) do
    triple(list, [])
  end

  defp triple([head|tail], current_list) do
    triple(tail, [head*3 | current_list])
  end

  defp triple([], current_list) do
    current_list |> Enum.reverse()
  end
end

IO.inspect Recurse.triple([1, 2, 3, 4, 5])

#Slicing and Dicing with Enum

# Ampersand operator for simplifying anonymous function to named function
phrases = ["lions", "tigers", "bears", "oh my"]
Enum.map(phrases, fn(x) -> String.upcase(x) end)
# ["LIONS", "TIGERS", "BEARS", "OH MY"]

# Equivalent!
Enum.map(phrases, &String.upcase(&1))
# ["LIONS", "TIGERS", "BEARS", "OH MY"]

# Also Equivalent!
Enum.map(phrases, &String.upcase/1)
# ["LIONS", "TIGERS", "BEARS", "OH MY"]

The ampersand wraps a named function in an anonymous function, and the numbers indicate the argument order. This can also be done with expressions.

add2 = fn(a, b) -> a + b end
add2.(1, 2)
# 3

#equivalent
add2 = &(&1 + &2)
add2.(3, 4)
# 7

Consider these examples for capturing String.duplicate/2:

String.duplicate("foo", 3)
# "foofoofoo"

dup = fn(string, num) -> String.duplicate(string, num) end
dup.("foo", 3)
# "foofoofoo"

dup = &String.duplicate(&1, &2)
dup.("foo", 3)
# "foofoofoo"

dup = &String.duplicate/2
dup.("foo", 3)
# "foofoofoo"
#Guard Clauses

These are conditionals that you can define at the function argument level that sets boolean checks on the argument types for function clause matching.

defmodule Doubler do
  def get_double_value(inp) when is_integer(inp) do
    inp * 2
  end
  def get_double_value(inp) when is_binary(inp) do
    inp |> String.to_integer |> get_double_value
  end
end

Doubler.get_double_value(30)
# 60
Doubler.get_double_value("40")
# 80

#Comprehensions

Enum.map([1, 2, 3], fn(x) -> x * 3 end)
# [3, 6, 9]

for x <- [1, 2, 3], do: x * 3
# [3, 6, 9]

In this above example, the generator is x <- [1, 2, 3]. An example with two generators is shown:

for size <- ["S", "M", "L"], color <- [:red, :blue], do: {size, color}

[
  {"S", :red},
  {"S", :blue},
  {"M", :red},
  {"M", :blue},
  {"L", :red},
  {"L", :blue}
]

You can also pattern match within comprehensions:

prefs = [ {"Betty", :dog}, {"Bob", :dog}, {"Becky", :cat} ]
for {name, :dog} <- prefs, do: name
["Betty", "Bob"]

# More explicit equivalent
for {name, pet_choice} <- prefs, pet_choice == :dog, do: name
["Betty", "Bob"]

# Using a function as the predicate expression
cat_lover? = fn(choice) -> choice == :cat end
for {name, pet_choice} <- prefs, cat_lover?.(pet_choice), do: name
["Becky"]

By default, values returned by a do block of a comprehension are packaged into a list. However, the :into option can return the values into anything that inherits Collectable.

style = %{"width" => 10, "height" => 20, "border" => "2px"}

# This is what we want to do, but how to make this a comprehension?
Map.new(style, fn {key, val} -> {String.to_atom(key), val} end)
%{border: "2px", height: 20, width: 10}

# this outputs a list
for {key, val} <- style, do: {String.to_atom(key), val}
[border: "2px", height: 20, width: 10]

# this outputs a map (notice the :into)
for {key, val} <- style, into: %{}, do: {String.to_atom(key), val}
%{border: "2px", height: 20, width: 10}

See an example using a deck of playing cards:

ranks =
  [ "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A" ]

suits =
  [ "♣", "♦", "♥", "♠" ]

# Show all cards
all_cards = for rank <- ranks, suit <- suits, do: {rank, suit}
IO.inspect(all_cards)

# Shuffle and deal out a single hand of 13 random cards
all_cards |> Enum.shuffle |> Enum.slice(0, 13)

# Shuffle and deal out four hands of 13 cards each
all_cards |> Enum.shuffle |> Enum.chunk_every(13)
#EEx Templates
<h1>All The Bears!</h1>

<ul>
  <%= for bear <- bears do %>
  <li><%= bear.name %> - <%= bear.type %></li>
  <% end %>
</ul>

The notice the subtle differences in the opening expression tags <%= and <%. In EEx, all expressions that output something to the template must include the equals = sign.

#Test Automation

By default, setting up a mix project will generate a test directory that uses ExUnit for testing.

# to run a specific test file
mix test test/handler_test.exs

# to run all test cases test/*_test.exs
mix test

# to run a specific test that is failing, set the line number of the given test
mix test test/handler_test.exs:7

mix help test

#Rendering JSON

The video tutorial uses Poison, but I used Jason instead. TODO: Look up Protocol Module Consolidation

Jason.encode!(%{"age" => 44, "name" => "Steve Irwin", "nationality" => "Australian"})
"{\"age\":44,\"name\":\"Steve Irwin\",\"nationality\":\"Australian\"}"

To use external libraries, add it to the dependencies list in mix.exs. Then run mix deps.get.

#Web Server Sockets

All Erlang libraries can be used in Elixir projects because Elixir is transpiled into erlang bytecode to be run in an erlang virtual machine.

Example of converting erlang code into elixir code:

server() ->
  {ok, LSock} = gen_tcp:listen(5678, [binary, {packet, 0},
                                      {active, false}]),
  {ok, Sock} = gen_tcp:accept(LSock),
  {ok, Bin} = do_recv(Sock, []),
  ok = gen_tcp:close(Sock),
  ok = gen_tcp:close(LSock),
  Bin.
defmodule Servy.OldHttpServer do
  def server do
    # {:ok, lsock} = :gen_tcp.listen(5678, [:binary, {:packet, 0}, {:active, false}])
    {:ok, lsock} = :gen_tcp.listen(5678, [:binary, packet: 0, active: false])
    {:ok, sock} = :gen_tcp.accept(lsock)
    {:ok, bin} = :gen_tcp.recv(sock, 0)
    :ok = :gen_tcp.close(sock)
    :ok = :gen_tcp.close(lsock)
    bin
  end
end
  • Erlang atoms have a lowercase letter start. Elixir atoms start with a colon (:) character.
  • Erlang variables start with an uppercase letter. Elixir atoms start with a lowercase letter.
  • Erlang modules are referenced as atoms. E.g. Erlang gen_tcp becomes Elixir :gen_tcp.
  • Erlang function calls use a colon (:) while Elixir function calls use a dot (.). E.g. Erlang gen_tcp:listen becomes Elixir :gen_tcp.listen
  • Erlang strings are not Elixir strings. Erlang "hello" becomes Elixir 'hello'
    • Erlang, double-quoted strings are a list of characters.
    • Elixir: double quoted strings are a sequence of bytes. To make a list of characters, use a single qoted string.

Wrote a quick webserver using gen_tcp to hook into our existing handler and serve responses to a http client (such as a web browser).

#Concurrent, Isolated Processes

Within Elixir, the spawn function is used to create a processes that runs concurrently in the background.

# spawn/1
spawn(fn() -> IO.puts "Hello world" end)

# spawn/3
spawn(IO, :puts, ["Hello world"])
  • spawn/1 takes a zero-arity anonymous function.
  • spawn/3 takes the module name, the function name as an atom, and a list of arguments passed to the function.

The functions spawned in the serve function are closures. All variables that are defined within the scope of the function are deep copied. Processes do not share memory.

To get the PID of the current process, use self().

IO.puts "Current PID: #{inspect self()}"

We can count the number of Elixir processes like so:

Process.list |> Enum.count
# equivalent
:erlang.system_info(:process_count)

Refer to the Erlang system_info function for more details.

Within an iex session, the :observer.start function will open up a graphical user interface enabling you to inspect overall system information and the individual Erlang/Elixir processes currently running in the application.

#Sending and Receiving Messages

Elixir processes (are not operating system processes and) have the following properties:

  • extremely lightweight and fast to spawn
  • run concurrently on a single CPU
    • if multiple CPU cores are available, runs in parallel
  • isolated from other processes (no sharing of memory or variables)
  • have their own private mailbox
  • communicate with other processes only by sending and receiving messages
parent = self()

# spawn three children processes to send messages to the parent
spawn(fn -> send(parent, "Yes") end)
spawn(fn -> send(parent, "No") end)
spawn(fn -> send(parent, "Maybe") end)

# the messages currently in the parent process mailbox
Process.info(parent, :messages)
# {:messages, ["Yes", "No", "Maybe"]}

receive do msg -> msg end
# "Yes"

Process.info(parent, :messages)
# {:messages, ["No", "Maybe"]}

flush
# "No"
# "Maybe"
# :ok

#Asynchronous Tasks

It is common practice to keep track of process IDs when working with asynchronous tasks in order to map message results back to their originating spawn call. Elixir provides a convenience function for dispatching asynchonous commands and retrieving the corresponding results.

task = Task.async(fn -> Servy.Tracker.get_location("bigfoot") end)
Task.await(task)  # by default, times out after 5 seconds raising exception

task = Task.async(Servy.Tracker, :get_location, ["bigfoot"])
Task.await(task, 7000)  # will wait for 7 seconds before timing out raising exception

task = Task.async(fn -> Servy.Tracker.get_location("bigfoot") end)
Task.await(task, :infinity)  # indefinite block

Because Task.await waits for a message to arrive it can only be called once for a given task. Use Task.yield to determine if a task has completed.

task = Task.async(fn -> :timer.sleep(8000); "Done!" end)

# waits 5 seconds and returns nil due to task not finishing within cutoff time
Task.yield(task, 5000)
nil

Task.yield(task, 5000)
{:ok, "Done!"}

If working with a receive block, consider setting a timeout using the after clause.

pid = Fetcher.async(fn -> Servy.Tracker.get_location("bigfoot") end)
Fetcher.get_result(pid)

# Will timeout after 2 seconds
def get_result(pid) do
  receive do
    {^pid, :result, value} -> value
  after 2000 ->
    raise "Timed out!"
  end
end
#Converting Milliseconds

Erlang timer module has useful built-in millisecond conversion functions.

:timer.seconds(5)
5000

:timer.minutes(5)
300000

:timer.hours(5)
18000000

#Stateful Server Processes

Within Elixir, modules cannot store state (in most OO languages, you can have class attributes that are shared among all instances of the class). Instead, you need to spawn a process and pass state into the process by arguments.

#Registering Unique Process Names
# Store the registered name of the PID as a module level constant
@name :pledge_server
# Register the PID under this name
Process.register(pid, @name)
# Then send to this name rather than the PID
send @name, {self(), :create_pledge, name, amount}

# an error will be raised if another attempt to register using the same name is made
Process.register(pid2, @name)

Referring to the Servy.PledgeServer.start/0 function we registered the spawned process under the name :pledge_server.

Servy.PledgeServer.start()
#PID<0.200.0>

# Determine the PID registered under a name
Process.whereis(:pledge_server)
#PID<0.200.0>

# Unregistering a process name can also be done
Process.unregister(:pledge_server)
#true

Process.whereis(:pledge_server)
#nil
#Agents

The Agent module is a simple wrapper around a server process that can store state and offers access to the state via a client interface.

iex> {:ok, agent} = Agent.start(fn -> [] end)
{:ok, #PID<0.90.0>}

A process is spawned containing an elixir list in memory. The agent is bound to a PID.

iex> Agent.update(agent, fn(state) -> [ {"larry", 10} | state ] end)
:ok
iex> Agent.update(agent, fn(state) -> [ {"moe", 20} | state ] end)
:ok

Additional calls for updating the state are provided. Pass in a function that takes the state and returns the new state.

iex> Agent.get(agent, fn(state) -> state end)
[{"moe", 20}, {"larry", 10}]

To retrieve the agent's state, pass the agent's PID and a function that returns the state.

#Refactoring Towards GenServer

In our Servy.PledgeServer example, we refactored our module such that all 'Generic Server' behaviour is defined in the Servy.GenericServer module. It supports initialization via start/3 (taking in the callback module, initial state, and name). It handles blocking actions via call/2 as well as non-blocking actions via cast/2 (both taking in the server PID and message as arguments). The server listen_loop/2 will handle listening for new call and cast messages, referencing the callback module's functions for server side logic.

#OTP GenServer

  • Task: For one-off computations or queries
  • Agent: For a simple process that holds state
  • GenServer: For long-running server processes that stores state and performs work concurrently
    • If you need to serialize access to a shared resource or service, GenServer is a decent choice
    • If you need to schedule background work to be performed on a periodic interval, GenServer is a decent choice
#GenServer callback functions
  • handle_call(message, from state)
    • Invoked to handle synchronous requests sent by the client using GenServer.call(pid, message).
    • Typically return {:reply, reply, new_state} which sends the reply to the client and recursively loops with the new_state.
    • Can return {:stop, reason, new_state} which will exit the process with reason.
    • Default use GenServer implementation returns {:stop, {:bad_call, msg}, state} and stops the server. You should implement a handle_call function clause for every message your server can handle.
  • handle_cast(message, state)
    • Invoked to handle asynchronous requests sent by the client using GenServer.cast(pid, message).
    • Typically return {:noreply, new_state} which recursively loops with the new_state.
    • Can return {:stop, reason, new_state} which will cause the process to exit with reason.
    • Default use GenServer implementation returns {:stop, {:bad_cast, msg}, state} and stops the server. You should implement a handle_cast function for every message your server can handle.
  • handle_info(message, state)
    • Invoked to handle all other requests sent by the client that are not call or cast requests, such as a direct send call to the GenServer PID.
    • Default implementation logs the message and returns {:noreply, state}.
  • init(args)
    • Invoked when the server is started.
    • e.g. If you start a server like GenServer.start(__MODULE__, [], name: @name) then init will be called and passed the second argument of start, which is currently [].
    • The default implementation will return {:ok, args} where the args parameter is the state used to start the server.
    • If initialization fails (for whatever reason), you can reutrn {:stop, reason} which will cause GenServer.start to return {:error, reason} and cause the process to exit with reason.
  • terminate(reason, state)
    • Invoked when the server is about to terminate. Intended to allow you to do cleanup (like closing resources used by the process).
    • There may be situations where terminate is not called, so using Supervisor is more reliable.
    • Default implementation returns :ok, ignoring the arguments.
  • code_change(old_version, state, extra)
    • Feature of the Erlang Virtual Machine is hot code-swapping. When a new version of a module is loaded while the server is running, a migration of the old process state structure may be necessary. This callback is invoked to allow for state migration.
    • Typically you will not need to implement this callback. By default, this function will return the current state:
    def code_change(_old_version, state, _extra) do
        {:ok, state}
    end
    
#Call Timeouts

Invoking GenServer.call is synchronous and will wait for 5 seconds by default. This is overried by passing a timeout value (in milliseconds) as the third argument to call.

# wait 2 seconds instead of default 5
GenServer.call @name, :recent_pledges, 2000
#Debugging and Tracing

Erlang has a sys odule that can be used to inspect the current state of a running GenServer process.

iex> {:ok, pid} = Servy.PledgeServer.start()

# get the current state of this process
iex> :sys.get_state(pid)
%Servy.PledgeServer.State{cache_size: 3, pledges: [{"wilma", 15}, {"fred", 25}]}

# Get the full status of a process
iex> :sys.get_status(pid)
{:status, #PID<0.212.0>, {:module, :gen_server},
 [
   [
     "$initial_call": {Servy.PledgeServer, :init, 1},
     "$ancestors": [#PID<0.210.0>, #PID<0.83.0>]
   ],
   :running,
   #PID<0.212.0>,
   [],
   [
     header: 'Status for generic server pledge_server',
     data: [
       {'Status', :running},
       {'Parent', #PID<0.212.0>},
       {'Logged events', []}
     ],
     data: [
       {'State',
        %Servy.PledgeServer.State{
          cache_size: 3,
          pledges: [{"Wilma", 15}, {"Fred", 25}]
        }}
     ]
   ]
 ]}

# turn on tracing for the server process
iex> :sys.trace(pid, true)
:ok

iex> Servy.PledgeServer.create_pledge("moe", 20)

Traces can look like the following:

*DBG* pledge_server got call {create_pledge,<<"moe">>,20} from <0.152.0>
*DBG* pledge_server sent <<"pledge-275">> to <0.152.0>, new state #{'__struct__'=>'Elixir.Servy.PledgeServer.State',cache_size=>3,pledges=>[{<<109,111,101>>,20},{<<108,97,114,114,121>>,10},{<<119,105,108,109,97>>,15}]}

#Another GenServer

Defined a new GenServer Servy.SensorServer that does long polling to periodically fetch images from a mock external API and keeps the results in a cache. The handle_info function is used to trigger off a :refresh event every 5 seconds. The refresh event will fetch from the mock external API and store the results into a cache.

Be sure to add a handle_info function that is generic after adding the new :refresh handler, in order to make your server robust to crashes (a new message of :boom will throw a FunctionClauseError because nothing will match with :boom otherwise).

#Linking Processes

When an Elixir process terminates, it will notify its linked processes by sending it an exit signal. If the process terminates normally, the exit signal reason is the atom :normal. Because the process exits normally, the linked process does not terminate.

If the process has an abnormal termination, the exit reason will be anything other than :normal. By default, the exit signal indicates that the process terminated abnormally and the linked process will terminate with the same reason unless the linked process is trapping exits.

Linked processes are always bidirectional.

#Linking Tasks

Referring back to Task.async for spawning functions and Task.await for waiting for the results:

iex> pid = Task.async(fn -> Servy.Tracker.get_location("bigfoot") end)

iex> Task.await(pid)
%{lat: "29.0469 N", lng: "98.8667 W"}

The spawned process is automatically linked to the calling process. If the spawned task process crashes, then the process that calls Task.async will also crash.

iex> pid = Task.async(fn -> raise "Kaboom!" end)

** (EXIT from #PID<0.368.0>) evaluator process exited with reason: an exception was raised:
    ** (RuntimeError) Kaboom!

#Fault Recovery with OTP Supervisors

Supervisors are special processes that that are hierarchical parents of GenServer processes and other supervisors. If a GenServer terminates, the Supervisor can restart the GenServer.

#Restart Strategies

One of the options that can be passed into Supervisor.init is strategy:

  • :one_for_one: if a child process terminates, only that process is restarted.
  • :one_for_all: if a child process terminates, all children processes are restarted
  • :rest_for_one: if a child process terminates, the rest of the child processes (children listed after the terminated child) that were started after it are terminated. All terminated children are then restarted.
  • :simple_one_for_one: restricted to when a supervisor has one child specification. Used for dynamically spawning child procsses that are then attached to the supervisor (ie a pool of similar worker processes).

Additional options are:

  • :max_restarts: indicates max number of restarts allowed within a given time frame (default is 3 restarts)
  • :max_seconds: indicates the time frame for :max_restarts (default is 5 seconds)
opts = [strategy: :one_for_one, max_restarts: 5, max_seconds: 10]

# These children will be supervised with the one_for_one strategy, allowing 5 restarts within 10 seconds before error occurs.
Supervisor.init(children, opts)

#Final OTP Application

An application is a first class citizen in Elixir. See Application for more details.

Application environment configuration can be specified directly in the mix.exs file, or by config/config.exs files.

  • The config directory files are no longer generated by default
  • Configuration settings set in config directory are restricted to this project, if the project is a dependency of another application then the contents of config/config.exs are never loaded.

Within the application callback module, the start/2 callback is what is invoked when calling iex -S mix or mix run and mix run --no-halt.