Packages
macula
0.44.0
7.1.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/macula_rpc_system/macula_rpc_failover.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% RPC Failover Strategy Module
%%%
%%% Encapsulates failover logic for RPC calls:
%%% - Determines if a call should be retried after failure
%%% - Manages provider exclusion list (failed providers)
%%% - Tracks attempt counts against max attempts
%%% - Provides retry decisions based on failure type
%%%
%%% Extracted from macula_rpc_handler.erl (Dec 2025) to improve testability
%%% and separation of concerns.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_rpc_failover).
%% API
-export([
new_context/4,
can_retry/1,
mark_provider_failed/2,
get_available_providers/1,
increment_attempt/1,
get_attempt_count/1,
should_failover/2
]).
%% Types
-export_type([failover_context/0, failure_reason/0]).
-type failure_reason() :: timeout | connection_error | provider_error | gateway_timeout.
-type failover_context() :: #{
procedure := binary(),
args := term(),
opts := map(),
providers := [map()],
excluded := [binary()],
attempt := pos_integer(),
max_attempts := pos_integer(),
tried_node_id => binary() | undefined
}.
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Create a new failover context for an RPC call.
%% Used when initiating a call that might need failover.
%% max_attempts is capped at provider count (can't try more providers than exist).
-spec new_context(binary(), term(), map(), [map()]) -> failover_context().
new_context(Procedure, Args, Opts, Providers) ->
ProviderCount = length(Providers),
RequestedMax = maps:get(max_attempts, Opts, 3),
MaxAttempts = min(RequestedMax, ProviderCount),
#{
procedure => Procedure,
args => Args,
opts => Opts,
providers => Providers,
excluded => [],
attempt => 1,
max_attempts => MaxAttempts
}.
%% @doc Check if the call can be retried based on current context.
%% Returns true if there are available providers and max attempts not exceeded.
-spec can_retry(failover_context()) -> boolean().
can_retry(#{attempt := Attempt, max_attempts := MaxAttempts}) when Attempt > MaxAttempts ->
false;
can_retry(Context) ->
case get_available_providers(Context) of
[] -> false;
_ -> true
end.
%% @doc Mark a provider as failed and add to exclusion list.
%% Returns updated context with provider added to excluded list.
-spec mark_provider_failed(binary() | undefined, failover_context()) -> failover_context().
mark_provider_failed(undefined, Context) ->
Context;
mark_provider_failed(NodeId, #{excluded := Excluded} = Context) ->
Context#{excluded := [NodeId | Excluded]}.
%% @doc Get list of providers that haven't been excluded.
%% Filters out all previously failed providers.
-spec get_available_providers(failover_context()) -> [map()].
get_available_providers(#{providers := Providers, excluded := []}) ->
Providers;
get_available_providers(#{providers := Providers, excluded := Excluded}) ->
lists:filter(fun(#{node_id := NodeId}) ->
not lists:member(NodeId, Excluded)
end, Providers).
%% @doc Increment the attempt counter.
%% Call this before each retry attempt.
-spec increment_attempt(failover_context()) -> failover_context().
increment_attempt(#{attempt := Attempt} = Context) ->
Context#{attempt := Attempt + 1}.
%% @doc Get current attempt count.
-spec get_attempt_count(failover_context()) -> pos_integer().
get_attempt_count(#{attempt := Attempt}) ->
Attempt.
%% @doc Determine if failover should be attempted for a given failure reason.
%% Some failures are not retryable (e.g., invalid procedure).
-spec should_failover(failure_reason(), failover_context()) -> boolean().
should_failover(timeout, Context) ->
can_retry(Context);
should_failover(connection_error, Context) ->
can_retry(Context);
should_failover(provider_error, Context) ->
can_retry(Context);
should_failover(gateway_timeout, _Context) ->
%% Gateway-direct calls have no failover
false.