Andre Henriques
ee9f5e1187
All checks were successful
continuous-integration/drone/push Build is passing
709 lines
18 KiB
Elixir
709 lines
18 KiB
Elixir
defmodule Utils do
|
|
|
|
@min_print_level 3
|
|
|
|
def safecast(p, m) when p == nil, do: IO.puts('Trying to safecast #{m} with p as nil')
|
|
def safecast(p, m) when is_pid(p), do: send(p, m)
|
|
def safecast(p, m) do
|
|
case :global.whereis_name(p) do
|
|
pid when is_pid(pid) -> send(pid, m)
|
|
:undefined -> :ok
|
|
end
|
|
end
|
|
|
|
def alter_name(name, part), do: :"#{name}#{part}"
|
|
def beb_broadcast(m, dest), do: for(p <- dest, do: safecast(p, m))
|
|
|
|
def register_name(name, pid, link \\ true) do
|
|
case :global.re_register_name(name, pid) do
|
|
:yes ->
|
|
# Note this is running on the parent so we are linking the parent to the rb
|
|
# so that when we close the parent the rb also dies
|
|
if link do
|
|
Process.link(pid)
|
|
end
|
|
pid
|
|
|
|
:no ->
|
|
Process.exit(pid, :kill)
|
|
:error
|
|
end
|
|
end
|
|
|
|
defmacro create_log(level) do
|
|
quote do
|
|
def log(msg) do
|
|
Utils._log(msg, unquote(level))
|
|
end
|
|
end
|
|
end
|
|
|
|
def _log(msg, level) do
|
|
if (@min_print_level <= level) do
|
|
IO.puts(msg)
|
|
end
|
|
end
|
|
|
|
defmacro or_state(val, do: expr) do
|
|
quote do
|
|
case unquote(val) do
|
|
true -> unquote(expr)
|
|
_ -> var!(state)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmacro runfn(do: expr) do
|
|
quote do
|
|
def run(s) do
|
|
var!(state) = s
|
|
run(receive do
|
|
unquote(expr)
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
#
|
|
# Possible actions
|
|
# :kill_before_decision
|
|
# :increase_ballot_number - this makes it so that it does not propose but jump simply increases the number of the current ballot
|
|
# this is usefull when forcing a nack
|
|
#
|
|
defmodule Paxos do
|
|
require Utils
|
|
import Utils
|
|
|
|
create_log 0
|
|
|
|
defmacro set_instmap(do: expr) do
|
|
quote do
|
|
var!(map) = var!(state).instmap[var!(inst)]
|
|
new_instmap = Map.put(var!(state).instmap, var!(inst), unquote(expr))
|
|
var!(state) = %{var!(state) | instmap: new_instmap }
|
|
end
|
|
end
|
|
|
|
# Starts the Paxos replica with a specific name and some processes
|
|
def start(name, processes, link \\ false) do
|
|
log("Starting paxos for #{name}")
|
|
|
|
pid = spawn(Paxos, :init, [name, processes])
|
|
register_name(name, pid, link)
|
|
end
|
|
|
|
# Init event must be the first
|
|
# one after the component is created
|
|
def init(name, processes) do
|
|
EventualLeaderElector.start(name, processes)
|
|
EagerReliableBroadcast.start(name, processes)
|
|
|
|
state = %{
|
|
name: name,
|
|
processes: processes,
|
|
leader: nil,
|
|
instmap: %{},
|
|
decided: %{}
|
|
}
|
|
|
|
run(state)
|
|
end
|
|
|
|
# Guarantees that a specific state exists
|
|
def has_or_create(state, inst, value \\ nil, pid_to_inform \\ nil, action \\ nil) do
|
|
or_state state.instmap[inst] == nil do
|
|
instmap =
|
|
Map.put(state.instmap, inst, %{
|
|
value: value,
|
|
other_value: nil,
|
|
ballot: Ballot.init(state.name, 0),
|
|
aborted: false,
|
|
ballot_value: nil,
|
|
prepared_values: [],
|
|
accepted: 0,
|
|
accepted_ballot: nil,
|
|
accepted_value: nil,
|
|
pid_to_inform: pid_to_inform,
|
|
has_sent_accept: false,
|
|
action: action,
|
|
has_sent_prepare: false,
|
|
})
|
|
|
|
%{state | instmap: instmap}
|
|
end
|
|
end
|
|
|
|
def has_finished(state, inst, ignore_aborted \\ false) do
|
|
cond do
|
|
Map.has_key?(state.decided, inst) -> true
|
|
ignore_aborted -> false
|
|
Map.has_key?(state.instmap, inst) -> state.instmap[inst].aborted
|
|
true -> false
|
|
end
|
|
end
|
|
|
|
runfn do
|
|
{:ele_trust, proc} ->
|
|
log("#{state.name} - #{proc} is leader")
|
|
|
|
Enum.reduce(Map.keys(state.instmap), %{state | leader: proc}, fn inst, st ->
|
|
prepare(st, inst)
|
|
end)
|
|
|
|
{:propose, inst, value, pid_to_inform, action} ->
|
|
log("#{state.name} - Propose #{inspect(inst)} with action #{inspect(action)}")
|
|
|
|
cond do
|
|
has_finished(state, inst, true) ->
|
|
log("#{state.name} - Has already decided for #{inspect(inst)} sending #{inspect(state.decided[inst])}")
|
|
send(pid_to_inform, {:decision, inst, state.decided[inst]})
|
|
state
|
|
|
|
action == :increase_ballot_number ->
|
|
log("#{state.name} - Got request to increase ballot number for inst #{inst}")
|
|
state = has_or_create(state, inst)
|
|
|
|
set_instmap do
|
|
%{map| ballot: Ballot.inc(map.ballot)}
|
|
end
|
|
|
|
not Map.has_key?(state.instmap, inst) ->
|
|
EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value})
|
|
state = has_or_create(state, inst, value, pid_to_inform, action)
|
|
prepare(state, inst)
|
|
|
|
state.instmap[inst].value == nil ->
|
|
EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value})
|
|
|
|
set_instmap do
|
|
%{ map |
|
|
value: value,
|
|
pid_to_inform: pid_to_inform,
|
|
action: action,
|
|
}
|
|
end
|
|
|
|
prepare(state, inst)
|
|
|
|
true ->
|
|
EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value})
|
|
prepare(state, inst)
|
|
end
|
|
|
|
{:rb_deliver, proc, {:other_propose, inst, value}} ->
|
|
cond do
|
|
has_finished(state, inst, true) ->
|
|
EagerReliableBroadcast.broadcast(
|
|
state.name,
|
|
{:decide, inst, state.decided[inst]}
|
|
)
|
|
state
|
|
|
|
true ->
|
|
state = has_or_create(state, inst)
|
|
set_instmap do
|
|
%{map | other_value: value}
|
|
end
|
|
prepare(state, inst)
|
|
end
|
|
|
|
{:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
|
|
log("#{state.name} - prepare from #{proc}")
|
|
|
|
cond do
|
|
has_finished(state, inst) ->
|
|
state
|
|
|
|
not Map.has_key?(state.instmap, inst) ->
|
|
state = has_or_create(state, inst)
|
|
|
|
safecast(proc, {:prepared, inst, ballot, state.instmap[inst].accepted_ballot, state.instmap[inst].accepted_value});
|
|
|
|
set_instmap do
|
|
%{ map | ballot: ballot }
|
|
end
|
|
|
|
Ballot.compare(ballot, &>/2, state.instmap[inst].ballot) ->
|
|
safecast(proc,
|
|
{:prepared, inst, ballot, state.instmap[inst].accepted_ballot,
|
|
state.instmap[inst].accepted_value}
|
|
)
|
|
|
|
set_instmap do
|
|
%{ map | ballot: ballot }
|
|
end
|
|
|
|
true ->
|
|
safecast(proc, {:nack, inst, ballot})
|
|
state
|
|
end
|
|
|
|
{:nack, inst, ballot} ->
|
|
log("#{state.name} - nack #{inspect(inst)} #{inspect(ballot)}")
|
|
|
|
cond do
|
|
has_finished(state, inst) ->
|
|
state
|
|
|
|
state.leader == state.name and state.instmap[inst].ballot == ballot ->
|
|
if Map.has_key?(state.instmap, inst) and state.instmap[inst].pid_to_inform != nil do
|
|
send(state.instmap[inst].pid_to_inform, {:abort, inst})
|
|
end
|
|
|
|
EagerReliableBroadcast.broadcast(state.name, {:abort, inst, ballot})
|
|
|
|
set_instmap do
|
|
%{ map | has_sent_accept: false,
|
|
has_sent_prepare: false,
|
|
ballot: Ballot.inc(map.ballot),
|
|
aborted: true,
|
|
}
|
|
end
|
|
|
|
true ->
|
|
state
|
|
end
|
|
|
|
{:rb_deliver, _proc, {:abort, inst, ballot}} ->
|
|
cond do
|
|
has_finished(state, inst) ->
|
|
state
|
|
|
|
true ->
|
|
log("#{state.name} - got information to send abort")
|
|
|
|
if Map.has_key?(state.instmap, inst) and state.instmap[inst].pid_to_inform != nil do
|
|
send(state.instmap[inst].pid_to_inform, {:abort, inst})
|
|
end
|
|
|
|
state
|
|
end
|
|
|
|
{:prepared, inst, ballot, accepted_ballot, accepted_value} ->
|
|
log(
|
|
"#{state.name} - prepared #{inspect(inst)} #{inspect(ballot)} #{inspect(accepted_ballot)} #{inspect(accepted_value)}"
|
|
)
|
|
|
|
cond do
|
|
has_finished(state, inst) ->
|
|
state
|
|
|
|
ballot == state.instmap[inst].ballot ->
|
|
set_instmap do
|
|
%{ map
|
|
| prepared_values: map.prepared_values ++ [{accepted_ballot, accepted_value}]
|
|
}
|
|
end
|
|
prepared(state, inst)
|
|
|
|
Ballot.compare(ballot, &>/2, state.instmap[inst].ballot) ->
|
|
log("#{state.name} - Probably recieved this before preare came to self sending with delay")
|
|
Process.send_after(self(), {:prepared, inst, ballot, accepted_ballot, accepted_value}, 100)
|
|
state
|
|
|
|
true ->
|
|
state
|
|
end
|
|
|
|
{:rb_deliver, proc, {:accept, inst, ballot, value}} ->
|
|
cond do
|
|
has_finished(state, inst) ->
|
|
state
|
|
|
|
true ->
|
|
state = has_or_create(state, inst)
|
|
|
|
if Ballot.compare(ballot, &>=/2, state.instmap[inst].ballot) do
|
|
log("#{state.name} - accept #{inspect(inst)} #{inspect(ballot)} #{inspect(value)}")
|
|
|
|
safecast(proc, {:accepted, inst, ballot})
|
|
|
|
set_instmap do
|
|
%{ map
|
|
| ballot: ballot,
|
|
accepted_value: value,
|
|
accepted_ballot: ballot
|
|
}
|
|
end
|
|
else
|
|
log("#{state.name} -> #{proc} nack")
|
|
safecast(proc, {:nack, inst, ballot})
|
|
state
|
|
end
|
|
end
|
|
|
|
{:accepted, inst, ballot} ->
|
|
log("#{state.name} - accepted #{inspect(inst)} #{inspect(ballot)}")
|
|
|
|
cond do
|
|
has_finished(state, inst) ->
|
|
state
|
|
|
|
state.leader == state.name and state.instmap[inst].ballot == ballot ->
|
|
set_instmap do
|
|
%{ map | accepted: map.accepted + 1 }
|
|
end
|
|
|
|
accepted( state, inst)
|
|
|
|
true ->
|
|
state
|
|
end
|
|
|
|
{:get_value, inst, pid_to_inform} ->
|
|
# log("#{state.name} get_value")
|
|
if has_finished(state, inst, true) do
|
|
safecast(pid_to_inform, {:get_value_res, inst, state.decided[inst]})
|
|
end
|
|
state
|
|
|
|
{:rb_deliver, _, {:decide, inst, value}} ->
|
|
log("#{state.name} - decided #{inspect(inst)} #{inspect(value)}")
|
|
|
|
or_state not has_finished(state, inst) do
|
|
if Map.has_key?(state.instmap, inst) != nil and
|
|
state.instmap[inst].pid_to_inform != nil do
|
|
safecast(state.instmap[inst].pid_to_inform, {:decision, inst, value})
|
|
end
|
|
|
|
%{ state |
|
|
decided: Map.put(state.decided, inst, value),
|
|
instmap: Map.delete(state.instmap, inst)
|
|
}
|
|
end
|
|
end
|
|
|
|
#
|
|
# Puts process in the preapre state
|
|
#
|
|
|
|
def prepare(state, _) when state.leader != state.name, do: state
|
|
|
|
def prepare(state, inst) do
|
|
cond do
|
|
state.instmap[inst] == nil ->
|
|
state
|
|
|
|
state.instmap[inst].value == nil and state.instmap[inst].other_value == nil ->
|
|
state
|
|
|
|
state.instmap[inst] != nil and state.instmap[inst].has_sent_prepare ->
|
|
state
|
|
|
|
state.instmap[inst] != nil and state.instmap[inst].has_sent_accept ->
|
|
state
|
|
|
|
true ->
|
|
ballot = Ballot.inc(state.instmap[inst].ballot)
|
|
log("#{state.name} - sending all prepare #{inst} #{inspect(ballot)}")
|
|
EagerReliableBroadcast.broadcast(state.name, {:prepare, state.name, inst, ballot})
|
|
|
|
set_instmap do
|
|
%{ map |
|
|
prepared_values: [],
|
|
accepted: 0,
|
|
aborted: false,
|
|
ballot_value: nil,
|
|
has_sent_prepare: true,
|
|
has_sent_accept: false
|
|
}
|
|
end
|
|
end
|
|
end
|
|
|
|
#
|
|
# Process the prepared responses
|
|
#
|
|
def prepared(state, _) when state.leader != state.name, do: state
|
|
|
|
def prepared(state, inst) do
|
|
if length(state.instmap[inst].prepared_values) >= floor(length(state.processes) / 2) + 1 and
|
|
not state.instmap[inst].has_sent_accept do
|
|
{_, a_val} =
|
|
Enum.reduce(state.instmap[inst].prepared_values, {Ballot.init(state.name, 0), nil}, fn {bal, val},
|
|
{acc_bal, acc_val} ->
|
|
cond do
|
|
val == nil ->
|
|
{acc_bal, acc_val}
|
|
Ballot.compare(acc_bal, &>/2, bal) ->
|
|
{acc_bal, acc_val}
|
|
true ->
|
|
{bal, val}
|
|
end
|
|
end)
|
|
|
|
a_val =
|
|
if a_val == nil do
|
|
if state.instmap[inst].value == nil do
|
|
state.instmap[inst].other_value
|
|
else
|
|
state.instmap[inst].value
|
|
end
|
|
else
|
|
a_val
|
|
end
|
|
|
|
EagerReliableBroadcast.broadcast(
|
|
state.name,
|
|
{:accept, inst, state.instmap[inst].ballot, a_val}
|
|
)
|
|
|
|
set_instmap do
|
|
%{ map |
|
|
ballot_value: a_val,
|
|
has_sent_accept: true
|
|
}
|
|
end
|
|
else
|
|
state
|
|
end
|
|
end
|
|
|
|
#
|
|
# Process the accepted responses
|
|
#
|
|
def accepted(state, _) when state.leader != state.name, do: state
|
|
|
|
def accepted(state, inst) do
|
|
if state.instmap[inst].accepted >= floor(length(state.processes) / 2) + 1 do
|
|
value = state.instmap[inst].ballot_value
|
|
|
|
if state.instmap[inst].action == :kill_before_decision do
|
|
log("#{state.name} - Leader has action to die before decision #{inspect({:decide, inst, value})}")
|
|
Process.exit(self(), :kill)
|
|
end
|
|
|
|
EagerReliableBroadcast.broadcast(
|
|
state.name,
|
|
{:decide, inst, value}
|
|
)
|
|
|
|
if state.instmap[inst].pid_to_inform != nil do
|
|
send(state.instmap[inst].pid_to_inform, {:decision, inst, value})
|
|
end
|
|
|
|
%{
|
|
state
|
|
| decided: Map.put(state.decided, inst, value),
|
|
instmap: Map.delete(state.instmap, inst)
|
|
}
|
|
else
|
|
state
|
|
end
|
|
end
|
|
|
|
def propose(pid, inst, value, t, action \\ nil) do
|
|
send(pid, {:propose, inst, value, self(), action})
|
|
|
|
propose_loop({inst, t})
|
|
end
|
|
|
|
def propose_loop(input) do
|
|
{_, t} = input
|
|
receive do
|
|
{:timeout, inst} ->
|
|
check_and_apply({:timeout}, inst, input, &propose_loop/1)
|
|
|
|
{:abort, inst} ->
|
|
check_and_apply({:abort}, inst, input, &propose_loop/1)
|
|
|
|
{:decision, inst, d} ->
|
|
check_and_apply({:decision, d}, inst, input, &propose_loop/1)
|
|
|
|
x ->
|
|
Process.send_after(self(), x, 500)
|
|
propose_loop(input)
|
|
after
|
|
t -> {:timeout}
|
|
end
|
|
end
|
|
|
|
def get_decision(pid, inst, t) do
|
|
send(pid, {:get_value, inst, self()})
|
|
get_decision_loop({inst, t})
|
|
end
|
|
|
|
def get_decision_loop(input) do
|
|
{_, t} = input
|
|
receive do
|
|
{:get_value_res, inst, v} ->
|
|
check_and_apply(v, inst, input, &get_decision_loop/1)
|
|
|
|
x ->
|
|
Process.send_after(self(), x, 500)
|
|
get_decision_loop(input)
|
|
after
|
|
t -> nil
|
|
end
|
|
end
|
|
|
|
def check_and_apply(v, inst, input, fun) do
|
|
{inInst, _} = input
|
|
if inst == inInst do
|
|
v
|
|
else
|
|
fun.(input)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Ballot do
|
|
def init(name, number \\ 0), do: {name, number}
|
|
|
|
def inc(b, name \\ nil) do
|
|
{old_name, number} = b
|
|
|
|
{
|
|
if name == nil do old_name else name end,
|
|
number + 1
|
|
}
|
|
end
|
|
|
|
defp lexicographical_compare(a, b) do
|
|
cond do
|
|
a == b -> 0
|
|
a > b -> 1
|
|
true -> -1
|
|
end
|
|
end
|
|
|
|
|
|
defp diff({name1, number1}, {name2, number2}) do
|
|
diff = number1 - number2
|
|
if diff == 0 do
|
|
lexicographical_compare(name1, name2)
|
|
else
|
|
diff
|
|
end
|
|
end
|
|
|
|
def compare(b1, operator, b2), do: operator.(diff(b1, b2), 0)
|
|
end
|
|
|
|
defmodule EagerReliableBroadcast do
|
|
require Utils
|
|
import Utils
|
|
|
|
def get_non_rb_name(name) do
|
|
String.to_atom(String.replace(Atom.to_string(name), "_rb", ""))
|
|
end
|
|
|
|
def start(name, processes) do
|
|
pid = spawn(EagerReliableBroadcast, :init, [name, processes])
|
|
register_name(alter_name(name, "_rb"), pid)
|
|
end
|
|
|
|
# Init event must be the first
|
|
# one after the component is created
|
|
def init(parent, processes) do
|
|
state = %{
|
|
name: alter_name(parent, "_rb"),
|
|
parent: parent,
|
|
processes: Enum.map(processes, fn name -> alter_name(name, "_rb") end),
|
|
# Use this data structure to remember IDs of the delivered messages
|
|
delivered: %{},
|
|
# Use this variable to remember the last sequence number used to identify a message
|
|
seq_no: 0
|
|
}
|
|
|
|
run(state)
|
|
end
|
|
|
|
runfn do
|
|
# Handle the broadcast request event
|
|
{:broadcast, m} ->
|
|
data_msg = {:data, state.name, state.seq_no, m}
|
|
beb_broadcast(data_msg, state.processes)
|
|
%{state | seq_no: state.seq_no + 1}
|
|
|
|
{:data, proc, seq_no, m} ->
|
|
if not Map.has_key?(state.delivered, {proc, seq_no, m}) do
|
|
data_msg = {:data, proc, seq_no, m}
|
|
beb_broadcast(data_msg, state.processes)
|
|
|
|
safecast(state.parent, {:rb_deliver, get_non_rb_name(proc), m})
|
|
%{state | delivered: Map.put(state.delivered, {proc, seq_no, m}, 1)}
|
|
else
|
|
val = Map.get(state.delivered, {proc, seq_no, m})
|
|
|
|
if val < Enum.count(state.processes) do
|
|
%{state | delivered: Map.put(state.delivered, {proc, seq_no, m}, val + 1)}
|
|
else
|
|
%{state | delivered: Map.delete(state.delivered, {proc, seq_no, m})}
|
|
end
|
|
end
|
|
end
|
|
|
|
#############
|
|
# Interface #
|
|
#############
|
|
def broadcast(name, m), do: safecast(alter_name(name, "_rb"), {:broadcast, m})
|
|
end
|
|
|
|
#
|
|
# Emits {:ele_trust, proc }
|
|
#
|
|
|
|
defmodule EventualLeaderElector do
|
|
require Utils
|
|
import Utils
|
|
|
|
def start(name, processes) do
|
|
new_name = alter_name(name, "_ele")
|
|
pid = spawn(EventualLeaderElector, :init, [new_name, name, processes])
|
|
|
|
register_name(new_name, pid)
|
|
end
|
|
|
|
# Init event must be the first
|
|
# one after the component is created
|
|
def init(name, parent, processes) do
|
|
processes = Enum.map(processes, fn name -> alter_name(name, "_ele") end)
|
|
|
|
state = %{
|
|
name: name,
|
|
parent: parent,
|
|
processes: processes,
|
|
timeout: 1000,
|
|
heard_back: MapSet.new(),
|
|
seq: 0,
|
|
last_trust: nil
|
|
}
|
|
|
|
run(request_heartbeats(state))
|
|
end
|
|
|
|
def request_heartbeats(state) do
|
|
state = %{state | heard_back: MapSet.new(), seq: state.seq + 1}
|
|
beb_broadcast({:heartbeat_request, state.name, state.seq}, state.processes)
|
|
|
|
Process.send_after(self(), {:timeout}, state.timeout)
|
|
state
|
|
end
|
|
|
|
runfn do
|
|
{:heartbeat_request, name, seq} ->
|
|
safecast(name, {:heartbeat, state.parent, seq})
|
|
state
|
|
|
|
{:heartbeat, name, seq} ->
|
|
or_state seq == state.seq do
|
|
%{state | heard_back: MapSet.put(state.heard_back, name)}
|
|
end
|
|
|
|
{:timeout} ->
|
|
state = or_state MapSet.size(state.heard_back) >= floor(length(state.processes)/2) + 1 do
|
|
to_trust = Enum.at(Enum.sort(MapSet.to_list(state.heard_back)), 0)
|
|
|
|
or_state state.last_trust != to_trust do
|
|
safecast(state.parent, {:ele_trust, to_trust})
|
|
%{state | last_trust: to_trust}
|
|
end
|
|
end
|
|
|
|
request_heartbeats(state)
|
|
end
|
|
end
|