Compare commits
22 Commits
ee9f5e1187
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d5383e67b2 | |||
| 05c09d10f3 | |||
| 134d958be8 | |||
| 0433210920 | |||
| 7ff5414eba | |||
| 074944ccb4 | |||
| 5740077954 | |||
| c803a3bb2b | |||
| 3cbf7b99af | |||
| e3b2e60ddc | |||
| 2d20363870 | |||
| 8b35a1fc85 | |||
| 0f9f885cc0 | |||
| b21496dad6 | |||
| 0a9f086d5d | |||
| b337101566 | |||
| c33b7edef6 | |||
| 8c674626f8 | |||
| a22005c868 | |||
| cc7dc7cea8 | |||
| b4d0527b41 | |||
| b015d8cabd |
12
.drone.yml
12
.drone.yml
@@ -10,6 +10,18 @@ steps:
|
||||
- marp presentation.md -o index.html
|
||||
- cp * /home/andr3/services/notes/presentation
|
||||
|
||||
- name: Create release
|
||||
environment:
|
||||
TOKEN:
|
||||
from_secret: token
|
||||
commands:
|
||||
- cp lib/server.ex .
|
||||
- zip server.zip server.ex README.md
|
||||
- tea login add --url https://git.andr3h3nriqu3s.com --token "$TOKEN"
|
||||
- tea r rm -y latest || echo "Release not found"
|
||||
- tea r c --title "Latest" --asset "README.md" --asset "lib/paxos.ex" --asset "lib/server.ex" --asset "server.zip" latest
|
||||
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
- main
|
||||
|
||||
245
README.md
Normal file
245
README.md
Normal file
@@ -0,0 +1,245 @@
|
||||
Andre Goncalves Henriques (URN: 6644818), Seoeun Lee (URN: 6595203)
|
||||
|
||||
# Cooperative Application "Atomas" Game
|
||||
|
||||
The application that this project implements is a cooperative version of the game [Atomas](https://play.google.com/store/apps/details?id=com.sirnic.atomas&hl=en&gl=US&pli=1).
|
||||
Atomas is a simple single player mobile phone game which allows players to combine atoms to make new atoms with higher atomic numbers.
|
||||
|
||||
The Server application implements a cooperative and simplified version of this game that allows players to play a game of Atomas in the same board at the same time.
|
||||
|
||||
The Server uses Paxos to guarantee that only one user can play at a time and that a player cannot make a move on a different game state than another player.
|
||||
|
||||
The `server.ex` file provides both `Server` Module that runs this service and a `Client` Module that can be used to display the game state in the terminal with nicer representation.
|
||||
|
||||
# API
|
||||
|
||||
The server provides the following API:
|
||||
|
||||
```elixir
|
||||
{:start_game, participants, pid_to_inform}
|
||||
{:get_game_state, game_id, pid_to_inform}
|
||||
{:make_move, game_id, move, pid_to_inform}
|
||||
```
|
||||
|
||||
## Important Concepts
|
||||
|
||||
The `game_id` is a number and is an identifier for the game. This can be a number as paxos guarantees that there cannot be two games with same number, as the server would have learned about that decision and would have incremented the number.
|
||||
|
||||
The `game_state` is a representation of a game state that corresponds to an array of numbers, and some special atoms, where the number represents the atomic number. The maximum length of this array is 16, so when an array gets bigger than that the game is over.
|
||||
|
||||
## Start Game
|
||||
|
||||
The `:start_game` message causes the server to create a new game.
|
||||
|
||||
The `:start_game` message takes the arguments `participants` and `pid_to_inform`.
|
||||
- `participants` is a list of names of the servers that are participating in the game.
|
||||
- `pid_to_inform` is a pid of the process that needs to be informed of the success or failure of this action.
|
||||
|
||||
For example, to request the start of a game, a user might do somethig like:
|
||||
|
||||
```elixir
|
||||
send(server_pid, {:start_game, [:p0, :p1], self()})
|
||||
# use recieve do to wait for the message
|
||||
|
||||
# Or, assuming the server was started with the name :p0
|
||||
|
||||
Server.start_game(:p0, [:p0, :p1])
|
||||
```
|
||||
|
||||
The server will answer with:
|
||||
```
|
||||
{:start_game_ans, game_id}
|
||||
```
|
||||
|
||||
This request will always create a new game (with the exception of crashes).
|
||||
|
||||
|
||||
## Get Game State
|
||||
|
||||
The `:get_game_state` message causes the server to return the current state of a game.
|
||||
|
||||
The `:get_game_state` message takes the arguments as `game_id` and `pid_to_inform`.
|
||||
- `game_id` is the game_id returned by the `:start_game` request
|
||||
- `pid_to_inform` is a pid of the process that needs to be informed of the success or failure of this action.
|
||||
|
||||
For example, to request the current state of a game:
|
||||
|
||||
```elixir
|
||||
send(server_pid, {:get_game_state, 1, self()})
|
||||
# use 'recieve do' to wait for the message
|
||||
|
||||
# Or, assuming the server was started with the name ':p0'
|
||||
|
||||
Server.get_game_state(:p0, 1)
|
||||
```
|
||||
|
||||
The server will answer with one of the following:
|
||||
```elixir
|
||||
{:game_state, game_id, :not_playing} # When the player is not playing in the game
|
||||
{:game_state, game_id, :game_does_not_exist} # When the game was never created
|
||||
{:game_state, game_id, :game_finished, score} # When the game is already over
|
||||
{:game_state, game_id, game_state, hand} # When the player is in the game, and the game has started
|
||||
# Returns the current game state of the game,
|
||||
# and the current "atom" on the hand of the player
|
||||
```
|
||||
|
||||
## Make a move
|
||||
|
||||
The `:make_move` message causes the server to try to play the move that the player is requesting.
|
||||
|
||||
The `:make_move` message takes the arguments as `game_id`, `move`, and `pid_to_inform`.
|
||||
- `game_id` is the game_id returned by the `:start_game` request
|
||||
- `move` is the position in the game state array where the play wants to insert their "atom"
|
||||
- `pid_to_inform` is a pid of the process that needs to be informed of the success or failure of this action.
|
||||
|
||||
For example, to request the current state of a agame:
|
||||
|
||||
```elixir
|
||||
send(server_pid, {:make_move, 1, 3, self()})
|
||||
# use recieve do to wait for the message
|
||||
|
||||
# Or, assuming the server was started with the name :p0
|
||||
|
||||
Server.make_move(:p0, 1, 3)
|
||||
```
|
||||
|
||||
The server will answer with one of the following:
|
||||
```elixir
|
||||
{:make_move, game_id, :not_playing} # When the player is not playing in the game
|
||||
{:make_move, game_id, :game_does_not_exist} # When the game was never created
|
||||
{:make_move, game_id, :game_finished, score} # When the game is already over
|
||||
{:make_move, game_id, :invalid_move} # When the move requested by the player is an invalid move
|
||||
{:make_move, game_id, :player_moved_before, game_state, hand} # When the player is in the game and the game has started,
|
||||
# but another player has played the game before this request was made
|
||||
# Returns the current game state of the game,
|
||||
# and the current "atom" on the player's hand
|
||||
{:make_move, game_id, game_state, hand} # When the player is in the game and the game has not finished
|
||||
# and the move is valid
|
||||
# Returns the current game state of the game,
|
||||
# and the current "atom" on the hand of the player
|
||||
```
|
||||
|
||||
# Properties
|
||||
|
||||
- If a game exists, then there was a process that creates it.
|
||||
- Eventually, all processes will be in the same game state
|
||||
- A player cannot make a move in a game state unless it's updated to the last game state
|
||||
- If two players make a move at the same time in the same game, only one of the moves happens and the other player eventually gets notified
|
||||
- If a player makes a valid move it will be evetually played
|
||||
|
||||
# Assumptions
|
||||
|
||||
- Processes won't behave in a byzantine way
|
||||
- Only a minority of processes can fail
|
||||
- Will run in a partial synchronous system
|
||||
- When the game is created, the game state will have atoms in it, and the participants will have atoms in their hands
|
||||
|
||||
# Usage instructions
|
||||
|
||||
There are 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`.
|
||||
Setting it to 0 enables all the logs
|
||||
|
||||
## API
|
||||
|
||||
Start a new iex session and compilie `paxos.ex` and `server.ex`:
|
||||
|
||||
```elixir
|
||||
c "paxos.ex"; c "server.ex"
|
||||
```
|
||||
|
||||
** Paxos needs to be compiled before the server as the server requires macros written in the paxos file **
|
||||
|
||||
Now start 3 instances of the server:
|
||||
|
||||
```elixir
|
||||
procs = Enum.to_list(1..3) |> Enum.map(fn x -> :"p#{x}" end)
|
||||
pids = procs |> Enum.map(fn x -> Server.start(x, procs) end)
|
||||
```
|
||||
|
||||
After the 3 instances have been started, you can use Server helper functions to communicate with the Server:
|
||||
|
||||
```elixir
|
||||
Server.start_game(:p1, [:p1, :p2])
|
||||
Server.get_game_state(:p1, 1)
|
||||
Server.make_move(:p1, 1, 0)
|
||||
```
|
||||
|
||||
## Using the Client
|
||||
|
||||
Open 2 different terminals. One terminal will display the game, and the other one will be used to control the game
|
||||
|
||||
In the first terminal, start an iex session with a session name:
|
||||
```
|
||||
iex --sname c1
|
||||
```
|
||||
|
||||
In the second terminal, start an iex session with a session name:
|
||||
```
|
||||
iex --sname c2
|
||||
```
|
||||
|
||||
On the first terminal, connect the first terminal to the second terminal:
|
||||
```elixir
|
||||
# In c1
|
||||
Node.connect(:"c2@<computer_name>")
|
||||
```
|
||||
|
||||
On both terminals compile paxos and server:
|
||||
```elixir
|
||||
# In both run
|
||||
c "paxos.ex"; c "server.ex"
|
||||
```
|
||||
|
||||
Start the servers on the first terminal, create a game, and enter display mode:
|
||||
```elixir
|
||||
# In c1
|
||||
procs = Enum.to_list(1..3) |> Enum.map(fn x -> :"p#{x}" end)
|
||||
pids = procs |> Enum.map(fn x -> Server.start(x, procs) end)
|
||||
|
||||
Server.start_game(:p1, [:p1, :p2])
|
||||
Client.display_game(:p1, 1)
|
||||
```
|
||||
|
||||
You now should see something like:
|
||||
|
||||
```
|
||||
0 H
|
||||
|
||||
1
|
||||
Be
|
||||
H
|
||||
|
||||
5 2
|
||||
Be
|
||||
|
||||
Be Li
|
||||
|
||||
|
||||
4 3
|
||||
|
||||
|
||||
Be
|
||||
```
|
||||
|
||||
The Center "Atom" is your hand, while the Letters arround are the "game_state" or "board",
|
||||
the number represents where you can put the "Atom" in your hand
|
||||
|
||||
|
||||
Start control mode on the second terminal by entering:
|
||||
```elixir
|
||||
# In c2
|
||||
Client.control_game(:p1, 1)
|
||||
```
|
||||
|
||||
You should see:
|
||||
```
|
||||
Type the number you want to play or q to exit:
|
||||
```
|
||||
|
||||
On the second terminal, insert the number in where you want to make your move and hit enter!
|
||||
|
||||
The map should update on the top screen and now you can play the game!
|
||||
347
lib/paxos.ex
347
lib/paxos.ex
@@ -1,7 +1,28 @@
|
||||
defmodule Utils do
|
||||
#
|
||||
# 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 all modules
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Sets the min log level
|
||||
0 - show all
|
||||
1 - ignore paxos
|
||||
2 - ignore server
|
||||
"""
|
||||
@min_print_level 3
|
||||
|
||||
# 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
|
||||
@@ -11,9 +32,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 ->
|
||||
@@ -29,7 +60,10 @@ defmodule Utils do
|
||||
:error
|
||||
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
|
||||
@@ -37,13 +71,26 @@ defmodule Utils do
|
||||
end
|
||||
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
|
||||
@@ -52,7 +99,10 @@ defmodule Utils do
|
||||
end
|
||||
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
|
||||
@@ -65,37 +115,43 @@ 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
|
||||
|
||||
# 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))
|
||||
var!(state) = %{var!(state) | instmap: new_instmap }
|
||||
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}")
|
||||
|
||||
pid = spawn(Paxos, :init, [name, processes])
|
||||
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)
|
||||
@@ -110,9 +166,9 @@ 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
|
||||
|
||||
# Guarantees that a specific state exists for a specific instance
|
||||
defp has_or_create(state, inst, value \\ nil, pid_to_inform \\ nil) do
|
||||
or_state state.instmap[inst] == nil do
|
||||
instmap =
|
||||
Map.put(state.instmap, inst, %{
|
||||
@@ -127,15 +183,17 @@ defmodule Paxos do
|
||||
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
|
||||
|
||||
# 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
|
||||
@@ -143,17 +201,32 @@ defmodule Paxos do
|
||||
true -> false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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} ->
|
||||
# {: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")
|
||||
|
||||
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)}")
|
||||
|
||||
# Handles a proposal message from the parent process
|
||||
{:propose, inst, value, pid_to_inform} ->
|
||||
log("#{state.name} - Propose #{inspect(inst)}")
|
||||
|
||||
cond do
|
||||
has_finished(state, inst, true) ->
|
||||
@@ -161,17 +234,9 @@ defmodule Paxos do
|
||||
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)
|
||||
state = has_or_create(state, inst, value, pid_to_inform)
|
||||
prepare(state, inst)
|
||||
|
||||
state.instmap[inst].value == nil ->
|
||||
@@ -181,7 +246,6 @@ defmodule Paxos do
|
||||
%{ map |
|
||||
value: value,
|
||||
pid_to_inform: pid_to_inform,
|
||||
action: action,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -191,8 +255,9 @@ defmodule Paxos do
|
||||
EagerReliableBroadcast.broadcast(state.name, {:other_propose, inst, value})
|
||||
prepare(state, inst)
|
||||
end
|
||||
|
||||
{:rb_deliver, proc, {:other_propose, inst, value}} ->
|
||||
|
||||
# Handles the sharing of a proposal value to other processes
|
||||
{:rb_deliver, _, {:other_propose, inst, value}} ->
|
||||
cond do
|
||||
has_finished(state, inst, true) ->
|
||||
EagerReliableBroadcast.broadcast(
|
||||
@@ -208,7 +273,8 @@ defmodule Paxos do
|
||||
end
|
||||
prepare(state, inst)
|
||||
end
|
||||
|
||||
|
||||
# Handles a prepare request from the leader
|
||||
{:rb_deliver, proc, {:prepare, proc, inst, ballot}} ->
|
||||
log("#{state.name} - prepare from #{proc}")
|
||||
|
||||
@@ -224,6 +290,7 @@ defmodule Paxos do
|
||||
set_instmap do
|
||||
%{ map | ballot: ballot }
|
||||
end
|
||||
state
|
||||
|
||||
Ballot.compare(ballot, &>/2, state.instmap[inst].ballot) ->
|
||||
safecast(proc,
|
||||
@@ -234,12 +301,14 @@ defmodule Paxos do
|
||||
set_instmap do
|
||||
%{ map | ballot: ballot }
|
||||
end
|
||||
state
|
||||
|
||||
true ->
|
||||
safecast(proc, {:nack, inst, ballot})
|
||||
state
|
||||
end
|
||||
|
||||
|
||||
# Handles a nack message
|
||||
{:nack, inst, ballot} ->
|
||||
log("#{state.name} - nack #{inspect(inst)} #{inspect(ballot)}")
|
||||
|
||||
@@ -262,11 +331,13 @@ defmodule Paxos do
|
||||
}
|
||||
end
|
||||
|
||||
state
|
||||
true ->
|
||||
state
|
||||
end
|
||||
|
||||
{:rb_deliver, _proc, {:abort, inst, ballot}} ->
|
||||
# Handles an abort message
|
||||
{:rb_deliver, _proc, {:abort, inst, _}} ->
|
||||
cond do
|
||||
has_finished(state, inst) ->
|
||||
state
|
||||
@@ -281,6 +352,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)}"
|
||||
@@ -307,6 +380,7 @@ defmodule Paxos do
|
||||
state
|
||||
end
|
||||
|
||||
# Handles an accept message
|
||||
{:rb_deliver, proc, {:accept, inst, ballot, value}} ->
|
||||
cond do
|
||||
has_finished(state, inst) ->
|
||||
@@ -327,6 +401,7 @@ defmodule Paxos do
|
||||
accepted_ballot: ballot
|
||||
}
|
||||
end
|
||||
state
|
||||
else
|
||||
log("#{state.name} -> #{proc} nack")
|
||||
safecast(proc, {:nack, inst, ballot})
|
||||
@@ -334,6 +409,8 @@ defmodule Paxos do
|
||||
end
|
||||
end
|
||||
|
||||
# Handles an accept message
|
||||
# Sends out accept when a decide when a quoeom is met
|
||||
{:accepted, inst, ballot} ->
|
||||
log("#{state.name} - accepted #{inspect(inst)} #{inspect(ballot)}")
|
||||
|
||||
@@ -351,14 +428,17 @@ defmodule Paxos do
|
||||
true ->
|
||||
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
|
||||
safecast(pid_to_inform, {:get_value_res, inst, state.decided[inst]})
|
||||
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)}")
|
||||
|
||||
@@ -375,13 +455,10 @@ 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
|
||||
# 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
|
||||
@@ -410,16 +487,14 @@ defmodule Paxos do
|
||||
has_sent_accept: false
|
||||
}
|
||||
end
|
||||
state
|
||||
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
|
||||
# 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
|
||||
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},
|
||||
@@ -456,25 +531,16 @@ defmodule Paxos do
|
||||
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
|
||||
# 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
|
||||
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}
|
||||
@@ -489,69 +555,72 @@ defmodule Paxos do
|
||||
| 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})
|
||||
#######################
|
||||
# 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) do
|
||||
send(pid, {:propose, inst, value, self()})
|
||||
|
||||
propose_loop({inst, t})
|
||||
propose_loop(inst, t)
|
||||
end
|
||||
|
||||
def propose_loop(input) do
|
||||
{_, t} = input
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
def init(name, number \\ 0), do: {name, number}
|
||||
@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
|
||||
|
||||
@@ -560,7 +629,8 @@ defmodule Ballot do
|
||||
number + 1
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
# Compare the name of 2 processes and select the lowest one
|
||||
defp lexicographical_compare(a, b) do
|
||||
cond do
|
||||
a == b -> 0
|
||||
@@ -569,7 +639,7 @@ defmodule Ballot do
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Callculate the difference between w ballots
|
||||
defp diff({name1, number1}, {name2, number2}) do
|
||||
diff = number1 - number2
|
||||
if diff == 0 do
|
||||
@@ -578,25 +648,40 @@ defmodule Ballot do
|
||||
diff
|
||||
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
|
||||
# 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"),
|
||||
@@ -639,17 +724,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])
|
||||
@@ -657,8 +749,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)
|
||||
|
||||
@@ -674,7 +767,10 @@ 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)
|
||||
@@ -684,15 +780,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)
|
||||
|
||||
214
lib/server.ex
214
lib/server.ex
@@ -1,6 +1,13 @@
|
||||
defmodule ServerMacros do
|
||||
|
||||
def create_create_loop(name, do: match_exp, else: process_exp) do
|
||||
@moduledoc """
|
||||
This module defines some helper macros that are used within the server macro
|
||||
"""
|
||||
|
||||
@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
|
||||
@@ -28,19 +34,11 @@ defmodule ServerMacros do
|
||||
end
|
||||
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,20 +66,29 @@ defmodule ServerMacros do
|
||||
end
|
||||
|
||||
defmodule Server do
|
||||
@moduledoc """
|
||||
Contains the to run the server Code
|
||||
"""
|
||||
require ServerMacros
|
||||
import ServerMacros
|
||||
require Utils
|
||||
import Utils
|
||||
|
||||
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 = %{
|
||||
@@ -97,10 +104,12 @@ defmodule Server do
|
||||
|
||||
runfn do
|
||||
{:start_game, participants, pid_to_inform} ->
|
||||
{state, game_id} = try_to_create_game(state, participants)
|
||||
state = set_modifed(state, game_id)
|
||||
safecast(pid_to_inform, {:start_game_ans, game_id})
|
||||
state
|
||||
or_state is_list(participants) do
|
||||
{state, game_id} = try_to_create_game(state, participants)
|
||||
state = set_modifed(state, game_id)
|
||||
safecast(pid_to_inform, {:start_game_ans, game_id})
|
||||
state
|
||||
end
|
||||
|
||||
{:get_game_state, game_id, pid_to_inform} ->
|
||||
state = get_game_state(state, game_id, pid_to_inform)
|
||||
@@ -111,7 +120,8 @@ defmodule Server do
|
||||
try_to_play_checks(state, game_id, move, pid_to_inform)
|
||||
|
||||
end
|
||||
|
||||
|
||||
# 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 ->
|
||||
@@ -143,7 +153,7 @@ defmodule Server do
|
||||
not is_number(move) ->
|
||||
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
|
||||
state
|
||||
move >= length(game.game_state) ->
|
||||
move >= length(game.game_state) or move < 0 ->
|
||||
safecast(pid_to_inform, {:make_move, game_id, :invalid_move})
|
||||
state
|
||||
true ->
|
||||
@@ -152,12 +162,13 @@ defmodule Server do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Tries to propose to paxos the game action
|
||||
defp try_to_play(state, game_id, move, pid_to_inform) do
|
||||
name = state.name
|
||||
|
||||
new_hand = if state.games[game_id].hand[state.name] == :- do
|
||||
state.games[game_id].game_state[move]
|
||||
Enum.at(state.games[game_id].game_state, move)
|
||||
else
|
||||
get_hand_for_game_state(state.games[game_id].game_state)
|
||||
end
|
||||
@@ -193,7 +204,8 @@ defmodule Server do
|
||||
try_to_play(state, game_id, move, pid_to_inform)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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 ->
|
||||
@@ -221,7 +233,8 @@ defmodule Server do
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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
|
||||
@@ -235,7 +248,8 @@ defmodule Server do
|
||||
Enum.random(mn..mx)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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)
|
||||
@@ -257,14 +271,16 @@ defmodule Server do
|
||||
#
|
||||
# Utils
|
||||
#
|
||||
|
||||
|
||||
# Checks if a game has been finished
|
||||
defp is_finished(state, game) do
|
||||
case state.games[game] do
|
||||
{:finished, _} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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
|
||||
@@ -272,7 +288,8 @@ defmodule Server do
|
||||
qurey_status(state)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 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})}
|
||||
@@ -283,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)
|
||||
@@ -295,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)
|
||||
@@ -313,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
|
||||
@@ -344,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
|
||||
@@ -356,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
|
||||
@@ -364,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)
|
||||
|
||||
@@ -380,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
|
||||
@@ -396,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)
|
||||
@@ -406,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 |>
|
||||
@@ -442,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
|
||||
@@ -475,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] ->
|
||||
@@ -502,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")
|
||||
@@ -529,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")
|
||||
@@ -555,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)
|
||||
@@ -563,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)
|
||||
|
||||
@@ -643,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()
|
||||
@@ -675,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")
|
||||
@@ -695,48 +754,55 @@ defmodule Client do
|
||||
Process.sleep(1000)
|
||||
display_game(process, game_id, game_state, hand)
|
||||
else
|
||||
Process.sleep(1000)
|
||||
display_game(process, game_id, temp_game, temp_hand)
|
||||
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" |>
|
||||
|
||||
@@ -66,25 +66,25 @@ test_suite = [
|
||||
|
||||
# Aditional Test functions
|
||||
|
||||
{&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
"Leader crashes right before decision, no concurrent ballots, 5 nodes"},
|
||||
{&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_local_config(5), 10,
|
||||
"Leader crashes right before decision, no concurrent ballots, 5 local procs"},
|
||||
|
||||
{&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
"Non-Leader proposes after leader is elected, 5 nodes"},
|
||||
{&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_local_config(5), 10,
|
||||
"Non-Leader proposes after leader is elected, 5 local procs"},
|
||||
|
||||
{&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
"Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
|
||||
{&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
|
||||
"Leader should nack before decision and then come to decision, 5 local procs"},
|
||||
|
||||
{&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
"Non-Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
|
||||
{&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
|
||||
"Non-Leader should nack before decision and then come to decision, 5 local procs"},
|
||||
# {&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
# "Leader crashes right before decision, no concurrent ballots, 5 nodes"},
|
||||
# {&PaxosTestAditional.run_leader_crash_simple_before_decision/3, TestUtil.get_local_config(5), 10,
|
||||
# "Leader crashes right before decision, no concurrent ballots, 5 local procs"},
|
||||
#
|
||||
# {&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
# "Non-Leader proposes after leader is elected, 5 nodes"},
|
||||
# {&PaxosTestAditional.run_non_leader_send_propose_after_leader_elected/3, TestUtil.get_local_config(5), 10,
|
||||
# "Non-Leader proposes after leader is elected, 5 local procs"},
|
||||
#
|
||||
# {&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
# "Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
|
||||
# {&PaxosTestAditional.run_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
|
||||
# "Leader should nack before decision and then come to decision, 5 local procs"},
|
||||
#
|
||||
# {&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_dist_config(host, 5), 10,
|
||||
# "Non-Leader should nack before decision and then come to decision, no concurrent ballots, 5 nodes"},
|
||||
# {&PaxosTestAditional.run_non_leader_should_nack_simple/3, TestUtil.get_local_config(5), 10,
|
||||
# "Non-Leader should nack before decision and then come to decision, 5 local procs"},
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user