Current section

Files

Jump to
reckon_db src reckon_db_leader_tracker.erl
Raw

src/reckon_db_leader_tracker.erl

%% @doc Leader tracker for reckon-db
%%
%% Tracks subscriptions and coordinates with pg groups.
%%
%% Responsibilities:
%% - Observe subscription changes via tracker_group
%% - Start emitter pools when subscriptions are created (on leader)
%% - Stop emitter pools when subscriptions are deleted (on leader)
%% - Update emitter pools when subscriptions are modified (on leader)
%%
%% Since Khepri triggers execute on the leader node, this module
%% coordinates emitter lifecycle with subscription changes.
%%
%% @author rgfaber
-module(reckon_db_leader_tracker).
-behaviour(gen_server).
-include("reckon_db.hrl").
%% API
-export([start_link/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()
}).
%%====================================================================
%% API
%%====================================================================
-spec start_link(store_config()) -> {ok, pid()} | {error, term()}.
start_link(#store_config{store_id = StoreId} = Config) ->
Name = reckon_db_naming:leader_tracker_name(StoreId),
gen_server:start_link({local, Name}, ?MODULE, Config, []).
%%====================================================================
%% gen_server callbacks
%%====================================================================
init(#store_config{store_id = StoreId} = Config) ->
process_flag(trap_exit, true),
logger:info("Leader tracker started (store: ~p)", [StoreId]),
%% Setup subscription tracking via tracker_group
ok = reckon_db_subscriptions:setup_tracking(StoreId, self()),
State = #state{
store_id = StoreId,
config = Config
},
{ok, State}.
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
handle_cast(_Msg, State) ->
{noreply, State}.
%% Handle subscription created event
handle_info({feature_created, subscriptions, Data}, #state{store_id = StoreId} = State) ->
logger:debug("Subscription created: ~p (store: ~p)", [Data, StoreId]),
handle_subscription_created(StoreId, Data),
{noreply, State};
%% Handle subscription updated event
handle_info({feature_updated, subscriptions, Data}, #state{store_id = StoreId} = State) ->
logger:debug("Subscription updated: ~p (store: ~p)", [Data, StoreId]),
handle_subscription_updated(StoreId, Data),
{noreply, State};
%% Handle subscription deleted event
handle_info({feature_deleted, subscriptions, Data}, #state{store_id = StoreId} = State) ->
logger:debug("Subscription deleted: ~p (store: ~p)", [Data, StoreId]),
handle_subscription_deleted(StoreId, Data),
{noreply, State};
%% Handle EXIT from linked processes
handle_info({'EXIT', Pid, Reason}, #state{store_id = StoreId} = State) ->
logger:warning("Linked process ~p exited: ~p (store: ~p)", [Pid, Reason, StoreId]),
%% Leave tracker group on exit
reckon_db_tracker_group:leave(StoreId, subscriptions, self()),
{noreply, State};
handle_info(_Info, State) ->
{noreply, State}.
terminate(Reason, #state{store_id = StoreId}) ->
logger:info("Leader tracker terminating: ~p (store: ~p)", [Reason, StoreId]),
%% Leave tracker group
reckon_db_tracker_group:leave(StoreId, subscriptions, self()),
ok.
%%====================================================================
%% Internal functions
%%====================================================================
%% @private Handle subscription created
-spec handle_subscription_created(atom(), map() | subscription()) -> ok.
handle_subscription_created(StoreId, Data) ->
case reckon_db_store_coordinator:is_leader(StoreId) of
true ->
Subscription = format_subscription_data(Data),
start_emitter_pool(StoreId, Subscription);
false ->
ok
end.
%% @private Handle subscription updated
-spec handle_subscription_updated(atom(), map() | subscription()) -> ok.
handle_subscription_updated(StoreId, Data) ->
case reckon_db_store_coordinator:is_leader(StoreId) of
true ->
Subscription = format_subscription_data(Data),
update_emitter_pool(StoreId, Subscription);
false ->
ok
end.
%% @private Handle subscription deleted
-spec handle_subscription_deleted(atom(), map() | subscription()) -> ok.
handle_subscription_deleted(StoreId, Data) ->
case reckon_db_store_coordinator:is_leader(StoreId) of
true ->
Subscription = format_subscription_data(Data),
stop_emitter_pool(StoreId, Subscription);
false ->
ok
end.
%% @private Start emitter pool for subscription
-spec start_emitter_pool(atom(), subscription()) -> ok.
start_emitter_pool(StoreId, #subscription{subscription_name = Name} = Subscription) ->
case reckon_db_emitter_pool:start_emitter(StoreId, Subscription) of
{ok, _Pid} ->
logger:info("Started emitter pool for subscription: ~s (store: ~p)",
[Name, StoreId]);
{error, {already_started, _Pid}} ->
logger:debug("Emitter pool already running for: ~s (store: ~p)",
[Name, StoreId]);
{error, Reason} ->
logger:warning("Failed to start emitter pool for ~s: ~p (store: ~p)",
[Name, Reason, StoreId])
end,
ok.
%% @private Update emitter pool for subscription
-spec update_emitter_pool(atom(), subscription()) -> ok.
update_emitter_pool(StoreId, #subscription{subscription_name = Name} = Subscription) ->
case reckon_db_emitter_pool:update_emitter(StoreId, Subscription) of
ok ->
logger:debug("Updated emitter pool for: ~s (store: ~p)", [Name, StoreId]);
{error, Reason} ->
logger:warning("Failed to update emitter pool for ~s: ~p (store: ~p)",
[Name, Reason, StoreId])
end,
ok.
%% @private Stop emitter pool for subscription
-spec stop_emitter_pool(atom(), subscription()) -> ok.
stop_emitter_pool(StoreId, #subscription{subscription_name = Name} = Subscription) ->
case reckon_db_emitter_pool:stop_emitter(StoreId, Subscription) of
ok ->
logger:info("Stopped emitter pool for: ~s (store: ~p)", [Name, StoreId]);
{error, Reason} ->
logger:warning("Failed to stop emitter pool for ~s: ~p (store: ~p)",
[Name, Reason, StoreId])
end,
ok.
%% @private Format subscription data into a subscription record
-spec format_subscription_data(map() | subscription()) -> subscription().
format_subscription_data(#subscription{} = Sub) ->
Sub;
format_subscription_data(Data) when is_map(Data) ->
#subscription{
id = maps:get(id, Data, undefined),
type = maps:get(type, Data, by_stream),
selector = maps:get(selector, Data, undefined),
subscription_name = maps:get(subscription_name, Data,
maps:get(name, Data, <<"unknown">>)),
subscriber_pid = maps:get(subscriber_pid, Data,
maps:get(subscriber, Data, undefined)),
created_at = maps:get(created_at, Data, 0),
pool_size = maps:get(pool_size, Data, 1)
};
format_subscription_data(_) ->
#subscription{
type = by_stream,
subscription_name = <<"unknown">>,
selector = <<"unknown">>,
pool_size = 1
}.