added some docs
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Andre Henriques 2024-01-19 18:23:08 +00:00
parent a22005c868
commit 8c674626f8
2 changed files with 291 additions and 129 deletions

View File

@ -1,11 +1,30 @@
#
# This files contains some modules nessesary for paxos to work
#
# Utils: General Utilities
# Paxos: Paxos logic
# Ballot: Contain the logic to handle the ballot numbers
# EventualLeaderElector: Leader elector code
# EagerReliableBroadcast: Handles eager reliable broadcast
#
defmodule Utils do
@moduledoc """
This module contains some helpful functions are used throughout
This module contains some helpful functions are used throughout all modules
"""
@doc """
Sets the min log level
0 - show all
1 - ignore paxos
2 - ignore server
"""
@min_print_level 3
@doc """
This function works similiary with unicast but it allows both for a pid or an atom
to be given as the first parameter
"""
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
@ -15,9 +34,19 @@ defmodule Utils do
end
end
@doc """
This function creates a new name for processes
"""
def alter_name(name, part), do: :"#{name}#{part}"
@doc """
This function sends a message to a list of porcesses
"""
def beb_broadcast(m, dest), do: for(p <- dest, do: safecast(p, m))
@doc """
This function register a pid in the global name space
"""
def register_name(name, pid, link \\ true) do
case :global.re_register_name(name, pid) do
:yes ->
@ -34,6 +63,9 @@ defmodule Utils do
end
end
@doc """
This macro defines a new log function for a specific log level
"""
defmacro create_log(level) do
quote do
def log(msg) do
@ -42,12 +74,25 @@ defmodule Utils do
end
end
@doc """
This function is used bu the create_log macro.
This function only prints logs if the level definied in the create_log macro
"""
def _log(msg, level) do
if (@min_print_level <= level) do
IO.puts(msg)
end
end
@doc """
This macro defines a an or_state function that acts like this:
if val do
expr
else
state
end
"""
defmacro or_state(val, do: expr) do
quote do
case unquote(val) do
@ -57,6 +102,9 @@ defmodule Utils do
end
end
@doc """
This macro defines a macro to simplify the process of creating run functions
"""
defmacro runfn(do: expr) do
quote do
def run(s) do
@ -69,20 +117,25 @@ defmodule Utils do
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
@moduledoc """
This module contains all the logic for Paxos
To start a paxos instance run Paxos.start
To propose a a value to paxos run Paxos.propose
To get a previous decision please run Paxos.get_decision
"""
require Utils
import Utils
create_log 0
defmacro set_instmap(do: expr) do
@doc """
This macro allows the state.instmap to be updated very easily
"""
defmacrop 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))
@ -90,7 +143,9 @@ defmodule Paxos do
end
end
# Starts the Paxos replica with a specific name and some processes
@doc """
Starts the Paxos replica with a specific name and some processes
"""
def start(name, processes, link \\ false) do
log("Starting paxos for #{name}")
@ -98,8 +153,9 @@ defmodule Paxos do
register_name(name, pid, link)
end
# Init event must be the first
# one after the component is created
@doc """
Inicializes the state starts the eager reliable broadcast, and starts the eventual leader eletector
"""
def init(name, processes) do
EventualLeaderElector.start(name, processes)
EagerReliableBroadcast.start(name, processes)
@ -115,8 +171,10 @@ defmodule Paxos do
run(state)
end
# Guarantees that a specific state exists
def has_or_create(state, inst, value \\ nil, pid_to_inform \\ nil, action \\ nil) do
@doc """
Guarantees that a specific state exists for a specific instance
"""
defp 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, %{
@ -139,7 +197,12 @@ defmodule Paxos do
end
end
def has_finished(state, inst, ignore_aborted \\ false) do
@doc """
Checks if an instance has finished or if it was aborted.
If the optional parameter ignore_aborted was set to true makes this function only check
if the the instance has finished
"""
defp has_finished(state, inst, ignore_aborted \\ false) do
cond do
Map.has_key?(state.decided, inst) -> true
ignore_aborted -> false
@ -148,7 +211,23 @@ defmodule Paxos do
end
end
@doc """
This is the run/recieve function
All the messages that are handled by this function are:
{:ele_trust, proc} ->
{:propose, inst, value, pid_to_inform, action} ->
{:rb_deliver, proc, {:other_propose, inst, value}} ->
{:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
{:nack, inst, ballot} ->
{:rb_deliver, _proc, {:abort, inst, ballot}} ->
{:prepared, inst, ballot, accepted_ballot, accepted_value} ->
{:rb_deliver, proc, {:accept, inst, ballot, value}} ->
{:accepted, inst, ballot} ->
{:get_value, inst, pid_to_inform} ->
{:rb_deliver, _, {:decide, inst, value}} ->
"""
runfn do
# Handles leader elector
{:ele_trust, proc} ->
log("#{state.name} - #{proc} is leader")
@ -156,6 +235,7 @@ defmodule Paxos do
prepare(st, inst)
end)
# Handles a proposal from the parent process
{:propose, inst, value, pid_to_inform, action} ->
log("#{state.name} - Propose #{inspect(inst)} with action #{inspect(action)}")
@ -196,6 +276,7 @@ defmodule Paxos do
prepare(state, inst)
end
# Handles the sharing of a proposal value to other processes
{:rb_deliver, proc, {:other_propose, inst, value}} ->
cond do
has_finished(state, inst, true) ->
@ -213,6 +294,7 @@ defmodule Paxos do
prepare(state, inst)
end
# Handles a prepare request from the leader
{:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
log("#{state.name} - prepare from #{proc}")
@ -244,6 +326,7 @@ defmodule Paxos do
state
end
# Handles a nack
{:nack, inst, ballot} ->
log("#{state.name} - nack #{inspect(inst)} #{inspect(ballot)}")
@ -270,6 +353,7 @@ defmodule Paxos do
state
end
# Handles an abort
{:rb_deliver, _proc, {:abort, inst, ballot}} ->
cond do
has_finished(state, inst) ->
@ -285,6 +369,8 @@ defmodule Paxos do
state
end
# Handles a prepared
# Sends out accept when a quorum is met
{:prepared, inst, ballot, accepted_ballot, accepted_value} ->
log(
"#{state.name} - prepared #{inspect(inst)} #{inspect(ballot)} #{inspect(accepted_ballot)} #{inspect(accepted_value)}"
@ -311,6 +397,7 @@ defmodule Paxos do
state
end
# Handles a accept
{:rb_deliver, proc, {:accept, inst, ballot, value}} ->
cond do
has_finished(state, inst) ->
@ -338,6 +425,8 @@ defmodule Paxos do
end
end
# Handles a accept
# Sends out accept when a decide when a quoeom is met
{:accepted, inst, ballot} ->
log("#{state.name} - accepted #{inspect(inst)} #{inspect(ballot)}")
@ -356,6 +445,7 @@ defmodule Paxos do
state
end
# handles a parent request to get a value
{:get_value, inst, pid_to_inform} ->
# log("#{state.name} get_value")
if has_finished(state, inst, true) do
@ -363,6 +453,8 @@ defmodule Paxos do
end
state
# Adds a decision made by a leader to the list of decisions
# and informs the parent of the decision
{:rb_deliver, _, {:decide, inst, value}} ->
log("#{state.name} - decided #{inspect(inst)} #{inspect(value)}")
@ -379,13 +471,12 @@ defmodule Paxos do
end
end
#
# Puts process in the preapre state
#
def prepare(state, _) when state.leader != state.name, do: state
def prepare(state, inst) do
@doc """
Does the logic to decide when to send the prepare messages
Also sets the state nessesary to run the proposal
"""
defp prepare(state, _) when state.leader != state.name, do: state
defp prepare(state, inst) do
cond do
state.instmap[inst] == nil ->
state
@ -417,12 +508,11 @@ defmodule Paxos do
end
end
#
# Process the prepared responses
#
def prepared(state, _) when state.leader != state.name, do: state
def prepared(state, inst) do
@doc """
Processes the prepared messages and when a quorum is met the accept messages are send
"""
defp prepared(state, _) when state.leader != state.name, do: state
defp 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} =
@ -465,13 +555,12 @@ defmodule Paxos do
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
@doc """
Processes the accepted messages and when a qurum is met decide on the value
"""
defp accepted(state, _) when state.leader != state.name, do: state
defp accepted(state, inst) do
or_state 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
@ -493,69 +582,76 @@ defmodule Paxos do
| decided: Map.put(state.decided, inst, value),
instmap: Map.delete(state.instmap, inst)
}
else
state
end
end
#######################
# Interface #
#######################
@doc """
Send the propose message to the paxos replica and waits for a response from the correct instance
"""
def propose(pid, inst, value, t, action \\ nil) do
send(pid, {:propose, inst, value, self(), action})
propose_loop({inst, t})
propose_loop(inst, t)
end
def propose_loop(input) do
{_, t} = input
@doc """
Waits the right message from the paxos replica
"""
defp propose_loop(inst, t) do
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)
{:timeout, ^inst} -> {:timeout}
{:abort, ^inst} -> {:abort}
{:decision, ^inst, d} -> {:decision, d}
x ->
Process.send_after(self(), x, 500)
propose_loop(input)
propose_loop(inst, t)
after
t -> {:timeout}
end
end
@doc """
Sends the :get_value message to the paxos replica
"""
def get_decision(pid, inst, t) do
send(pid, {:get_value, inst, self()})
get_decision_loop({inst, t})
get_decision_loop(inst, t)
end
def get_decision_loop(input) do
{_, t} = input
@doc """
Sends waits for the right message from the paxos replica
"""
defp get_decision_loop(inst, t) do
receive do
{:get_value_res, inst, v} ->
check_and_apply(v, inst, input, &get_decision_loop/1)
{:get_value_res, ^inst, v} ->
v
x ->
Process.send_after(self(), x, 500)
get_decision_loop(input)
get_decision_loop(inst, t)
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
@moduledoc """
A set of a helper functions to manage ballots
"""
@doc """
Create a new ballot
"""
def init(name, number \\ 0), do: {name, number}
@doc """
Increase the ballot number
"""
def inc(b, name \\ nil) do
{old_name, number} = b
@ -565,6 +661,9 @@ defmodule Ballot do
}
end
@doc """
Compare the name of 2 processes and select the lowest one
"""
defp lexicographical_compare(a, b) do
cond do
a == b -> 0
@ -573,7 +672,9 @@ defmodule Ballot do
end
end
@doc """
Callculate the difference between w ballots
"""
defp diff({name1, number1}, {name2, number2}) do
diff = number1 - number2
if diff == 0 do
@ -583,24 +684,41 @@ defmodule Ballot do
end
end
@doc """
Compare 2 ballots
"""
def compare(b1, operator, b2), do: operator.(diff(b1, b2), 0)
end
defmodule EagerReliableBroadcast do
@moduledoc """
This modules implents eager reiable broadcast with the optimization of removing the from the deliver list after hearing
the same number of echos as there is processes
emits {:rb_deliver, proc, message}
"""
require Utils
import Utils
def get_non_rb_name(name) do
@doc """
Removes _br from the name of a process
"""
defp get_non_rb_name(name) do
String.to_atom(String.replace(Atom.to_string(name), "_rb", ""))
end
@doc """
Starts the Eager reliable process and registers it with a name and links the process with the parent process
"""
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
@doc """
Sets state and calls the run function
"""
def init(parent, processes) do
state = %{
name: alter_name(parent, "_rb"),
@ -643,17 +761,24 @@ defmodule EagerReliableBroadcast do
#############
# Interface #
#############
@doc """
Sends a a broadcast request to the eager reliable broadcast replica
"""
def broadcast(name, m), do: safecast(alter_name(name, "_rb"), {:broadcast, m})
end
#
# Emits {:ele_trust, proc }
#
defmodule EventualLeaderElector do
@moduledoc """
This modules implents eventual leader elector
emits {:ele_leader, proc}
"""
require Utils
import Utils
@doc """
Initializes the leader elector process and registers is under name_ele and links it with the parent process
"""
def start(name, processes) do
new_name = alter_name(name, "_ele")
pid = spawn(EventualLeaderElector, :init, [new_name, name, processes])
@ -661,8 +786,9 @@ defmodule EventualLeaderElector do
register_name(new_name, pid)
end
# Init event must be the first
# one after the component is created
@doc """
Initializes state and starts the run function
"""
def init(name, parent, processes) do
processes = Enum.map(processes, fn name -> alter_name(name, "_ele") end)
@ -679,6 +805,9 @@ defmodule EventualLeaderElector do
run(request_heartbeats(state))
end
@doc """
Clears heard_back and updates the sequence number and then sends a request for heartbeats to all proccesses
"""
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)
@ -688,15 +817,18 @@ defmodule EventualLeaderElector do
end
runfn do
# Answer to a heartbeat_request
{:heartbeat_request, name, seq} ->
safecast(name, {:heartbeat, state.parent, seq})
state
# Update heard_back with this name
{:heartbeat, name, seq} ->
or_state seq == state.seq do
%{state | heard_back: MapSet.put(state.heard_back, name)}
end
# One time out check the heard_back and send if a new leader is elected inform the parent
{: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)

