Current section
Files
Jump to
Current section
Files
src/macula_rpc_router.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% RPC call routing strategies.
%%% Selects which provider to use for a call (local or remote).
%%% @end
%%%-------------------------------------------------------------------
-module(macula_rpc_router).
%% API
-export([
select_provider/3,
select_provider_stateful/3,
select_provider_closest/3,
select_local/1,
select_remote_random/1,
new_state/1
]).
%% Types
-type strategy() :: local_first | round_robin | random | closest.
-type registration() :: macula_rpc_registry:registration().
-type provider_info() :: macula_rpc_discovery:provider_info().
-type router_state() :: #{
strategy := strategy(),
round_robin_index := non_neg_integer()
}.
-export_type([strategy/0, router_state/0]).
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Create new router state.
-spec new_state(strategy()) -> router_state().
new_state(Strategy) ->
#{
strategy => Strategy,
round_robin_index => 0
}.
%% @doc Select provider using stateless strategy.
%% For local_first, random, and closest strategies.
-spec select_provider(strategy(), [registration()], [provider_info()]) ->
{local, registration()} | {remote, provider_info()} | {error, no_provider}.
select_provider(local_first, LocalHandlers, RemoteProviders) ->
case select_local(LocalHandlers) of
{ok, Handler} -> {local, Handler};
not_found ->
case select_remote_random(RemoteProviders) of
{ok, Provider} -> {remote, Provider};
not_found -> {error, no_provider}
end
end;
select_provider(random, _LocalHandlers, RemoteProviders) ->
case select_remote_random(RemoteProviders) of
{ok, Provider} -> {remote, Provider};
not_found -> {error, no_provider}
end;
select_provider(round_robin, _LocalHandlers, _RemoteProviders) ->
%% Round robin requires state, use select_provider_stateful instead
{error, use_stateful_api};
select_provider(closest, _LocalHandlers, _RemoteProviders) ->
%% Closest requires local node ID, use select_provider_closest instead
{error, use_closest_api}.
%% @doc Select provider using stateful strategy (for round_robin).
-spec select_provider_stateful(router_state(), [registration()], [provider_info()]) ->
{{local, registration()} | {remote, provider_info()} | {error, no_provider}, router_state()}.
select_provider_stateful(#{strategy := round_robin, round_robin_index := Index} = State, _LocalHandlers, RemoteProviders) ->
case RemoteProviders of
[] ->
{{error, no_provider}, State};
_ ->
%% Select provider at current index
ProviderIndex = Index rem length(RemoteProviders),
Provider = lists:nth(ProviderIndex + 1, RemoteProviders),
%% Increment index for next call
NewState = State#{round_robin_index => Index + 1},
{{remote, Provider}, NewState}
end;
select_provider_stateful(#{strategy := Strategy} = State, LocalHandlers, RemoteProviders) ->
%% For non-stateful strategies, use stateless version
Result = select_provider(Strategy, LocalHandlers, RemoteProviders),
{Result, State}.
%% @doc Select provider using closest strategy (requires local node ID).
-spec select_provider_closest(binary(), [registration()], [provider_info()]) ->
{local, registration()} | {remote, provider_info()} | {error, no_provider}.
select_provider_closest(_LocalNodeId, LocalHandlers, RemoteProviders) ->
case select_local(LocalHandlers) of
{ok, Handler} -> {local, Handler};
not_found ->
case find_closest_provider(_LocalNodeId, RemoteProviders) of
{ok, Provider} -> {remote, Provider};
not_found -> {error, no_provider}
end
end.
%% @doc Select local handler (returns first one).
-spec select_local([registration()]) -> {ok, registration()} | not_found.
select_local([]) ->
not_found;
select_local([Handler | _Rest]) ->
{ok, Handler}.
%% @doc Select random remote provider.
-spec select_remote_random([provider_info()]) -> {ok, provider_info()} | not_found.
select_remote_random([]) ->
not_found;
select_remote_random(Providers) ->
Index = rand:uniform(length(Providers)),
Provider = lists:nth(Index, Providers),
{ok, Provider}.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc Find provider with smallest XOR distance to local node.
-spec find_closest_provider(binary(), [provider_info()]) -> {ok, provider_info()} | not_found.
find_closest_provider(_LocalNodeId, []) ->
not_found;
find_closest_provider(LocalNodeId, Providers) ->
%% Calculate XOR distances
ProvidersWithDistance = lists:map(
fun(Provider) ->
RemoteNodeId = maps:get(node_id, Provider),
Distance = xor_distance(LocalNodeId, RemoteNodeId),
{Distance, Provider}
end,
Providers
),
%% Sort by distance (ascending)
Sorted = lists:sort(ProvidersWithDistance),
%% Return closest
{_Distance, Closest} = hd(Sorted),
{ok, Closest}.
%% @doc Calculate XOR distance between two node IDs.
-spec xor_distance(binary(), binary()) -> non_neg_integer().
xor_distance(A, B) ->
<<AInt:256>> = A,
<<BInt:256>> = B,
AInt bxor BInt.