From e3b2e60ddc66140f830ae68d57cd599f43378c5c Mon Sep 17 00:00:00 2001 From: Andre Henriques Date: Sat, 20 Jan 2024 20:26:41 +0000 Subject: [PATCH] fixed compiler errors and updated read me --- README.md | 5 ++ lib/paxos.ex | 98 ++++++++++++------------------ lib/server.ex | 163 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 142 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index 67e23b1..b14b702 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,11 @@ The server will answer with one of the following: There is two ways of interacting with the server via the API or the `Client` Module +## Notes + +By default all the logs are disabled to enable all the logs inside the paxos file there is a module variable called `@min_print_level`, +by setting it to 0 it enables all the logs + ## API Start a new iex session and compilie `paxos.ex` and `server.ex`: diff --git a/lib/paxos.ex b/lib/paxos.ex index d265710..dea5023 100644 --- a/lib/paxos.ex +++ b/lib/paxos.ex @@ -21,10 +21,8 @@ defmodule Utils do """ @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 - """ + # 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 @@ -132,9 +130,7 @@ defmodule Paxos do create_log 0 - @doc """ - This macro allows the state.instmap to be updated very easily - """ + # 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)] @@ -171,9 +167,7 @@ defmodule Paxos do run(state) end - @doc """ - Guarantees that a specific state exists for a specific instance - """ + # 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 = @@ -197,11 +191,9 @@ defmodule Paxos do end end - @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 - """ + # 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 @@ -211,21 +203,19 @@ 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}} -> - """ + # 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} -> @@ -253,6 +243,8 @@ defmodule Paxos do %{map| ballot: Ballot.inc(map.ballot)} end + state + 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) @@ -277,7 +269,7 @@ defmodule Paxos do end # Handles the sharing of a proposal value to other processes - {:rb_deliver, proc, {:other_propose, inst, value}} -> + {:rb_deliver, _, {:other_propose, inst, value}} -> cond do has_finished(state, inst, true) -> EagerReliableBroadcast.broadcast( @@ -310,6 +302,7 @@ defmodule Paxos do set_instmap do %{ map | ballot: ballot } end + state Ballot.compare(ballot, &>/2, state.instmap[inst].ballot) -> safecast(proc, @@ -320,6 +313,7 @@ defmodule Paxos do set_instmap do %{ map | ballot: ballot } end + state true -> safecast(proc, {:nack, inst, ballot}) @@ -349,12 +343,13 @@ defmodule Paxos do } end + state true -> state end # Handles an abort - {:rb_deliver, _proc, {:abort, inst, ballot}} -> + {:rb_deliver, _proc, {:abort, inst, _}} -> cond do has_finished(state, inst) -> state @@ -418,6 +413,7 @@ defmodule Paxos do accepted_ballot: ballot } end + state else log("#{state.name} -> #{proc} nack") safecast(proc, {:nack, inst, ballot}) @@ -471,10 +467,8 @@ defmodule Paxos do end end - @doc """ - Does the logic to decide when to send the prepare messages - Also sets the state nessesary to run the proposal - """ + # 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 @@ -505,15 +499,14 @@ defmodule Paxos do has_sent_accept: false } end + state end end - @doc """ - Processes the prepared messages and when a quorum is met the accept messages are send - """ + # 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 + or_state 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}, @@ -550,14 +543,11 @@ defmodule Paxos do has_sent_accept: true } end - else state end end - @doc """ - Processes the accepted messages and when a qurum is met decide on the value - """ + # 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 @@ -598,9 +588,7 @@ defmodule Paxos do propose_loop(inst, t) end - @doc """ - Waits the right message from the paxos replica - """ + # Waits the right message from the paxos replica defp propose_loop(inst, t) do receive do {:timeout, ^inst} -> {:timeout} @@ -622,9 +610,7 @@ defmodule Paxos do get_decision_loop(inst, t) end - @doc """ - Sends waits for the right message from the paxos replica - """ + # Sends waits for the right message from the paxos replica defp get_decision_loop(inst, t) do receive do {:get_value_res, ^inst, v} -> @@ -661,9 +647,7 @@ defmodule Ballot do } end - @doc """ - Compare the name of 2 processes and select the lowest one - """ + # Compare the name of 2 processes and select the lowest one defp lexicographical_compare(a, b) do cond do a == b -> 0 @@ -672,9 +656,7 @@ defmodule Ballot do end end - @doc """ - Callculate the difference between w ballots - """ + # Callculate the difference between w ballots defp diff({name1, number1}, {name2, number2}) do diff = number1 - number2 if diff == 0 do @@ -701,9 +683,7 @@ defmodule EagerReliableBroadcast do require Utils import Utils - @doc """ - Removes _br from the name of a process - """ + # 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 diff --git a/lib/server.ex b/lib/server.ex index ac85326..b378e6d 100644 --- a/lib/server.ex +++ b/lib/server.ex @@ -121,9 +121,7 @@ defmodule Server do end - @doc """ - Checks if the user can play the move before starting the requesting the user to play - """ + # 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 -> @@ -165,9 +163,7 @@ defmodule Server do end end - @doc """ - Tries to propose to paxos the game action - """ + # Tries to propose to paxos the game action defp try_to_play(state, game_id, move, pid_to_inform) do name = state.name @@ -209,9 +205,7 @@ defmodule Server do end end - @doc """ - Get the most recent game_state and return it to the player - """ + # 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 -> @@ -240,9 +234,7 @@ defmodule Server do end end - @doc """ - This generates a new hand based on the current game state - """ + # 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 @@ -257,9 +249,7 @@ defmodule Server do end end - @doc """ - This tries to create a game by sending the create message to paxos - """ + # 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) @@ -281,9 +271,8 @@ defmodule Server do # # Utils # - @doc """ - Checks if a game has been finished - """ + + # Checks if a game has been finished defp is_finished(state, game) do case state.games[game] do {:finished, _} -> true @@ -291,9 +280,7 @@ defmodule Server do end end - @doc """ - Gets up to the most recent instance - """ + # 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 @@ -302,9 +289,7 @@ defmodule Server do end end - @doc """ - Sets the modified flag - """ + # 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})} @@ -315,6 +300,7 @@ defmodule Server do # Apply Game States # + # Calculates the index based on the incoming index and a move defp get_index(indexed_game_state, spos, index) do index = spos + index len = length(indexed_game_state) @@ -327,7 +313,10 @@ defmodule Server do index end end - + + # This funcion looks at pluses and blackholes and chekes if they can be merged + # This funcion will pick a state like this [ 1, 2, :+, 2, 1 ] and create a new state [ 1, {:merged, 3} , 1] and then call expand merge which will expand the merge + # Note by this point the state is indexed so it looks like [{1, 0}, {:+, 1}, {1, 2}] defp simplify_game_state_pluses([], indexed_game_state), do: {false, indexed_game_state} defp simplify_game_state_pluses([{:+, i} | tl], indexed_game_state) do before_i = get_index(indexed_game_state, i, -1) @@ -345,7 +334,7 @@ defmodule Server do list = indexed_game_state |> Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, n + 1}, i}, else: {x, ti} end) |> - Enum.filter(fn {x, ti} -> cond do + Enum.filter(fn {_, ti} -> cond do b_i == ti -> false a_i == ti -> false true -> true @@ -376,7 +365,7 @@ defmodule Server do list = indexed_game_state |> Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, trunc(:math.floor((a + b) / 2))}, i}, else: {x, ti} end) |> - Enum.filter(fn {x, ti} -> cond do + Enum.filter(fn {_, ti} -> cond do b_i == ti -> false a_i == ti -> false true -> true @@ -388,7 +377,8 @@ defmodule Server do simplify_game_state_pluses(tl, indexed_game_state) end end - + + # Check if the item in an game is the substate merged defp is_merged(item) do case item do {:merged, _} -> true @@ -396,6 +386,9 @@ defmodule Server do end end + # This funcion expands merges + # With a state like this [1, {:merged, 3}, 1] it will create a game state like this [ {:merged, 4 } ] + # Note by this point the state is indexed so it looks like [{1, 0}, {:+, 1}, {1, 2}] defp expand_merge(indexed_game_state) do {{:merged, n}, i} = indexed_game_state |> Enum.find(fn {x, _} -> is_merged(x) end) @@ -412,7 +405,7 @@ defmodule Server do _ -> indexed_game_state |> Enum.map(fn {x, ti} -> if ti == i, do: {{:merged, max(n, a) + 1}, i}, else: {x, ti} end) |> - Enum.filter(fn {x, ti} -> cond do + Enum.filter(fn {_, ti} -> cond do b_i == ti -> false a_i == ti -> false true -> true @@ -428,7 +421,8 @@ defmodule Server do indexed_game_state end end - + + # This funcion removes previous indexes and reindexs the state defp reindex(list, flat \\ true) do list = if flat do list |> Enum.map(fn {n, _} -> n end) @@ -438,18 +432,14 @@ defmodule Server do [list, 0..(length(list) - 1)] |> Enum.zip() end - - defp remove_merged([], rec, _), do: rec - - defp remove_merged([{:merged, n} | tl], rec, add), - do: if add, do: remove_merged(tl, rec ++ [n], false), - else: remove_merged(tl, rec, false) - - defp remove_merged([n | tl], rec, add), do: - remove_merged(tl, rec ++ [n], add) - - defp remove_merged(list), do: remove_merged(list, [], true) - + + # Removes all merged from the array + defp remove_merged(l), do: l |> Enum.map(fn x -> case x do + {:merged, n} -> n + x -> x + end end) + + # This funcion recieves the game state after the move was done and tries to simplify the game defp simplify_game_state(game_state) do indexed_game_state = game_state |> @@ -474,7 +464,8 @@ defmodule Server do indexed_game_state |> Enum.map(fn {v, _} -> v end) end end - + + # This function tries to apply the make_move command defp apply_game(state, {:make_move, game_id, player_name, pos_move, new_hand}) do game = state.games[game_id] case game do @@ -507,6 +498,7 @@ defmodule Server do end end + # This function tries to apply the start_game command defp apply_game(state, {:start_game, game_id, participants, new_game_state, hand}) do cond do state.games[game_id] -> @@ -534,18 +526,23 @@ defmodule Server do ############ # Interface ############ - + + # handles responses from the start game request create_loop :start_game do {:start_game_ans, game_id} -> log("Started a game #{game_id}") {:start_game, game_id} end + @doc """ + Requests the server to start a game + """ def start_game(name, participants) do safecast(name, {:start_game, participants, self()}) start_game_loop(nil, 10000) end + # handles responses from the get game state request create_loop :get_game_state do {:game_state, ^v, :not_playing} -> log("Not Playing in that game") @@ -561,11 +558,15 @@ defmodule Server do {:not_exists} end + @doc """ + Requests the server to get the game state + """ def get_game_state(name, game_id) do safecast(name, {:get_game_state, game_id, self()}) get_game_state_loop(game_id, 10000) end + # handles responses from the make move request create_loop :make_move do {:make_move, ^v, :game_does_not_exist} -> log("Got game does not exist") @@ -587,6 +588,9 @@ defmodule Server do {:state, game_state, hand} end + @doc """ + Requests to make a move + """ def make_move(name, game_id, move) do safecast(name, {:make_move, game_id, move, self()}) make_move_loop(game_id, 10000) @@ -595,23 +599,36 @@ defmodule Server do ############ # Debug ############ - + + @doc """ + Quicky creates some servers it's useful in testing + """ def spinup(number_of_participants) do - procs = Enum.to_list(0..number_of_participants) |> Enum.map(fn n -> :"p#{n}" end) + procs = Enum.to_list(1..number_of_participants) |> Enum.map(fn n -> :"p#{n}" end) Enum.map(procs, fn proc -> Server.start(proc, procs) end) end + @doc """ + Quicky kills some servers it's useful in testing + """ def kill (pids) do pids |> Enum.map(fn m -> Process.exit(m, :kill) end) end end defmodule Client do + @moduledoc """ + This module handles displaying the game state in a nice to understand + way + """ import Utils require Utils create_log 3 + @doc """ + This function plays the displays the game then waits for user input and displays the next state of the game + """ def play_game(process, game_id) do game = Server.get_game_state(process, game_id) @@ -675,6 +692,11 @@ defmodule Client do end + @doc """ + This funcion waits for user input and then commands the server to act + + Use the display_game funcion in a seperate terminal to see the game + """ def control_game(process, game_id) do to_play = IO.gets("Type the number you want to play or q to exit: ") to_play = to_play |> String.trim("\"") |> String.trim() @@ -707,10 +729,15 @@ defmodule Client do end + @doc """ + This funcion tries to display the most recent version of the game + + Use control_game to control game + """ def display_game(process, game_id, temp_game \\ [], temp_hand \\ []) do game = Server.get_game_state(process, game_id) - cont = case game do + case game do :timeout -> log("Could to not comunicate with the server") {:not_exists} -> log("Game does not exist") {:not_playing} -> log("Not Playing in that game") @@ -732,44 +759,50 @@ defmodule Client do end end end - - def to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end) - def to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> IO.ANSI.color(15) <> " + " <> IO.ANSI.reset() - def to_name(atom) when atom == :-, do: IO.ANSI.color_background(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset() - def to_name(atom) when atom == :b, do: IO.ANSI.color_background(232) <> IO.ANSI.color(15) <> " b " <> IO.ANSI.reset() - def to_name(atom) when is_atom(atom), do: atom - def to_name(i) do + + # This funcion recieves some objects and transforms them into user friendly text + defp to_name(list) when is_list(list), do: list |> Enum.map(fn x -> to_name(x) end) + defp to_name(atom) when atom == :+, do: IO.ANSI.color_background(9) <> IO.ANSI.color(15) <> " + " <> IO.ANSI.reset() + defp to_name(atom) when atom == :-, do: IO.ANSI.color_background(21) <> IO.ANSI.color(15) <> " - " <> IO.ANSI.reset() + defp to_name(atom) when atom == :b, do: IO.ANSI.color_background(232) <> IO.ANSI.color(15) <> " b " <> IO.ANSI.reset() + defp to_name(atom) when is_atom(atom), do: atom + defp to_name(i) do letter = [ "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og" ] |> Enum.at(i - 1) color = [46,45,138,19,11,140,8,47,57,159,72,48,55,35,251,188,107,110,21,11,156,134,128,179,140,234,14,90,206,7,249,209,253,123,192,165,234,136,198,208,43,34,215,127,23,250,177,237,124,202,229, 63,206,220,224,109,202,113,253,7,243,26,160,65,39,112,57,75, 252,82,213,186,68,243,134,100,226,48,90,134,208,102,25,106,72, 242,26,59,166,26,187,54,194,165,97,219,186,130,7,154,233,85, 130,67,43,200,90,60,148,49,161,110,247,116,223,159,132,132] |> Enum.at(i - 1) |> IO.ANSI.color() color <> String.pad_leading("#{letter}", 3, " ") <> IO.ANSI.reset() end - - def interpolate(list) do + + # Adds the index as an indicator and pads the index + defp interpolate(list) do [list, 0..length(list) - 1] |> Enum.zip() |> Enum.reduce([], fn {v, i}, acc -> acc ++ [String.pad_leading("#{i}", 3, " "), v] end) end - def grow_empty_list(t, i, acc) when i == 0, do: t ++ acc - def grow_empty_list([], i, acc), do: grow_empty_list(acc, i, []) - def grow_empty_list([h | tl], i, acc), do: + # This grows a list with between spaces every item of the interpolated list + # This allows the items to take as puch space in the possilble + defp grow_empty_list(t, i, acc) when i == 0, do: t ++ acc + defp grow_empty_list([], i, acc), do: grow_empty_list(acc, i, []) + defp grow_empty_list([h | tl], i, acc), do: grow_empty_list(tl, i - 1, acc ++ [ h ++ [" "] ]) - def fill(list) do + # This takes the list that was generated in the grow_empty_list function and merges it between every other item + defp fill(list) do to_fill = 32 - length(list) to_add = grow_empty_list(Enum.map(list, fn _ -> [] end), to_fill, []) fill(list, to_add, []) end - def fill([], _, acc), do: acc - def fill([hd | tail], [add_hd | add_tail], acc) do + defp fill([], _, acc), do: acc + defp fill([hd | tail], [add_hd | add_tail], acc) do fill(tail, add_tail ,acc ++ [hd] ++ add_hd) end - def printpt(game_state, hand), do: printpt(game_state, hand, 0) - def printpt(_, _, i) when i > 16, do: nil - def printpt(game_state, hand, i) do + # This functions prints the circle + defp printpt(game_state, hand), do: printpt(game_state, hand, 0) + defp printpt(_, _, i) when i > 16, do: nil + defp printpt(game_state, hand, i) do res = case i do 0 -> " xxx \n" |>