├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── rebar.config ├── rebar.lock ├── src ├── backoff.app.src └── backoff.erl └── test ├── prop_backoff.erl └── prop_backoff_statem.erl /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | ci: 12 | name: Run checks and tests over ${{matrix.otp_vsn}} and ${{matrix.os}} 13 | runs-on: ${{matrix.os}} 14 | strategy: 15 | matrix: 16 | otp_vsn: [22, 23, 24] 17 | os: [ubuntu-latest] 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: ${{matrix.otp_vsn}} 23 | rebar3-version: '3.16' 24 | - run: rebar3 xref 25 | - run: rebar3 dialyzer 26 | - run: rebar3 as test proper 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | _build/ 3 | erl_crash.dump 4 | deps/ 5 | doc/*.html 6 | doc/erlang.png 7 | doc/stylesheet.css 8 | doc/edoc-info 9 | .eunit/ 10 | *.swp 11 | *~ 12 | .#* 13 | logs/ 14 | log/ 15 | test/*.beam 16 | .DS_Store 17 | .rebar3 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Heroku 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Backoff 2 | 3 | Backoff is an Erlang library to deal with exponential backoffs and timers to 4 | be used within OTP processes when dealing with cyclical events, such as 5 | reconnections, or generally retrying things. 6 | 7 | # Compiling 8 | 9 | rebar3 compile 10 | 11 | # Running Tests 12 | 13 | Tests are implemented as a basic PropEr property-based test suite. Running them 14 | requires getting PropEr for the project. The following command line does 15 | everything needed: 16 | 17 | $ rebar3 as test proper 18 | 19 | # Modes of Operation 20 | 21 | Backoff can be used in 3 main ways: 22 | 23 | 1. a simple way to calculate exponential backoffs 24 | 2. calculating exponential backoffs with caps and state tracking 25 | 3. using it to fire timeout events 26 | 27 | ## Simple Backoffs 28 | 29 | Simple backoffs work by calling the functions `increment/1-2`. The function 30 | with one argument will grow in an unbounded manner: 31 | 32 | 1> backoff:increment(1). 33 | 2 34 | 2> backoff:increment(backoff:increment(1)). 35 | 4 36 | 3> backoff:increment(backoff:increment(backoff:increment(1))). 37 | 8 38 | 39 | The version with 2 arguments specifies a ceiling to the value: 40 | 41 | 4> backoff:increment(backoff:increment(backoff:increment(2))). 42 | 16 43 | 5> backoff:increment(backoff:increment(backoff:increment(2)), 10). 44 | 10 45 | 46 | ## Simple Backoffs with jitter 47 | 48 | Jitter based incremental backoffs increase the back off period for each retry attempt using a randomization function that grows exponentially. They work by calling the functions `rand_increment/1-2`. The function with one argument will grow in an unbounded manner: 49 | 50 | 1> backoff:rand_increment(1). 51 | 3 52 | 2> backoff:rand_increment(backoff:rand_increment(1)). 53 | 7 54 | 3> backoff:rand_increment(backoff:rand_increment(backoff:rand_increment(1))). 55 | 19 56 | 4> backoff:rand_increment(backoff:rand_increment(backoff:rand_increment(1))). 57 | 14 58 | 5> backoff:rand_increment(backoff:rand_increment(backoff:rand_increment(1))). 59 | 17 60 | 61 | The version with 2 arguments specifies a ceiling to the value. If the 62 | delay is close to the ceiling the new delay will also be close to the 63 | ceiling and may be less than the previous delay. 64 | 65 | 6> backoff:rand_increment(backoff:rand_increment(backoff:rand_increment(2))). 66 | 21 67 | 7> backoff:rand_increment(backoff:rand_increment(backoff:rand_increment(2)), 10). 68 | 10 69 | 70 | ## State Backoffs 71 | 72 | State backoffs keep track of the current value, the initial value, and the 73 | maximal value for you. A backoff of that kind is initialized by calling 74 | `init(Start,Max)` and returns an opaque data type to be used with `get/1` 75 | (fetches the current timer value), `fail/1` (increments the value), and 76 | `succeed/1` (resets the value): 77 | 78 | 6> B0 = backoff:init(2, 10). 79 | ... 80 | 7> {_, B1} = backoff:fail(B0). 81 | {4, ...} 82 | 8> backoff:get(B1). 83 | 4 84 | 9> {_, B2} = backoff:fail(B1). 85 | {8, ...} 86 | 10> {_, B3} = backoff:fail(B2). 87 | {10, ...} 88 | 11> {_, _} = backoff:fail(B3). 89 | {10, ...} 90 | 91 | And here we've hit the cap with the failures. Now to succeed again: 92 | 93 | 12> {_, B4} = backoff:succeed(B3). 94 | {2, ...} 95 | 13> backoff:get(B4). 96 | 2 97 | 98 | That way, backoffs carry all their relevant state. 99 | 100 | If what you want are unbound exponential backoffs, you can initiate them with: 101 | 102 | 14> backoff:init(Start, 'infinity'). 103 | 104 | And still use them as usual. The increments will have no upper limit. 105 | 106 | ## State Backoffs with jitter 107 | 108 | You can enable a jitter based incremental backoff by calling `type/2` 109 | that swaps the state of the backoff: 110 | 111 | 1> B0 = backoff:init(2, 30). 112 | {backoff,2,30,2,normal,undefined,undefined} 113 | 2> B1 = backoff:type(B0, jitter). 114 | {backoff,2,30,2,jitter,undefined,undefined} 115 | 3> {_, B2} = backoff:fail(B1). 116 | {7, ...} 117 | 4> {_, B3} = backoff:fail(B2). 118 | {12, ...} 119 | 120 | Calling `type/2` with argument `normal` will swap the backoff state back 121 | to its default behavior: 122 | 123 | 5> B4 = backoff:type(B3, normal). 124 | {backoff,2,30,12,normal,undefined,undefined} 125 | 6> {_, B5} = backoff:fail(B4). 126 | {24, ...} 127 | 128 | ## Timeout Events 129 | 130 | A very common usage for exponential backoffs are with timer events, to be used 131 | when driving reconnections or retries to certain sources. Most implementations 132 | of this will call `erlang:start_timer(Delay, Dest, Message)` to do this, and 133 | re-use the same values all the time. 134 | 135 | Given we want Backoff to carry us the whole way there, additional arguments can 136 | be given to the `init` function to deal with such state and fire events 137 | whenever necessary. We first initialize the backoff with `init(Start, Max, 138 | Dest, Message)`: 139 | 140 | 1> B = backoff:init(5000, 20000, self(), hello_world). 141 | ... 142 | 143 | Then by entering: 144 | 145 | 2> backoff:fire(B). timer:sleep(2500), flush(). timer:sleep(3000), flush(). 146 | 147 | and pressing enter, the following sequence of events will unfold: 148 | 149 | 3> backoff:fire(B). timer:sleep(2500), flush(). timer:sleep(3000), flush(). 150 | #Ref<0.0.0.719> 151 | 4> timer:sleep(2500), flush(). timer:sleep(3000), flush(). 152 | ok 153 | 5> timer:sleep(3000), flush(). 154 | Shell got {timeout,#Ref<0.0.0.719>,hello_world} 155 | ok 156 | 157 | Showing that `backoff:fire/1` generates a new timer, and returns the timer 158 | reference. This reference can be manipulated with `erlang:cancel_timer(Ref)` 159 | and `erlang:read_timer(Ref)`. 160 | 161 | The shell then sleeps (2000 ms), receives nothing, then sleeps some more (3000 162 | ms) and finally receives the timeout event as a regular Erlang timeout message. 163 | 164 | Do note that Backoff will *not* track the timer references given there can be 165 | enough use cases with multiple timers, event cancellation, and plenty of other 166 | things that can happen with them. Backoff makes it easy to fire them for 167 | the right interval, but *it is not* a wrapper around Erlang timers for all 168 | operations. 169 | 170 | # Changelog 171 | 172 | - 1.1.6: fix compile regexes since darwin version 17.5 would be confused with OTP 17.x 173 | - 1.1.5: move `proper` plugin to test profile to avoid build warnings on newer Erlangs 174 | - 1.1.4: fix dialyzer warnings, update doc 175 | - 1.1.3: switch to package version of PropEr plugin to avoid mix conflicts 176 | - 1.1.2: eliminate compilation warnings 177 | - 1.1.1: corrections to incremental backoff 178 | - 1.1.0: added jitter based incremental backoff 179 | - 1.0.0: initial commit stable for over a year 180 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [ 2 | {platform_define, "^(R12|R13|R14|R15|R16|17)", 'OLD_RANDOM'} 3 | ]}. 4 | 5 | {profiles, [ 6 | {test, [ 7 | {plugins, [ 8 | {rebar3_proper, "0.9.0"} 9 | ]}, 10 | {deps, [ 11 | {proper, "1.2.0"} 12 | ]} 13 | ]} 14 | ]}. 15 | 16 | {xref_checks, [ 17 | deprecated_function_calls, 18 | locals_not_used, 19 | undefined_function_calls 20 | ]}. 21 | 22 | {dialyzer, [ 23 | {warnings, [ 24 | error_handling, 25 | underspecs, 26 | unknown, 27 | unmatched_returns 28 | ]} 29 | ]}. 30 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /src/backoff.app.src: -------------------------------------------------------------------------------- 1 | {application, backoff, 2 | [{description, "Exponential backoffs library"}, 3 | {vsn, "1.1.6"}, 4 | {applications, [stdlib, kernel]}, 5 | {registered, []}, 6 | 7 | {licenses, ["MIT"]}, 8 | {links, [{"Github", "https://github.com/ferd/backoff"}]}, 9 | {maintainers, ["Fred Hebert"]} 10 | ]}. 11 | -------------------------------------------------------------------------------- /src/backoff.erl: -------------------------------------------------------------------------------- 1 | -module(backoff). 2 | -export([increment/1, increment/2]). 3 | -export([rand_increment/1, rand_increment/2]). 4 | -export([type/2]). 5 | -export([init/2, init/4, 6 | fire/1, get/1, succeed/1, fail/1]). 7 | 8 | -record(backoff, {start :: pos_integer(), 9 | max :: pos_integer() | infinity, 10 | current :: pos_integer(), 11 | type=normal :: normal | jitter, 12 | value :: term(), 13 | dest :: pid() | undefined }). 14 | 15 | -opaque backoff() :: #backoff{}. 16 | 17 | -export_type([backoff/0]). 18 | 19 | 20 | -ifdef(OLD_RANDOM). 21 | -define(random, random). 22 | -else. 23 | -define(random, rand). 24 | -endif. 25 | 26 | %% @doc Increment an integer exponentially 27 | -spec increment(pos_integer()) -> pos_integer(). 28 | increment(N) when is_integer(N) -> N bsl 1. 29 | 30 | %% @doc Increment an integer exponentially within a range 31 | -spec increment(N, Max) -> pos_integer() when 32 | N :: pos_integer(), 33 | Max :: pos_integer(). 34 | increment(N, Max) -> min(increment(N), Max). 35 | 36 | %% @doc Increment an integer exponentially with randomness or jitter 37 | %% Chooses a delay uniformly from `[0.5 * Time, 1.5 * Time]' as recommended in: 38 | %% Sally Floyd and Van Jacobson, The Synchronization of Periodic Routing Messages, 39 | %% April 1994 IEEE/ACM Transactions on Networking. 40 | %% http://ee.lbl.gov/papers/sync_94.pdf 41 | -spec rand_increment(pos_integer()) -> pos_integer(). 42 | rand_increment(N) -> 43 | %% New delay chosen from [N, 3N], i.e. [0.5 * 2N, 1.5 * 2N] 44 | Width = N bsl 1, 45 | N + ?random:uniform(Width + 1) - 1. 46 | 47 | %% @doc Increment an integer with exponentially randomness 48 | %% or jitter within a range 49 | %% Chooses a delay uniformly from `[0.5 * Time, 1.5 * Time]' as recommended in: 50 | %% Sally Floyd and Van Jacobson, The Synchronization of Periodic Routing Messages, 51 | %% April 1994 IEEE/ACM Transactions on Networking. 52 | %% http://ee.lbl.gov/papers/sync_94.pdf 53 | -spec rand_increment(N, Max) -> pos_integer() when 54 | N :: pos_integer(), 55 | Max :: pos_integer(). 56 | rand_increment(N, Max) -> 57 | %% The largest interval for [0.5 * Time, 1.5 * Time] with maximum Max is 58 | %% [Max div 3, Max]. 59 | MaxMinDelay = Max div 3, 60 | if 61 | MaxMinDelay =:= 0 -> 62 | ?random:uniform(Max); 63 | N > MaxMinDelay -> 64 | rand_increment(MaxMinDelay); 65 | true -> 66 | rand_increment(N) 67 | end. 68 | 69 | %% Increments + Timer support 70 | 71 | %% @doc init function to be used when the user doesn't feel like using a timer 72 | %% provided by this library 73 | -spec init(Start, Max) -> backoff() when 74 | Start :: pos_integer(), 75 | Max :: pos_integer() | infinity. 76 | init(Start,Max) -> 77 | init(Start, Max, undefined, undefined). 78 | 79 | %% @doc init function when the user feels like using a timer 80 | %% provided by this library 81 | -spec init(Start, Max, Dest, Value) -> backoff() when 82 | Start :: pos_integer(), 83 | Max :: pos_integer() | infinity, 84 | Value :: term() | undefined, 85 | Dest :: pid() | undefined. 86 | init(Start, Max, Dest, Value) -> 87 | #backoff{start=Start, current=Start, max=Max, value=Value, dest=Dest}. 88 | 89 | %% @doc Starts a timer from the `backoff()' argument, using `erlang:start_timer/3'. 90 | %% No reference tracking is done, and this is left to the user. This function 91 | %% is purely a convenience function. 92 | -spec fire(backoff()) -> Timer::reference(). 93 | fire(#backoff{current=Delay, value=Value, dest=Dest}) -> 94 | erlang:start_timer(Delay, Dest, Value). 95 | 96 | %% @doc Reads the current backoff value 97 | -spec get(backoff()) -> pos_integer(). 98 | get(#backoff{current=Delay}) -> Delay. 99 | 100 | %% @doc Swaps between the states of the backoff, going between either 101 | %% `normal' or `jitter' modes. 102 | -spec type(backoff(), normal | jitter) -> backoff(). 103 | type(#backoff{}=B, jitter) -> 104 | maybe_seed(), 105 | B#backoff{type=jitter}; 106 | type(#backoff{}=B, normal) -> 107 | B#backoff{type=normal}. 108 | 109 | %% @doc Mark an attempt as failed, which increments the backoff value 110 | %% for the next round. 111 | -spec fail(backoff()) -> {New::pos_integer(), backoff()}. 112 | fail(B=#backoff{current=Delay, max=infinity, type=normal}) -> 113 | NewDelay = increment(Delay), 114 | {NewDelay, B#backoff{current=NewDelay}}; 115 | fail(B=#backoff{current=Delay, max=Max, type=normal}) -> 116 | NewDelay = increment(Delay, Max), 117 | {NewDelay, B#backoff{current=NewDelay}}; 118 | fail(B=#backoff{current=Delay, max=infinity, type=jitter}) -> 119 | NewDelay = rand_increment(Delay), 120 | {NewDelay, B#backoff{current=NewDelay}}; 121 | fail(B=#backoff{current=Delay, max=Max, type=jitter}) -> 122 | NewDelay = rand_increment(Delay, Max), 123 | {NewDelay, B#backoff{current=NewDelay}}. 124 | 125 | %% @doc Mark an attempt as successful, which resets the backoff 126 | %% value for the next round. 127 | -spec succeed(backoff()) -> {New::pos_integer(), backoff()}. 128 | succeed(B=#backoff{start=Start}) -> 129 | {Start, B#backoff{current=Start}}. 130 | 131 | -ifdef(OLD_RANDOM). 132 | maybe_seed() -> 133 | case erlang:get(random_seed) of 134 | undefined -> random:seed(erlang:now()); 135 | {X,X,X} -> random:seed(erlang:now()); 136 | _ -> ok 137 | end. 138 | -else. 139 | maybe_seed() -> ok. 140 | -endif. 141 | -------------------------------------------------------------------------------- /test/prop_backoff.erl: -------------------------------------------------------------------------------- 1 | -module(prop_backoff). 2 | -include_lib("proper/include/proper.hrl"). 3 | 4 | %% Increment operations are always returning bigger 5 | %% and bigger values, assuming positive integers 6 | prop_increment_increases() -> 7 | ?FORALL(X, pos_integer(), 8 | backoff:increment(X) > X). 9 | 10 | %% increments should never go higher than the max 11 | %% value allowed. 12 | prop_increment_ceiled_increases() -> 13 | ?FORALL({X,Y}, backoff_range(), 14 | ?WHENFAIL(io:format("~p~n",[{X,Y,backoff:increment(X,Y)}]), 15 | backoff:increment(X,Y) =< Y 16 | andalso 17 | (backoff:increment(X,Y) > X 18 | orelse 19 | (backoff:increment(X,Y) =:= X andalso X =:= Y)) 20 | )). 21 | 22 | %% random increment operations are always returning bigger 23 | %% and bigger values, assuming positive integers 24 | prop_rand_increment_increases() -> 25 | ?FORALL(X, pos_integer(), 26 | begin 27 | Delay = backoff:rand_increment(X), 28 | ?WHENFAIL(io:format("~p~n",[{X,Delay}]), 29 | Delay >= X andalso X * 3 >= Delay) 30 | end). 31 | 32 | %% random increments should never go higher than the max 33 | %% value allowed. 34 | prop_rand_increment_ceiled_increases() -> 35 | ?FORALL({X,Y}, backoff_range(), 36 | begin 37 | Delay = backoff:rand_increment(X, Y), 38 | ?WHENFAIL(io:format("~p~n",[{X,Y,Delay}]), 39 | Delay =< Y andalso X * 3 >= Delay 40 | andalso 41 | (Delay >= X 42 | orelse 43 | (X > Y div 3 andalso Delay >= Y div 3 andalso Delay >= 1))) 44 | end). 45 | 46 | %% increments from an init value always go higher when unbound 47 | prop_fail_increases() -> 48 | ?FORALL(S0, backoff_infinity(), 49 | begin 50 | X = backoff:get(S0), 51 | {Y, S1} = backoff:fail(S0), 52 | Y = backoff:get(S1), 53 | {Z, S2} = backoff:fail(S1), 54 | Z = backoff:get(S2), 55 | Y > X andalso Z > Y 56 | end). 57 | 58 | %% increments with a max value keep growing until a fixed point 59 | %% when failing 60 | prop_fail_bounded_increase() -> 61 | ?FORALL(S0, backoff(), 62 | begin 63 | List = until_fixed_point(S0), 64 | [{X,_},{X,S1}|Rest] = lists:reverse(List), 65 | try 66 | lists:foldl(fun({N,_},Prev) when N > Prev -> N end, 67 | 0, 68 | lists:reverse([{X,S1}|Rest])), 69 | true 70 | catch 71 | _:_ -> false 72 | end 73 | end). 74 | 75 | %% Failing multiple times then succeeding brings the value back 76 | %% to its initial one. 77 | prop_succeed_reset() -> 78 | ?FORALL(S0, backoff(), 79 | begin 80 | X = backoff:get(S0), 81 | [{_,S1}|_] = lists:reverse(until_fixed_point(S0)), 82 | {Y, _} = backoff:succeed(S1), 83 | Y =:= X 84 | end). 85 | 86 | %% Backoffs started with the right arguments can be used to 87 | %% start timers. 88 | prop_fire_message() -> 89 | ?FORALL({S,Term}, backoff_with_timer(50), 90 | begin 91 | Ref = backoff:fire(S), 92 | receive 93 | {timeout, Ref, Term} -> true 94 | after 100 -> 95 | false 96 | end 97 | end). 98 | 99 | %% Helpers 100 | until_fixed_point(S) -> until_fixed_point(S, backoff:get(S)). 101 | 102 | until_fixed_point(S0, Prev) -> 103 | {Next, S1} = backoff:fail(S0), 104 | if Next =:= Prev -> [{Next,S1}]; 105 | Next > Prev -> [{Next,S1} | until_fixed_point(S1, Next)] 106 | end. 107 | 108 | prop_statem() -> 109 | Module = prop_backoff_statem, 110 | ?FORALL(Cmds, commands(Module), 111 | begin 112 | {History, State, Result} = run_commands(Module, Cmds), 113 | ?WHENFAIL( 114 | begin 115 | io:format("History~n~p~n", [History]), 116 | io:format("State~n~p~n", [State]), 117 | io:format("Result~n~p~n", [Result]) 118 | end, 119 | aggregate(command_names(Cmds), Result =:= ok)) 120 | end). 121 | 122 | %% Generators 123 | backoff() -> 124 | ?LET({Start,Max}, backoff_range(), backoff:init(Start,Max)). 125 | 126 | backoff_infinity() -> 127 | ?LET(Start, pos_integer(), backoff:init(Start,infinity)). 128 | 129 | backoff_with_timer(N) -> 130 | ?LET({{Start,Max},T}, {bound_backoff_range(N),term()}, 131 | {backoff:init(Start,Max,self(),T),T}). 132 | 133 | backoff_range() -> 134 | ?SUCHTHAT({X,Y}, {pos_integer(), pos_integer()}, 135 | X < Y). 136 | 137 | bound_backoff_range(N) -> 138 | ?SUCHTHAT({X,Y}, {pos_integer(), pos_integer()}, 139 | X < Y andalso Y < N). 140 | -------------------------------------------------------------------------------- /test/prop_backoff_statem.erl: -------------------------------------------------------------------------------- 1 | -module(prop_backoff_statem). 2 | -include_lib("proper/include/proper.hrl"). 3 | 4 | -export([initial_state/0]). 5 | -export([command/1]). 6 | -export([precondition/2]). 7 | -export([next_state/3]). 8 | -export([postcondition/3]). 9 | 10 | -record(state, {backoff, type=normal, delay, start, max}). 11 | 12 | init_args() -> 13 | ?SUCHTHAT([X,Y], [pos_integer(), oneof([pos_integer(), infinity])], 14 | X < Y). 15 | type() -> 16 | elements([normal, jitter]). 17 | 18 | initial_state() -> 19 | #state{}. 20 | 21 | command(#state{backoff=undefined}) -> 22 | {call, backoff, init, init_args()}; 23 | command(#state{backoff=B}) -> 24 | oneof([{call, backoff, type, [B, type()]}, 25 | {call, backoff, fail, [B]}, 26 | {call, backoff, succeed, [B]}, 27 | {call, backoff, get, [B]}]). 28 | 29 | precondition(#state{backoff=B}, {call, _, init, _}) -> 30 | B =:= undefined; 31 | precondition(#state{backoff=B}, _) -> 32 | B =/= undefined. 33 | 34 | next_state(State, B, {call, _, init, [Start, Max]}) -> 35 | State#state{backoff=B, delay= Start, start=Start, max=Max}; 36 | next_state(#state{start=Start} = State, Value, {call, _, succeed, _}) -> 37 | NewB = {call, erlang, element, [2, Value]}, 38 | State#state{backoff=NewB, delay=Start}; 39 | next_state(State, Value, {call, _, fail, _}) -> 40 | NewDelay = {call, erlang, element, [1, Value]}, 41 | NewB = {call, erlang, element, [2, Value]}, 42 | State#state{backoff=NewB, delay=NewDelay}; 43 | next_state(State, NewB, {call, _, type, [_, Type]}) -> 44 | State#state{backoff=NewB, type=Type}; 45 | next_state(State, _, {call, _, get, _}) -> 46 | State. 47 | 48 | postcondition(#state{start=Start}, {call, _, succeed, _}, {NewDelay, _}) -> 49 | NewDelay =:= Start; 50 | postcondition(#state{type=normal, delay=Delay, max=Max}, {call, _, fail, _}, 51 | {NewDelay, _}) -> 52 | (NewDelay > Delay andalso NewDelay =< Max) orelse 53 | (NewDelay =:= Delay andalso NewDelay =:= Max); 54 | postcondition(#state{type=jitter, delay=Delay, max=Max}, {call, _, fail, _}, 55 | {NewDelay, _}) -> 56 | (NewDelay >= Delay orelse 57 | (Delay > Max div 3 andalso NewDelay >= 1 andalso NewDelay >= Max div 3)) 58 | andalso NewDelay =< Max andalso Delay * 3 >= NewDelay; 59 | postcondition(#state{delay=Delay}, {call, _, get, _}, Result) -> 60 | Result =:= Delay; 61 | postcondition(_, _, _) -> 62 | true. 63 | --------------------------------------------------------------------------------