Elixir Introduction with Servy
- Servy
- High-Level Transformations
- Simple Pattern Matching
- Immutable Data
- Function Clauses
- Additional Pattern Matching
- Advanced Pattern Matching
- Reading a File, Case Operator
- Module Attributes
- Code Reorganization
- Struct Usage
- Matching Heads and Tails
- Recursion
- Slicing and Dicing with Enum
- Comprehensions
- Test Automation
- Rendering JSON
- Web Server Sockets
- Concurrent, Isolated Processes
- Sending and Receiving Messages
- Asynchronous Tasks
- Stateful Server Processes
- Refactoring Towards GenServer
- OTP GenServer
- Another GenServer
- Linking Processes
- Fault Recovery with OTP Supervisors
- Final OTP Application
Servy
This post is a summary of the excellent introductory course to Elixir from The Pragmatic Studio.
Please also refer to the source code for Servy, where all of this is documented again in the README file.
High-Level Transformations
|
Simple Pattern Matching
|
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 asdef 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
|
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
|
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
|
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 ConsolidationJason.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. Erlanggen_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
|
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
|
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 queriesAgent
: For a simple process that holds stateGenServer
: 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
- If you need to serialize access to a shared resource or service,
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 thereply
to the client and recursively loops with thenew_state
. - Can return
{:stop, reason, new_state}
which will exit the process withreason
. - Default
use GenServer
implementation returns{:stop, {:bad_call, msg}, state}
and stops the server. You should implement ahandle_call
function clause for every message your server can handle.
- Invoked to handle synchronous requests sent by the client using
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 thenew_state
. - Can return
{:stop, reason, new_state}
which will cause the process to exit withreason
. - Default
use GenServer
implementation returns{:stop, {:bad_cast, msg}, state}
and stops the server. You should implement ahandle_cast
function for every message your server can handle.
- Invoked to handle asynchronous requests sent by the client using
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}
.
- Invoked to handle all other requests sent by the client that are not call or cast requests, such as a direct
init(args)
- Invoked when the server is started.
- e.g. If you start a server like
GenServer.start(__MODULE__, [], name: @name)
theninit
will be called and passed the second argument ofstart
, 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 causeGenServer.start
to return{:error, reason}
and cause the process to exit withreason
.
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)
|
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 ofconfig/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
.