Current section

Files

Jump to
evoq src evoq_aggregate_registry.erl
Raw

src/evoq_aggregate_registry.erl

%% @doc pg-based registry for aggregate processes.
%%
%% Uses OTP pg module for process registration and lookup.
%% Aggregates register with a group key of {aggregate, AggregateId}.
%%
%% @author rgfaber
-module(evoq_aggregate_registry).
-behaviour(gen_server).
-include("evoq.hrl").
%% API
-export([start_link/0]).
-export([register/2, unregister/1, lookup/1, get_or_start/2, get_or_start/3]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-define(PG_SCOPE, evoq_pg).
%%====================================================================
%% API
%%====================================================================
%% @doc Start the aggregate registry.
-spec start_link() -> {ok, pid()} | {error, term()}.
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
%% @doc Register an aggregate process.
-spec register(binary(), pid()) -> ok.
register(AggregateId, Pid) ->
pg:join(?PG_SCOPE, {aggregate, AggregateId}, Pid).
%% @doc Unregister an aggregate process.
-spec unregister(binary()) -> ok.
unregister(AggregateId) ->
case lookup(AggregateId) of
{ok, Pid} ->
pg:leave(?PG_SCOPE, {aggregate, AggregateId}, Pid);
{error, not_found} ->
ok
end.
%% @doc Lookup an aggregate by ID.
-spec lookup(binary()) -> {ok, pid()} | {error, not_found}.
lookup(AggregateId) ->
case pg:get_members(?PG_SCOPE, {aggregate, AggregateId}) of
[] ->
{error, not_found};
Pids ->
find_alive_local(Pids)
end.
%% @private Find the first alive local PID from a list.
%% In a cluster, pg returns PIDs from all nodes. We only want local ones
%% since aggregate processes must run on the node that owns the store.
find_alive_local([]) ->
{error, not_found};
find_alive_local([Pid | Rest]) when node(Pid) =:= node() ->
case is_process_alive(Pid) of
true -> {ok, Pid};
false -> find_alive_local(Rest)
end;
find_alive_local([_ | Rest]) ->
find_alive_local(Rest).
%% @doc Get an existing aggregate or start a new one (uses env store_id).
%%
%% @deprecated Use get_or_start/3 with explicit store_id instead.
-spec get_or_start(atom(), binary()) -> {ok, pid()} | {error, term()}.
get_or_start(AggregateModule, AggregateId) ->
StoreId = application:get_env(evoq, store_id, default_store),
get_or_start(AggregateModule, AggregateId, StoreId).
%% @doc Get an existing aggregate or start a new one with explicit store_id.
-spec get_or_start(atom(), binary(), atom()) -> {ok, pid()} | {error, term()}.
get_or_start(AggregateModule, AggregateId, StoreId) ->
case lookup(AggregateId) of
{ok, Pid} ->
{ok, Pid};
{error, not_found} ->
start_aggregate(AggregateModule, AggregateId, StoreId)
end.
%%====================================================================
%% gen_server callbacks
%%====================================================================
%% @private
init([]) ->
%% Monitor pg for membership changes
{ok, #{}}.
%% @private
handle_call(_Request, _From, State) ->
{reply, {error, unknown_request}, State}.
%% @private
handle_cast(_Msg, State) ->
{noreply, State}.
%% @private
handle_info(_Info, State) ->
{noreply, State}.
%% @private
terminate(_Reason, _State) ->
ok.
%%====================================================================
%% Internal functions
%%====================================================================
%% @private
start_aggregate(AggregateModule, AggregateId, StoreId) ->
evoq_aggregates_sup:start_aggregate(AggregateModule, AggregateId, StoreId).