Current section
Files
Jump to
Current section
Files
lib/squabble.ex
defmodule Squabble do
@moduledoc """
A simple implementation of the raft protocol
https://raft.github.io/
"""
use GenServer
alias Squabble.PG
alias Squabble.Server
alias Squabble.State
require Logger
@key :squabble
@election_initial_delay 500
@election_random_delay 500
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Check the state of the election, look for a current leader
"""
def leader_check(pid) do
GenServer.cast(pid, {:leader, :check, self()})
end
@doc """
Let the new follower know about the current term and who the leader is
"""
def notify_of_leader(pid, term) do
GenServer.cast(pid, {:leader, :notice, self(), node(), term})
end
@doc """
Announce a node as running for leader
"""
def announce_candidate(pid, term) do
GenServer.cast(pid, {:election, :running, self(), term})
end
@doc """
Vote for a node as the leader
"""
def vote_for(pid, term) do
GenServer.cast(pid, {:election, :cast_vote, self(), term})
end
@doc """
Set the node as the new leader for a term
"""
def new_leader(pid, term) do
GenServer.cast(pid, {:election, :winner, self(), node(), term})
end
@doc """
Get debug information out of the squabble server
"""
def debug() do
GenServer.call(Squabble, :debug)
end
@doc """
Check if the current node is the leader
"""
@spec node_is_leader?() :: boolean()
def node_is_leader?() do
case :ets.lookup(@key, :is_leader?) do
[{_, value}] when is_boolean(value) ->
value
_ ->
false
end
end
def init(opts) do
PG.join()
:ets.new(@key, [:set, :protected, :named_table])
:ets.insert(@key, {:is_leader?, false})
start_election(1)
:ok = :net_kernel.monitor_nodes(true)
size = Keyword.get(opts, :size, 1)
subscriptions = Keyword.get(opts, :subscriptions, [])
state = %State{
state: "candidate",
size: size,
subscriptions: subscriptions,
term: 0,
highest_seen_term: 0,
votes: []
}
{:ok, state, {:continue, {:election, :check}}}
end
def handle_continue({:election, :check}, state) do
{:ok, state} = Server.look_for_leader(state)
{:noreply, state}
end
def handle_call(:state, _from, state) do
{:reply, Map.put(state, :node, node()), state}
end
def handle_call(:debug, _from, state) do
debug = Server.debug(state)
{:reply, debug, state}
end
def handle_cast({:leader, :check, pid}, state) do
{:ok, state} = Server.leader_check(state, pid)
{:noreply, state}
end
def handle_cast({:leader, :notice, leader_pid, leader_node, term}, state) do
{:ok, state} = Server.set_leader(state, leader_pid, leader_node, term)
{:noreply, state}
end
def handle_cast({:election, :running, pid, term}, state) do
{:ok, state} = Server.vote_leader(state, pid, term)
{:noreply, state}
end
def handle_cast({:election, :cast_vote, pid, term}, state) do
{:ok, state} = Server.vote_received(state, pid, term)
{:noreply, state}
end
def handle_cast({:election, :winner, leader_pid, leader_node, term}, state) do
{:ok, state} = Server.set_leader(state, leader_pid, leader_node, term)
{:noreply, state}
end
def handle_info({:election, :start, term}, state) do
{:ok, state} = Server.start_election(state, term)
{:noreply, state}
end
def handle_info({:election, :check_election_status, term}, state) do
{:ok, state} = Server.check_election_status(state, term)
{:noreply, state}
end
def handle_info({:nodeup, _node}, state) do
Process.send_after(self(), :assert_leader, 300)
{:noreply, state}
end
def handle_info({:nodedown, node}, state) do
{:ok, state} = Server.node_down(state, node)
{:noreply, state}
end
def handle_info(:assert_leader, state) do
{:ok, state} = Server.assert_leader(state)
{:noreply, state}
end
def start_election(term) do
Process.send_after(
self(),
{:election, :start, term},
@election_initial_delay + :rand.uniform(@election_random_delay)
)
end
end