-
a
:public
table can only be accessed “globally” if it is named - otherwise any process accessing it has to somehow have to get ahold of the table ID. So it isn’t uncommon for a supervisor to create a public table for one of its child processes and hand the child process the table id. Then each time the process is restarted it takes over the existing table. -
with protected tables you can play the
heir
-give_away
game. A simple owner process transfers ownership to a requesting process but gets it back when that process dies.
Demo script:
# file: lib/demo.ex
#
defmodule Demo do
def run do
cycle(3)
end
defp cycle(n) when n < 1 do
:ok
end
defp cycle(n) do
increment(3)
pid = kill_and_wait()
if(pid, do: cycle(n - 1), else: :error)
end
defp increment(n) when n < 1 do
:ok
end
defp increment(n) do
increment()
increment(n - 1)
end
defp increment do
{:count, value} = DontLose.Counter.increment()
IO.puts("#{value}")
end
defp kill_and_wait() do
name = DontLose.Counter
pid = Process.whereis(name)
ref = Process.monitor(pid)
Process.exit(pid, :kill)
receive do
{:DOWN, ^ref, :process, ^pid, :killed} ->
:ok
end
wait(name, 10, 10)
end
defp wait(_, _, left) when left < 1 do
nil
end
defp wait(name, timeout, left) do
case Process.whereis(name) do
nil ->
Process.sleep(timeout)
wait(name, timeout, left - 1)
pid ->
pid
end
end
end
Demo session:
Interactive Elixir (1.8.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Demo.run()
1
2
3
4
5
6
7
8
9
:ok
iex(2)>
Public table:
# file: lib/dont_lose/application.ex
#
defmodule DontLose.Application do
use Application
def start(_type, _args) do
DontLose.Supervisor.start_link([])
end
end
# file: lib/dont_lose/supervisor.ex
#
defmodule DontLose.Supervisor do
use Supervisor
def start_link(init_arg) do
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
end
@impl true
def init(_init_arg) do
table = :ets.new(:counter_storage, [:set, :public])
:ets.insert(table, {:counter, 0})
children = [
{DontLose.Counter, table}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
# file: lib/dont_lose/counter.ex
#
defmodule DontLose.Counter do
use GenServer
def start_link(table) do
GenServer.start_link(__MODULE__, table, name: __MODULE__)
end
@impl true
def init(table) do
# retrieve value from backup
[{:counter, count}] = :ets.lookup(table, :counter)
{:ok, {table, count}}
end
@impl true
def handle_call(:increment, _from, {table, count}) do
new_count = count + 1
# backup value
:ets.insert(table, {:counter, new_count})
{:reply, {:count, new_count}, {table, new_count}}
end
# --- API
def increment,
do: GenServer.call(__MODULE__, :increment)
end
Protected “heir” table:
# file: lib/dont_lose/application.ex
#
defmodule DontLose.Application do
use Application
def start(_type, _args) do
supervisor = DontLose.Supervisor
children = [
{DontLose.Keeper, nil},
{DontLose.Counter, supervisor}
]
opts = [strategy: :rest_for_one, name: supervisor]
Supervisor.start_link(children, opts)
end
end
# file: lib/dont_lose/keeper.ex
#
defmodule DontLose.Keeper do
use GenServer
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
@impl true
def init(_args) do
# create and initialize table
table = :ets.new(:counter_storage, [:set, :protected, {:heir, self(), :counter_heir}])
:ets.insert(table, {:counter, 0})
{:ok, table}
end
@impl true
def handle_call({:request_table, pid}, _from, table) when not is_nil(table) do
:ets.give_away(table, pid, :counter_transfer)
{:reply, :ok, nil}
end
@impl true
def handle_info({:"ETS-TRANSFER", table, _pid, :counter_heir}, _state) do
{:noreply, table}
end
# --- API
def request_table(keeper, pid),
do: GenServer.call(keeper, {:request_table, pid})
end
# file: lib/dont_lose/counter.ex
#
defmodule DontLose.Counter do
use GenServer
alias DontLose.Keeper
def start_link(sup) do
GenServer.start_link(__MODULE__, sup, name: __MODULE__)
end
@impl true
def init(sup) do
state = []
{:ok, state, {:continue, sup}}
end
@impl true
def handle_continue(sup, _state) do
children = Supervisor.which_children(sup)
case find_keeper(children) do
nil ->
{:stop, :no_keeper, nil}
pid ->
# request table from keeper
:ok = Keeper.request_table(pid, self())
{:noreply, nil}
end
end
@impl true
def handle_call(:increment, _from, {table, count}) do
new_count = count + 1
# backup value
:ets.insert(table, {:counter, new_count})
{:reply, {:count, new_count}, {table, new_count}}
end
@impl true
def handle_info({:"ETS-TRANSFER", table, _pid, :counter_transfer}, _state) do
# retrieve current count
[{:counter, count}] = :ets.lookup(table, :counter)
{:noreply, {table, count}}
end
defp find_keeper([]) do
nil
end
defp find_keeper([{DontLose.Keeper, pid, :worker, _modules} | _tail]) do
pid
end
defp find_keeper([_ | tail]) do
find_keeper(tail)
end
# --- API
def increment,
do: GenServer.call(__MODULE__, :increment)
end
The above is for simple demonstration only as not all edge cases are covered.