Current section

Files

Jump to
launchdarkly_server_sdk src ldclient_backoff.erl
Raw

src/ldclient_backoff.erl

%%-------------------------------------------------------------------
%% @doc
%% @private
%% @end
%%-------------------------------------------------------------------
-module(ldclient_backoff).
%% API
-export([init/4, fail/1, succeed/1, fire/1]).
%% Types
-type backoff() :: #{
initial => non_neg_integer(),
current => non_neg_integer(),
max => non_neg_integer(),
attempt => non_neg_integer(),
active_since => integer() | undefined,
destination => pid(),
value => term()
}.
-define(JITTER_RATIO, 0.5).
%% Reset interval in seconds.
-define(RESET_INTERVAL, 60).
-export_type([backoff/0]).
%% Expose non-exported methods for tests.
-ifdef(TEST).
-compile(export_all).
-endif.
-spec init(Initial :: non_neg_integer(), Max :: non_neg_integer(), Destination :: pid(), Value :: term()) -> backoff().
init(Initial, Max, Destination, Value) ->
#{
initial => Initial,
current => Initial,
max => Max,
attempt => 0,
active_since => undefined,
destination => Destination,
value => Value
}.
%% @doc Get an updated backoff with updated delay. Does not start a timer automatically.
%% Use fire/1 to start a timer with the updated backoff.
%% @end
-spec fail(Backoff :: backoff()) -> backoff().
fail(#{active_since := undefined} = Backoff) ->
%% ActiveSince is undefined, so we have not had a successful connection since the last attempt.
update_backoff(Backoff, undefined);
fail(#{active_since := ActiveSince} = Backoff) ->
ActiveDuration = ldclient_time:time_seconds() - ActiveSince,
update_backoff(Backoff, ActiveDuration).
%% @doc Get an updated backoff with information about the successful connection attempt.
%% Does not clear any timers. Use the timer handle from fire/1 to cancel pending timers.
%% @end
-spec succeed(Backoff :: backoff()) -> backoff().
succeed(Backoff) ->
Backoff#{active_since => ldclient_time:time_seconds()}.
%% @doc Start a timer with the current delay.
%% @end
-spec fire(backoff()) -> Timer :: reference().
fire(#{current := Current, destination := Destination, value := Value}) ->
ldclient_time:start_timer(Current, Destination, Value).
%%===================================================================
%% Internal functions
%%===================================================================
-spec update_backoff(Backoff :: backoff(), ActiveSince :: integer() | undefined) -> backoff().
update_backoff(#{attempt := Attempt} = Backoff, undefined = _ActiveDuration) ->
%% There has not been a successful connection.
NewAttempt = Attempt + 1,
Backoff#{current => delay(NewAttempt, Backoff), attempt => NewAttempt};
update_backoff(Backoff, ActiveDuration) when ActiveDuration > ?RESET_INTERVAL ->
%% The last successful connection was active for more than 1 minute.
NewAttempt = 1,
Backoff#{current => delay(NewAttempt, Backoff), attempt => NewAttempt, active_since => undefined};
update_backoff(#{attempt := Attempt} = Backoff, _ActiveDuration) ->
%% The last successful connection was less than 1 minute,
NewAttempt = Attempt + 1,
%% Resetting ActiveSince here, otherwise after being disconnected for a minute it would
%% reset the the AttemptCount.
Backoff#{current => delay(NewAttempt, Backoff), attempt => NewAttempt, active_since => undefined}.
-spec delay(Attempt :: non_neg_integer(), Backoff :: backoff()) -> non_neg_integer().
delay(Attempt, #{initial := Initial, max := Max} = _Backoff) ->
jitter(min(backoff(Initial, Attempt), Max)).
-spec backoff(Initial :: non_neg_integer(), Attempt :: non_neg_integer()) -> non_neg_integer().
backoff(Initial, Attempt) ->
trunc(Initial * (math:pow(2, Attempt - 1))).
-spec jitter(Value :: non_neg_integer()) -> non_neg_integer().
jitter(Value) ->
trunc(Value - (rand:uniform() * ?JITTER_RATIO * Value)).