Packages

macula

0.7.0
7.0.0 6.0.0 5.2.2 5.2.1 5.2.0 5.1.0 5.0.0 4.8.0 4.7.1 4.7.0 4.6.0 4.5.0 4.4.10 4.4.9 4.4.8 4.4.7 4.4.6 4.4.5 4.4.4 4.4.3 4.4.2 4.4.1 4.4.0 4.3.1 4.3.0 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.1 4.1.0 4.0.0 3.16.0 3.15.3 3.15.2 3.15.1 3.14.0 3.13.0 3.12.1 3.12.0 3.11.1 3.11.0 3.10.3 3.10.2 3.10.1 3.9.0 3.8.0 3.7.0 3.5.0 3.4.0 3.3.0 3.2.0 3.1.0 3.0.0 2.1.1 2.1.0 2.0.0 1.5.2 1.5.1 1.4.30 1.4.29 1.4.28 1.4.27 1.4.26 1.4.25 1.4.24 1.4.23 1.4.22 1.4.21 1.4.20 1.4.19 1.4.18 1.4.17 1.4.16 1.4.15 1.4.14 1.4.13 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.1 1.3.0 1.2.0 1.1.0 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.48.6 0.48.5 0.48.4 0.48.3 0.48.2 0.48.1 0.48.0 0.47.1 0.47.0 0.46.3 0.46.1 0.46.0 0.45.3 0.45.2 0.45.1 0.45.0 0.44.2 0.44.1 0.44.0 0.43.3 0.43.2 0.43.1 0.43.0 0.42.9 0.42.8 0.42.7 0.42.6 0.42.5 0.42.4 0.42.3 0.42.2 0.42.1 0.42.0 0.41.1 0.41.0 0.40.1 0.40.0 0.39.9 0.39.8 0.39.7 0.39.6 0.39.5 0.39.4 0.39.3 0.39.2 0.39.1 0.39.0 0.38.8 0.38.7 0.38.6 0.38.5 0.38.4 0.38.3 0.38.2 0.38.1 0.38.0 0.37.7 0.37.6 0.37.5 0.37.4 0.37.3 0.37.2 0.37.1 0.37.0 0.36.6 0.36.5 0.36.4 0.36.3 0.36.2 0.36.1 0.36.0 0.35.4 0.35.3 0.35.2 0.35.1 0.35.0 0.34.1 0.34.0 0.33.1 0.33.0 0.32.5 0.32.4 0.32.3 0.32.2 0.32.1 0.32.0 0.31.9 0.31.8 0.31.7 0.31.6 0.31.5 0.31.4 0.31.3 0.31.2 0.31.1 0.31.0 0.30.10 0.30.9 0.30.8 0.30.7 0.30.6 0.30.5 0.30.4 0.30.3 0.30.2 0.30.1 0.30.0 0.29.0 0.28.3 0.28.2 0.28.1 0.28.0 0.27.1 0.27.0 0.26.1 0.26.0 0.25.6 0.25.5 0.25.4 0.25.3 0.25.2 0.25.1 0.25.0 0.24.6 0.24.5 0.24.4 0.24.3 0.24.2 0.24.1 0.24.0 0.23.3 0.23.2 0.23.1 0.23.0 0.22.12 0.22.11 0.22.10 0.22.9 0.22.8 0.22.7 0.22.6 0.22.5 0.22.4 0.22.3 0.22.2 0.22.1 0.22.0 0.21.7 0.21.6 0.21.5 0.21.4 0.21.2 0.21.1 0.21.0 0.20.25 0.20.24 0.20.23 0.20.22 0.20.21 0.20.20 0.20.19 0.20.18 0.20.17 0.20.16 0.20.15 0.20.14 0.20.13 0.20.12 0.20.11 0.20.10 0.20.9 0.20.8 0.20.7 0.20.6 0.20.5 0.20.3 0.20.2 0.20.1 0.20.0 0.19.2 0.19.1 0.19.0 0.18.1 0.18.0 0.17.4 0.17.3 0.17.2 0.17.1 0.17.0 0.16.6 0.16.5 0.16.4 0.16.3 0.16.2 0.16.1 0.16.0 0.15.1 0.15.0 0.14.3 0.14.2 0.14.1 0.14.0 0.12.6 0.12.5 0.12.3 0.11.3 0.10.2 0.10.1 0.10.0 0.9.2 0.9.1 0.9.0 0.8.25 0.8.24 0.8.23 0.8.22 0.8.21 0.8.20 0.8.19 0.8.18 0.8.17 0.8.16 0.8.15 0.8.14 0.8.13 0.8.12 0.8.11 0.8.10 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.3 0.8.2 0.8.1 0.8.0 0.7.30 0.7.29 0.7.28 0.7.27 0.7.26 0.7.25 0.7.24 0.7.23 0.7.22 0.7.21 0.7.20 0.7.19 0.7.18 0.7.17 0.7.16 0.7.15 0.7.14 0.7.13 0.7.12 0.7.11 0.7.10 0.7.9 0.7.8 0.7.7 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.7 0.6.6 0.6.5 0.6.4 0.6.3 0.6.2 0.6.1 0.6.0 0.5.0 0.4.4 0.4.3 0.4.2 0.4.1 0.4.0 0.3.4 0.3.3 0.3.2 0.3.1

Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise

