The free sample of Designing for Scalability with Erlang/OTP: Implement Robust, Fault-Tolerant Systems on Google play has the complete Chapter 3. Behaviors. The code is in Erlang - for the equivalent Elixir code see below. Points of note:
- Here the
Frequency
callback module injects itself into the Server
behaviour module through its start/0
function.
- The
Server
behaviour module loop/2
function keeps the callback module that the process was initialized with as part of the loop state. The arguments to the loop are 1.) the callback module
, 2.) the state
which is unique to this particular process and transformed by the callback module.
The callbacks implemented by the Frequency
callback module are:
-
init/1
(used in Server.init/2
)
-
handle/2
(used in Server.loop/2
)
-
terminate/1
(used in Server.loop/2
)
Frequency
before behaviour separation
#
# file: freq.exs - based on:
# "Designing for Scalability with Erlang/OTP Implement Robust, Fault-Tolerant Systems"
# by Francesco Cesarini and Steve Vinoski (O’Reilly).
# Copyright 2016 Francesco Cesarini and Stephen Vinoski, 978-1-449-32073-7.
#
# https://github.com/francescoc/scalabilitywitherlangotp/blob/master/ch3/frequency.erl
#
defmodule Frequency do
def start do
__MODULE__
|> Kernel.spawn(:init, [])
|> Process.register(__MODULE__)
end
def init do
# state: {[free frequencies], [allocated frequencies]}
frequencies = {get_frequencies(), []}
loop(frequencies)
end
## hard-coded
defp get_frequencies(),
do: Enum.to_list(10..15)
def stop,
do: call(:stop)
def allocate,
do: call(:allocate)
def deallocate(freq),
do: call({:deallocate, freq})
defp call(message) do
send(__MODULE__, {:request, self(), message})
receive do
{:reply, reply} ->
reply
end
end
defp reply(pid, reply),
do: send(pid, {:reply, reply})
defp loop(frequencies) do
receive do
{:request, pid, :allocate} ->
{new_frequencies, reply} = allocate(frequencies, pid)
reply(pid, reply)
loop(new_frequencies)
{:request, pid, {:deallocate, freq}} ->
new_frequencies = deallocate(frequencies, freq)
reply(pid, :ok)
loop(new_frequencies)
{:request, pid, :stop} ->
reply(pid, :ok)
end
end
## The internal helper functions used to allocate and
## deallocate frequencies.
defp allocate({[], _allocated} = frequencies, _pid),
do: {frequencies, {:error, :no_frequency}}
defp allocate({[freq | free], allocated}, pid),
do: {{free, [{freq, pid} | allocated]}, {:ok, freq}}
defp deallocate({free, allocated}, freq) do
new_allocated = List.keydelete(allocated, freq, 0)
{[freq | free], new_allocated}
end
end
freqs = []
IO.inspect(Frequency.start())
IO.puts("> started")
# allocate 10
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 11
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 12
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 13
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 14
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 15
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate error
IO.inspect(Frequency.allocate())
freqs = :lists.reverse(freqs)
# deallocate 10
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# allocate 10
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# deallocate 10
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 11
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 12
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 13
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 14
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 15
[freq | _] = freqs
IO.inspect(Frequency.deallocate(freq))
IO.puts("> stopping")
IO.inspect(Frequency.stop())
After split into the generic Server
behaviour module and the specific Frequency
callback module
#
# file: server.exs - based on:
# "Designing for Scalability with Erlang/OTP Implement Robust, Fault-Tolerant Systems"
# by Francesco Cesarini and Steve Vinoski (O’Reilly).
# Copyright 2016 Francesco Cesarini and Stephen Vinoski, 978-1-449-32073-7.
#
# https://github.com/francescoc/scalabilitywitherlangotp/blob/master/ch3/behavior/server.erl
#
# https://github.com/francescoc/scalabilitywitherlangotp/blob/master/ch3/behavior/frequency.erl
#
defmodule Server do
# behaviour module (generic)
def start(module, args) do
__MODULE__
|> Kernel.spawn(:init, [module, args])
|> Process.register(module)
end
def stop(module) do
send(module, {:stop, self()})
receive do
{:reply, reply} ->
reply
end
end
def init(module, args) do
state = module.init(args)
loop(module, state)
end
def call(name, msg) do
send(name, {:request, self(), msg})
receive do
{:reply, reply} ->
reply
end
end
defp reply(to, reply),
do: send(to, {:reply, reply})
## note how the callback "module" is part
## of the loop state
##
defp loop(module, state) do
receive do
{:request, from, msg} ->
{new_state, reply} = module.handle(msg, state)
reply(from, reply)
loop(module, new_state)
{:stop, from} ->
reply = module.terminate(state)
reply(from, reply)
end
end
end
defmodule Frequency do
# callback module (specific)
# Here the behaviour module (Server) is provided with
# the callback module (Frequency)
def start,
do: Server.start(__MODULE__, [])
# state: {[free frequencies], [allocated frequencies]}
def init(_args),
do: {get_frequencies(), []}
## hard-coded
defp get_frequencies(),
do: Enum.to_list(10..15)
def stop,
do: Server.stop(__MODULE__)
def allocate,
do: Server.call(__MODULE__, {:allocate, self()})
def deallocate(freq),
do: Server.call(__MODULE__, {:deallocate, freq})
def handle({:allocate, pid}, frequencies),
do: allocate(frequencies, pid)
def handle({:deallocate, freq}, frequencies),
do: {deallocate(frequencies, freq), :ok}
def terminate(_frequencies),
do: :ok
## The internal helper functions used to allocate and
## deallocate frequencies.
defp allocate({[], _allocated} = frequencies, _pid),
do: {frequencies, {:error, :no_frequency}}
defp allocate({[freq | free], allocated}, pid),
do: {{free, [{freq, pid} | allocated]}, {:ok, freq}}
defp deallocate({free, allocated}, freq) do
new_allocated = List.keydelete(allocated, freq, 0)
{[freq | free], new_allocated}
end
end
freqs = []
IO.inspect(Frequency.start())
IO.puts("> started")
# allocate 10
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 11
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 12
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 13
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 14
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate 15
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# allocate error
IO.inspect(Frequency.allocate())
freqs = :lists.reverse(freqs)
# deallocate 10
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# allocate 10
{:ok, freq} = IO.inspect(Frequency.allocate())
freqs = [freq | freqs]
# deallocate 10
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 11
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 12
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 13
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 14
[freq | freqs] = freqs
IO.inspect(Frequency.deallocate(freq))
# deallocate 15
[freq | _] = freqs
IO.inspect(Frequency.deallocate(freq))
IO.puts("> stopping")
IO.inspect(Frequency.stop())
$ elixir server.exs
true
> started
{:ok, 10}
{:ok, 11}
{:ok, 12}
{:ok, 13}
{:ok, 14}
{:ok, 15}
{:error, :no_frequency}
:ok
{:ok, 10}
:ok
:ok
:ok
:ok
:ok
:ok
> stopping
:ok
$