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