View File

@ -1,6 +1,13 @@
defmodule ServerMacros do
@moduledoc """
This module defines some helper macros that are used within the server macro
"""
def create_create_loop(name, do: match_exp, else: process_exp) do
@doc """
This macro creates a wait loop to wait for the messages that are receive match what is inside the
loop
"""
defmacro create_loop(name, do: match_exp) do
function_name = :"#{name}_loop"
ast1 = quote do
@ -19,7 +26,6 @@ defmodule ServerMacros do
quote do
defp unquote(function_name)(v, t) do
var!(v) = v
unquote(process_exp)
receive do
unquote(ast3)
after
@ -29,18 +35,10 @@ defmodule ServerMacros do
end
end
def create_create_loop(name, do: exp, else: else_exp) do
create_create_loop(name, do: exp, else: else_exp)
end
def create_create_loop(name, do: exp) do
create_create_loop(name, do: exp, else: nil)
end
defmacro create_loop(name, clauses) do
create_create_loop(name, clauses)
end
@doc """
this function tries to propose and on failure it calls the else block and
on success tries to match with the expressions on the do block
"""
defmacro try_propose(val, do: ready, else: recal_do) do
ast1 = quote do
{:timeout} -> unquote(recal_do)
@ -68,6 +66,9 @@ defmodule ServerMacros do
end
defmodule Server do
@moduledoc """
Contains the to run the server Code
"""
require ServerMacros
import ServerMacros
require Utils
@ -75,13 +76,19 @@ defmodule Server do
create_log 2
@doc """
Contains the start code for the server
"""
def start(name, participants) do
log("starting server")
log("#{name} Starting server")
pid = spawn(Server, :init, [name, participants])
register_name(name, pid, false)
end
@doc """
Initializes the state and starts the paxos inspect and then it calls the run fn
"""
def init(name, participants) do
paxos = Paxos.start(alter_name(name, "_paxos"), Enum.map(participants, fn name -> alter_name(name, "_paxos") end), true)
state = %{
@ -114,6 +121,9 @@ defmodule Server do
end
@doc """
Checks if the user can play the move before starting the requesting the user to play
"""
defp try_to_play_checks(state, game_id, move, pid_to_inform, repeat \\ false) do
cond do
state.games[game_id] == :not_playing_in_game ->
@ -155,6 +165,9 @@ defmodule Server do
end
end
@doc """
Tries to propose to paxos the game action
"""
defp try_to_play(state, game_id, move, pid_to_inform) do
name = state.name
@ -196,6 +209,9 @@ defmodule Server do
end
end
@doc """
Get the most recent game_state and return it to the player
"""
defp get_game_state(state, game_id, pid_to_inform, repeat \\ false) do
cond do
state.games[game_id] == :not_playing_in_game ->
@ -224,6 +240,9 @@ defmodule Server do
end
end
@doc """
This generates a new hand based on the current game state
"""
defp get_hand_for_game_state(game_state) do
r1 = Enum.random(0..100)
cond do
@ -238,6 +257,9 @@ defmodule Server do
end
end
@doc """
This tries to create a game by sending the create message to paxos
"""
defp try_to_create_game(state, participants) do
game_ids = Map.keys(state.games)
latest = Enum.at(Enum.sort(game_ids), length(game_ids) - 1)
@ -259,7 +281,9 @@ defmodule Server do
#
# Utils
#
@doc """
Checks if a game has been finished
"""
defp is_finished(state, game) do
case state.games[game] do
{:finished, _} -> true
@ -267,6 +291,9 @@ defmodule Server do
end
end
@doc """
Gets up to the most recent instance
"""
defp qurey_status(state) do
v = Paxos.get_decision(state.paxos, state.instance, 100)
or_state v != nil do
@ -275,6 +302,9 @@ defmodule Server do
end
end
@doc """
Sets the modified flag
"""
defp set_modifed(state, game, val \\ false) do
or_state not is_finished(state, game) and state.games[game] != :not_playing_in_game and state.games[game] != nil do
%{state | games: Map.put(state.games, game, %{state.games[game] | modified: val})}