Current section

Files

Jump to
macula src macula_membership_detector.erl
Raw

src/macula_membership_detector.erl

%%%-------------------------------------------------------------------
%%% @doc
%%% SWIM failure detector (pure logic, no GenServer).
%%% Orchestrates member list, gossip, and protocol timing.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_membership_detector).
%% API
-export([
new/2,
local_node_id/1,
protocol_period/1,
add_member/2,
get_member/2,
get_alive_members/1,
select_probe_target/1,
mark_suspect/2,
mark_dead/2,
refute_suspicion/1,
get_gossip_updates/2,
apply_gossip_updates/2
]).
%% Types
-type detector_state() :: #{
local_node_id := binary(),
protocol_period := pos_integer(),
indirect_count := pos_integer(),
suspect_timeout := pos_integer(),
member_list := macula_membership_list:member_list(),
gossip := macula_membership_gossip:gossip_state()
}.
-export_type([detector_state/0]).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Create a new detector state.
-spec new(macula_membership_member:member(), map()) -> detector_state().
new(LocalMember, Config) ->
LocalNodeId = macula_membership_member:node_id(LocalMember),
#{
local_node_id => LocalNodeId,
protocol_period => maps:get(protocol_period, Config, 1000),
indirect_count => maps:get(indirect_count, Config, 3),
suspect_timeout => maps:get(suspect_timeout, Config, 5000),
member_list => macula_membership_list:new(LocalMember),
gossip => macula_membership_gossip:new()
}.
%% @doc Get local node ID.
-spec local_node_id(detector_state()) -> binary().
local_node_id(#{local_node_id := NodeId}) -> NodeId.
%% @doc Get protocol period.
-spec protocol_period(detector_state()) -> pos_integer().
protocol_period(#{protocol_period := Period}) -> Period.
%% @doc Add a member to the list.
-spec add_member(detector_state(), macula_membership_member:member()) -> detector_state().
add_member(#{member_list := List} = State, Member) ->
State#{member_list => macula_membership_list:add_member(List, Member)}.
%% @doc Get a member by node ID.
-spec get_member(detector_state(), binary()) -> {ok, macula_membership_member:member()} | not_found.
get_member(#{member_list := List}, NodeId) ->
macula_membership_list:get_member(List, NodeId).
%% @doc Get all alive members.
-spec get_alive_members(detector_state()) -> [macula_membership_member:member()].
get_alive_members(#{member_list := List}) ->
macula_membership_list:get_alive_members(List).
%% @doc Select a random member to probe (excluding self).
-spec select_probe_target(detector_state()) -> {ok, macula_membership_member:member(), detector_state()} | none.
select_probe_target(#{local_node_id := LocalNodeId, member_list := List} = State) ->
case macula_membership_list:get_random_members(List, 1, LocalNodeId) of
[Target] ->
{ok, Target, State};
[] ->
none
end.
%% @doc Mark a member as suspect.
-spec mark_suspect(detector_state(), binary()) -> detector_state().
mark_suspect(#{member_list := List, gossip := Gossip} = State, NodeId) ->
case macula_membership_list:get_member(List, NodeId) of
{ok, Member} ->
%% Update member status
Suspected = macula_membership_member:mark_suspect(Member),
List2 = macula_membership_list:update_member(List, Suspected),
%% Add to gossip
Status = macula_membership_member:status(Suspected),
Inc = macula_membership_member:incarnation(Suspected),
Gossip2 = macula_membership_gossip:add_update(Gossip, NodeId, Status, Inc),
State#{member_list => List2, gossip => Gossip2};
not_found ->
State
end.
%% @doc Mark a member as dead.
-spec mark_dead(detector_state(), binary()) -> detector_state().
mark_dead(#{member_list := List, gossip := Gossip} = State, NodeId) ->
case macula_membership_list:get_member(List, NodeId) of
{ok, Member} ->
%% Update member status
Dead = macula_membership_member:mark_dead(Member),
List2 = macula_membership_list:update_member(List, Dead),
%% Add to gossip
Status = macula_membership_member:status(Dead),
Inc = macula_membership_member:incarnation(Dead),
Gossip2 = macula_membership_gossip:add_update(Gossip, NodeId, Status, Inc),
State#{member_list => List2, gossip => Gossip2};
not_found ->
State
end.
%% @doc Refute suspicion (increment local incarnation).
-spec refute_suspicion(detector_state()) -> detector_state().
refute_suspicion(#{local_node_id := LocalNodeId, member_list := List, gossip := Gossip} = State) ->
case macula_membership_list:get_member(List, LocalNodeId) of
{ok, LocalMember} ->
%% Increment incarnation
CurrentInc = macula_membership_member:incarnation(LocalMember),
NewInc = CurrentInc + 1,
Refuted = macula_membership_member:mark_alive(LocalMember, NewInc),
%% Update in list
List2 = macula_membership_list:update_member(List, Refuted),
%% Add to gossip (broadcast refutation)
Gossip2 = macula_membership_gossip:add_update(Gossip, LocalNodeId, alive, NewInc),
State#{member_list => List2, gossip => Gossip2};
not_found ->
State
end.
%% @doc Get gossip updates to piggyback on messages.
-spec get_gossip_updates(detector_state(), pos_integer()) -> [macula_membership_gossip:update()].
get_gossip_updates(#{gossip := Gossip}, MaxUpdates) ->
macula_membership_gossip:get_updates(Gossip, MaxUpdates).
%% @doc Apply received gossip updates.
-spec apply_gossip_updates(detector_state(), [{binary(), macula_membership_member:status(), non_neg_integer()}]) ->
detector_state().
apply_gossip_updates(#{member_list := List, gossip := Gossip} = State, Updates) ->
%% Merge into gossip
Gossip2 = macula_membership_gossip:merge_updates(Gossip, Updates),
%% Apply to member list
List2 = lists:foldl(fun apply_gossip_update/2, List, Updates),
State#{member_list => List2, gossip => Gossip2}.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc Apply a gossip update to the member list.
apply_gossip_update({NodeId, Status, Incarnation}, AccList) ->
case macula_membership_list:get_member(AccList, NodeId) of
{ok, ExistingMember} ->
update_existing_member(AccList, ExistingMember, Status, Incarnation);
not_found ->
add_new_member(AccList, NodeId, Status, Incarnation)
end.
%% @doc Update existing member with new status.
update_existing_member(AccList, Member, alive, Incarnation) ->
Updated = macula_membership_member:mark_alive(Member, Incarnation),
macula_membership_list:update_member(AccList, Updated);
update_existing_member(AccList, Member, suspect, _Incarnation) ->
Updated = macula_membership_member:mark_suspect(Member),
macula_membership_list:update_member(AccList, Updated);
update_existing_member(AccList, Member, dead, _Incarnation) ->
Updated = macula_membership_member:mark_dead(Member),
macula_membership_list:update_member(AccList, Updated).
%% @doc Add new member to list (unless already dead).
add_new_member(AccList, _NodeId, dead, _Incarnation) ->
%% Don't add already-dead members
AccList;
add_new_member(AccList, NodeId, Status, Incarnation) ->
%% Create member with placeholder address (should come from discovery)
NewMember = macula_membership_member:new(NodeId, {{0,0,0,0}, 0}),
Updated = apply_initial_status(NewMember, Status, Incarnation),
macula_membership_list:add_member(AccList, Updated).
%% @doc Apply initial status to new member.
apply_initial_status(Member, alive, Incarnation) ->
macula_membership_member:mark_alive(Member, Incarnation);
apply_initial_status(Member, suspect, _Incarnation) ->
macula_membership_member:mark_suspect(Member).