Skip to content
This repository has been archived by the owner on Aug 26, 2024. It is now read-only.

Simple raft implementation (raft-lite) #39

Merged
merged 9 commits into from
Apr 24, 2018
53 changes: 10 additions & 43 deletions lib/game/world/master.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ defmodule Game.World.Master do

@start_world Application.get_env(:ex_venture, :game)[:world]

def leader_selected() do
if @start_world do
GenServer.cast(__MODULE__, :start_zones)
end
end

def start_zone(pid, zone) do
GenServer.cast(pid, {:start, zone})
end
Expand All @@ -26,10 +32,6 @@ defmodule Game.World.Master do
:ok = :pg2.create(:world)
:ok = :pg2.join(:world, self())

if @start_world do
Process.send_after(self(), :elect, 5_000 + :rand.uniform(5_000))
end

{:ok, %{leader: nil}}
end

Expand All @@ -39,45 +41,10 @@ defmodule Game.World.Master do
{:noreply, state}
end

def handle_info(:elect, state) do
case state.leader do
nil ->
:world
|> :pg2.get_members()
|> Enum.each(fn pid ->
send(pid, {:leader, self()})
end)

Process.send_after(self(), :start_zones, 3_000)

_ ->
{:leader, :elected}
end

{:noreply, state}
end

def handle_info({:leader, pid}, state) do
case state.leader do
nil ->
Logger.info("Selecing a new leader #{inspect(self())}")

{:noreply, %{state | leader: pid}}

_ ->
{:noreply, state}
end
end

def handle_info(:start_zones, state) do
case state.leader == self() do
true ->
Logger.info("Starting zones")
start_zones()

false ->
:error
end
# This is started by the raft
def handle_cast(:start_zones, state) do
Logger.info("Starting zones")
start_zones()

{:noreply, state}
end
Expand Down
5 changes: 4 additions & 1 deletion lib/raft.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ defmodule Raft do

require Logger

@election_initial_delay 500
@election_random_delay 300

def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end

def start_election(term) do
Process.send_after(self(), {:election, :start, term}, 5_000 + :rand.uniform(5_000))
Process.send_after(self(), {:election, :start, term}, @election_initial_delay + :rand.uniform(@election_random_delay))
end

@doc """
Expand Down
61 changes: 45 additions & 16 deletions lib/raft/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ defmodule Raft.Server do

require Logger

@winner_subscriptions [Game.World.Master]

@doc """
Check for a leader already in the cluster
"""
@spec look_for_leader(State.t()) :: {:ok, State.t()}
def look_for_leader(state) do
Logger.debug("Checking for a current leader.")

Expand All @@ -17,6 +23,10 @@ defmodule Raft.Server do
{:ok, state}
end

@doc """
Reply to the leader check if the node is a leader
"""
@spec leader_check(State.t(), pid()) :: {:ok, State.t()}
def leader_check(state, pid) do
case state.state do
"leader" ->
Expand Down Expand Up @@ -91,6 +101,10 @@ defmodule Raft.Server do
Raft.new_leader(pid, term)
end)

Enum.map(@winner_subscriptions, fn module ->
module.leader_selected()
end)

{:ok, state} = set_leader(state, self(), node(), term)
{:ok, %{state | state: "leader"}}
else
Expand All @@ -108,27 +122,34 @@ defmodule Raft.Server do

@doc """
Set the winner as leader

TODO: check for term is newer
"""
def set_leader(state, leader_pid, leader_node, term) do
Logger.debug(fn ->
"Setting leader for term #{term} as #{inspect(leader_pid)}"
end)

state =
state
|> Map.put(:term, term)
|> Map.put(:highest_seen_term, term)
|> Map.put(:leader_pid, leader_pid)
|> Map.put(:leader_node, leader_node)
|> Map.put(:state, "follower")
|> Map.put(:votes, [])
|> Map.put(:voted_for, nil)
with {:ok, :newer} <- check_term_newer(state, term) do
Logger.debug(fn ->
"Setting leader for term #{term} as #{inspect(leader_pid)}"
end)

{:ok, state}
state =
state
|> Map.put(:term, term)
|> Map.put(:highest_seen_term, term)
|> Map.put(:leader_pid, leader_pid)
|> Map.put(:leader_node, leader_node)
|> Map.put(:state, "follower")
|> Map.put(:votes, [])
|> Map.put(:voted_for, nil)

{:ok, state}
else
_ ->
{:ok, state}
end
end

@doc """
A node went down, check if it was the leader
"""
@spec node_down(State.t(), atom()) :: {:ok, State.t()}
def node_down(state, node) do
case state.leader_node do
^node ->
Expand Down Expand Up @@ -159,6 +180,10 @@ defmodule Raft.Server do
{:ok, %{state | votes: [pid | state.votes]}}
end

@doc """
Check if the node has a majority of the votes
"""
@spec check_majority_votes(State.t()) :: {:ok, :majority} | {:error, :not_enough}
def check_majority_votes(state) do
case length(state.votes) >= length(PG.members()) / 2 do
true ->
Expand All @@ -169,6 +194,10 @@ defmodule Raft.Server do
end
end

@doc """
Check if the ndoe has voted in this term

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

node instead of ndoe?

"""
@spec check_voted(State.t()) :: {:ok, :not_voted} | {:error, :voted}
def check_voted(state) do
case state.voted_for do
nil ->
Expand Down