Packages
reckon_db
1.2.3
5.11.0
5.10.4
5.10.3
5.10.1
5.10.0
5.9.1
5.9.0
5.8.3
5.8.2
5.8.1
5.8.0
5.7.0
5.6.1
5.6.0
5.5.5
5.5.4
5.5.3
5.5.2
5.5.1
5.5.0
5.4.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.0.0
3.1.2
3.1.1
3.0.0
2.3.7
2.3.6
2.3.5
2.3.4
2.3.3
2.3.2
2.3.1
2.3.0
2.2.2
2.2.0
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.1
1.1.0
1.0.3
1.0.2
1.0.1
1.0.0
BEAM-native Event Store built on Khepri/Ra with Raft consensus. Event sourcing, persistent subscriptions, snapshots, and automatic cluster formation via UDP multicast discovery. Ships embedded Rust NIFs for 3-15x acceleration of crypto, hashing, compression, aggregation, filter matching, and grap...
Current section
Files
Jump to
Current section
Files
src/reckon_db_leader.erl
%% @doc Leader worker for reckon-db
%%
%% Handles leader responsibilities when this node is the Raft leader.
%%
%% Responsibilities:
%% - Save default subscriptions (like $all stream)
%% - Start emitter pools for active subscriptions
%% - Coordinate leader-specific tasks
%%
%% @author rgfaber
-module(reckon_db_leader).
-behaviour(gen_server).
-include("reckon_db.hrl").
-include("reckon_db_telemetry.hrl").
%% API
-export([start_link/1]).
-export([activate/1]).
-export([is_active/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-record(state, {
store_id :: atom(),
config :: store_config(),
active :: boolean()
}).
%%====================================================================
%% API
%%====================================================================
-spec start_link(store_config()) -> {ok, pid()} | {error, term()}.
start_link(#store_config{store_id = StoreId} = Config) ->
Name = reckon_db_naming:leader_name(StoreId),
gen_server:start_link({local, Name}, ?MODULE, Config, []).
%% @doc Activate leader responsibilities
%% Called when this node becomes the cluster leader.
-spec activate(atom()) -> ok | {error, term()}.
activate(StoreId) ->
Name = reckon_db_naming:leader_name(StoreId),
case whereis(Name) of
undefined ->
{error, not_started};
_Pid ->
%% First save default subscriptions synchronously
case gen_server:call(Name, {save_default_subscriptions, StoreId}, 10000) of
{ok, _} ->
%% Then activate leadership asynchronously
gen_server:cast(Name, {activate, StoreId}),
logger:info("Leader activated (store: ~p, node: ~p)", [StoreId, node()]),
ok;
{error, Reason} ->
logger:warning("Failed to activate leader: ~p (store: ~p)", [Reason, StoreId]),
{error, Reason}
end
end.
%% @doc Check if leader is currently active
-spec is_active(atom()) -> boolean().
is_active(StoreId) ->
Name = reckon_db_naming:leader_name(StoreId),
case whereis(Name) of
undefined -> false;
_Pid -> gen_server:call(Name, is_active)
end.
%%====================================================================
%% gen_server callbacks
%%====================================================================
init(#store_config{store_id = StoreId} = Config) ->
process_flag(trap_exit, true),
logger:info("Leader worker started (store: ~p)", [StoreId]),
State = #state{
store_id = StoreId,
config = Config,
active = false
},
{ok, State}.
handle_call({save_default_subscriptions, StoreId}, _From, State) ->
Result = save_default_subscriptions(StoreId),
{reply, {ok, Result}, State};
handle_call(is_active, _From, #state{active = Active} = State) ->
{reply, Active, State};
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast({activate, StoreId}, State) ->
logger:info("Activating leadership responsibilities (store: ~p, node: ~p)",
[StoreId, node()]),
%% Get all subscriptions
Subscriptions = get_subscriptions(StoreId),
SubscriptionCount = length(Subscriptions),
case SubscriptionCount of
0 ->
logger:info("No active subscriptions to manage (store: ~p)", [StoreId]);
N ->
logger:info("Managing ~p active subscriptions (store: ~p)", [N, StoreId]),
%% Start emitters for each subscription
start_emitters_for_subscriptions(StoreId, Subscriptions)
end,
telemetry:execute(
?CLUSTER_LEADER_ELECTED,
#{system_time => erlang:system_time(millisecond)},
#{store_id => StoreId, leader => node(),
subscription_count => SubscriptionCount}
),
logger:info("Leadership activation complete (store: ~p)", [StoreId]),
{noreply, State#state{active = true}};
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(Reason, #state{store_id = StoreId}) ->
logger:info("Leader worker terminating (store: ~p, reason: ~p)", [StoreId, Reason]),
ok.
%%====================================================================
%% Internal functions
%%====================================================================
%% @private Save default subscriptions
-spec save_default_subscriptions(atom()) -> ok | {error, term()}.
save_default_subscriptions(StoreId) ->
%% Create default $all subscription if it doesn't exist
case reckon_db_subscriptions:exists(StoreId, <<"all-events">>) of
false ->
logger:info("Creating default $all subscription (store: ~p)", [StoreId]),
reckon_db_subscriptions:subscribe(StoreId, by_stream, <<"$all">>,
<<"all-events">>, #{});
true ->
logger:debug("Default $all subscription already exists (store: ~p)", [StoreId]),
ok
end.
%% @private Get all subscriptions
-spec get_subscriptions(atom()) -> [subscription()].
get_subscriptions(StoreId) ->
case reckon_db_subscriptions:list(StoreId) of
{ok, Subscriptions} -> Subscriptions;
{error, _} -> []
end.
%% @private Start emitters for all subscriptions
-spec start_emitters_for_subscriptions(atom(), [subscription()]) -> ok.
start_emitters_for_subscriptions(StoreId, Subscriptions) ->
lists:foreach(
fun(Subscription) ->
start_emitter_for_subscription(StoreId, Subscription)
end,
Subscriptions
).
%% @private Start emitter for a single subscription
-spec start_emitter_for_subscription(atom(), subscription()) -> ok.
start_emitter_for_subscription(StoreId, #subscription{subscription_name = Name} = Subscription) ->
case reckon_db_emitter_pool:start_emitter(StoreId, Subscription) of
{ok, _Pid} ->
logger:debug("Started emitter pool for subscription: ~s (store: ~p)",
[Name, StoreId]);
{error, {already_started, _Pid}} ->
logger:debug("Emitter pool already running for subscription: ~s (store: ~p)",
[Name, StoreId]);
{error, Reason} ->
logger:warning("Failed to start emitter pool for ~s: ~p (store: ~p)",
[Name, Reason, StoreId])
end,
ok.