use Croma alias Croma.TypeGen, as: TG alias Croma.Result, as: R defmodule RaftedValue.Members do alias RaftedValue.{PidSet, LogEntry, LogIndex} use Croma.Struct, fields: [ leader: TG.nilable(Croma.Pid), all: PidSet, # replicated using raft logs (i.e. reproducible from logs) uncommitted_membership_change: TG.nilable(LogEntry), # replicated using raft logs (i.e. reproducible from logs) pending_leader_change: TG.nilable(Croma.Pid), ] defun new_for_lonely_leader() :: t do %__MODULE__{leader: self(), all: PidSet.put(PidSet.new(), self())} end defun other_members_list(%__MODULE__{all: all}) :: [pid] do PidSet.delete(all, self()) |> PidSet.to_list() end defun put_leader(m :: t, leader_or_nil :: nil | pid) :: t do # when resetting `leader`, `pending_leader_change` field should be discarded (if any) %__MODULE__{m | leader: leader_or_nil, pending_leader_change: nil} end defun start_adding_follower(%__MODULE__{all: all} = m, {_term, _index, :add_follower, new_follower} = entry) :: R.t(t) do reject_if_membership_changing(m, fn -> reject_if_leader_changing(m, fn -> if PidSet.member?(all, new_follower) do {:error, :already_joined} else %__MODULE__{m | all: PidSet.put(all, new_follower), uncommitted_membership_change: entry} |> R.pure() end end) end) end defun start_removing_follower(%__MODULE__{all: all} = m, {_term, _index, :remove_follower, old_follower} = entry) :: R.t(t) do reject_if_membership_changing(m, fn -> reject_if_leader_changing(m, fn -> cond do old_follower == self() -> {:error, :cannot_remove_leader} PidSet.member?(all, old_follower) -> %__MODULE__{m | all: PidSet.delete(all, old_follower), uncommitted_membership_change: entry} |> R.pure() true -> {:error, :not_member} end end) end) end defun start_replacing_leader(%__MODULE__{all: all} = m, new_leader :: nil | pid) :: R.t(t) do reject_if_membership_changing(m, fn -> cond do is_nil(new_leader) -> %__MODULE__{m | pending_leader_change: nil} |> R.pure() new_leader == self() -> {:error, :already_leader} PidSet.member?(all, new_leader) -> %__MODULE__{m | pending_leader_change: new_leader} |> R.pure() true -> {:error, :not_member} end end) end defp reject_if_membership_changing(%__MODULE__{uncommitted_membership_change: change}, f) do if change do {:error, :uncommitted_membership_change} else f.() end end defp reject_if_leader_changing(%__MODULE__{pending_leader_change: change}, f) do if change do {:error, :pending_leader_change} else f.() end end defun membership_change_committed(%__MODULE__{uncommitted_membership_change: change} = m, index :: LogIndex.t) :: t do case change do {_, i, _, _} when i <= index -> %__MODULE__{m | uncommitted_membership_change: nil} _ -> m end end defun force_remove_member(%__MODULE__{all: all, uncommitted_membership_change: mchange, pending_leader_change: lchange} = m, pid :: pid) :: t do mchange2 = case mchange do {_term, _index, :add_follower , ^pid} -> nil {_term, _index, :remove_follower, ^pid} -> nil _ -> mchange end %__MODULE__{m | all: PidSet.delete(all, pid), uncommitted_membership_change: mchange2, pending_leader_change: (if lchange == pid, do: nil, else: lchange), } end end