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
$