Packages
reckon_db
5.2.2
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 ->
do_activate(Name, StoreId)
end.
do_activate(Name, StoreId) ->
%% Non-blocking: cast activation to the leader worker.
%% The worker handles save_default_subscriptions + activate sequentially
%% in its own process, avoiding timeout crashes in the node monitor.
gen_server:cast(Name, {do_activate, StoreId}),
ok.
%% @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};
%% Legacy — keep for backward compat if anything still calls it
handle_call({activate_sync, StoreId}, _From, State) ->
save_default_subscriptions(StoreId),
{reply, ok, 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({do_activate, StoreId}, State) ->
logger:info("Leader activation starting (store: ~p)", [StoreId]),
save_default_subscriptions(StoreId),
activate_leadership(StoreId, State);
handle_cast({activate, StoreId}, State) ->
activate_leadership(StoreId, State);
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 — activation
%%====================================================================
%% @private Activate leadership: start emitters and manage subscriptions.
activate_leadership(StoreId, State) ->
logger:info("Activating leadership responsibilities (store: ~p, node: ~p)",
[StoreId, node()]),
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_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}}.
%%====================================================================
%% 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.