Current section

6 Versions

Jump to

Compare versions

4 files changed
+206 additions
-38 deletions
  @@ -1,20 +1,176 @@
1 - # Exbackoff
1 + # ExBackoff
2 2
3 - **TODO: Add description**
3 + [![Build Status](https://travis-ci.org/mingchuno/exbackoff.svg?branch=master)](https://travis-ci.org/mingchuno/exbackoff)
4 + [![Hex Version](http://img.shields.io/hexpm/v/exbackoff.svg)](https://hex.pm/packages/exbackoff)
5 + [![Inline docs](http://inch-ci.org/github/mingchuno/exbackoff.svg?branch=master)](http://inch-ci.org/github/mingchuno/exbackoff)
6 +
7 + ExBackoff is an Erlang library to deal with exponential backoffs and timers to
8 + be used within OTP processes when dealing with cyclical events, such as
9 + reconnections, or generally retrying things. This is port of Erlang counterpart
10 + in [ferd/backoff](https://github.com/ferd/backoff).
4 11
5 12 ## Installation
6 13
7 - If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
14 + [Available in Hex](https://hex.pm/packages/exbackoff), the package can be installed as:
8 15
9 16 1. Add exbackoff to your list of dependencies in `mix.exs`:
10 -
11 - def deps do
12 - [{:exbackoff, "~> 0.0.1"}]
13 - end
17 + ```
18 + [{:exbackoff, "~> 0.0.2"}]
19 + ```
14 20
15 21 2. Ensure exbackoff is started before your application:
22 + ```
23 + [applications: [:exbackoff]]
24 + ```
16 25
17 - def application do
18 - [applications: [:exbackoff]]
19 - end
20 26
27 + # Modes of Operation
28 +
29 + Backoff can be used in 3 main ways:
30 +
31 + 1. a simple way to calculate exponential backoffs
32 + 2. calculating exponential backoffs with caps and state tracking
33 + 3. using it to fire timeout events
34 +
35 + ## Simple Backoffs
36 +
37 + Simple backoffs work by calling the functions `increment/1-2`. The function
38 + with one argument will grow in an unbounded manner:
39 +
40 + 1> 1 |> ExBackoff.increment
41 + 2
42 + 2> 1 |> ExBackoff.increment |> ExBackoff.increment
43 + 4
44 + 3> 1 |> ExBackoff.increment |> ExBackoff.increment |> ExBackoff.increment
45 + 8
46 +
47 + The version with 2 arguments specifies a ceiling to the value:
48 +
49 + 4> 2 |> ExBackoff.increment |> ExBackoff.increment |> ExBackoff.increment
50 + 16
51 + 5> 2 |> ExBackoff.increment(10) |> ExBackoff.increment |> ExBackoff.increment
52 + 10
53 +
54 + ## Simple Backoffs with jitter
55 +
56 + 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:
57 +
58 + 1> 1 |> ExBackoff.rand_increment
59 + 3
60 + 2> 1 |> ExBackoff.rand_increment |> ExBackoff.rand_increment
61 + 7
62 + 3> 1 |> ExBackoff.rand_increment |> ExBackoff.rand_increment |> ExBackoff.rand_increment
63 + 19
64 + 4> 1 |> ExBackoff.rand_increment |> ExBackoff.rand_increment |> ExBackoff.rand_increment
65 + 14
66 + 5> 1 |> ExBackoff.rand_increment |> ExBackoff.rand_increment |> ExBackoff.rand_increment
67 + 17
68 +
69 + The version with 2 arguments specifies a ceiling to the value. If the
70 + delay is close to the ceiling the new delay will also be close to the
71 + ceiling and may be less than the previous delay.
72 +
73 + 6> 2 |> ExBackoff.rand_increment |> ExBackoff.rand_increment |> ExBackoff.rand_increment
74 + 21
75 + 7> 2 |> ExBackoff.rand_increment(10) |> ExBackoff.rand_increment |> ExBackoff.rand_increment
76 + 10
77 +
78 + ## State Backoffs
79 +
80 + State backoffs keep track of the current value, the initial value, and the
81 + maximal value for you. A backoff of that kind is initialized by calling
82 + `init(Start,Max)` and returns an opaque data type to be used with `get/1`
83 + (fetches the current timer value), `fail/1` (increments the value), and
84 + `succeed/1` (resets the value):
85 +
86 + 6> b0 = ExBackoff.init(2, 10)
87 + ...
88 + 7> {_, b1} = ExBackoff.fail(Bb)
89 + {4, ...}
90 + 8> ExBackoff.get(b1)
91 + 4
92 + 9> {_, b2} = ExBackoff.fail(b1)
93 + {8, ...}
94 + 10> {_, b3} = ExBackoff.fail(b2)
95 + {10, ...}
96 + 11> {_, _} = ExBackoff.fail(b3)
97 + {10, ...}
98 +
99 + And here we've hit the cap with the failures. Now to succeed again:
100 +
101 + 12> {_, b4} = ExBackoff.succeed(b3).
102 + {2, ...}
103 + 13> ExBackoff.get(b4)
104 + 2
105 +
106 + That way, backoffs carry all their relevant state.
107 +
108 + If what you want are unbound exponential backoffs, you can initiate them with:
109 +
110 + 14> ExBackoff.init(start, :infinity)
111 +
112 + And still use them as usual. The increments will have no upper limit.
113 +
114 + ## State Backoffs with jitter
115 +
116 + You can enable a jitter based incremental backoff by calling `type/2`
117 + that swaps the state of the backoff:
118 +
119 + 1> b0 = ExBackoff.init(2, 30)
120 + {:backoff,2,30,2,:normal,nil,nil}
121 + 2> b1 = ExBackoff.type(b0, jitter)
122 + {:backoff,2,30,2,:jitter,nil,nil}
123 + 3> {_, b2} = ExBackoff.fail(b1)
124 + {7, ...}
125 + 4> {_, b3} = ExBackoff.fail(b2)
126 + {12, ...}
127 +
128 + Calling `type/2` with argument `:normal` will swap the backoff state back
129 + to its default behavior:
130 +
131 + 5> b4 = ExBackoff.type(b3, :normal)
132 + {:backoff,2,30,12,:normal,nil,nil}
133 + 6> {_, b5} = ExBackoff.fail(b4)
134 + {24, ...}
135 +
136 + ## Timeout Events
137 +
138 + A very common usage for exponential backoffs are with timer events, to be used
139 + when driving reconnections or retries to certain sources. Most implementations
140 + of this will call `erlang:start_timer(Delay, Dest, Message)` to do this, and
141 + re-use the same values all the time.
142 +
143 + Given we want Backoff to carry us the whole way there, additional arguments can
144 + be given to the `init` function to deal with such state and fire events
145 + whenever necessary. We first initialize the backoff with `init(start, max,
146 + dest, message)`:
147 +
148 + 1> b = ExBackoff.init(5000, 20000, self(), :hello_world).
149 + ...
150 +
151 + Then by entering:
152 +
153 + 2> ExBackoff.fire(B). :timer.sleep(2500), flush(). :timer.sleep(3000), flush().
154 +
155 + and pressing enter, the following sequence of events will unfold:
156 +
157 + 3> ExBackoff.fire(B). :timer.sleep(2500), flush(). :timer.sleep(3000), flush().
158 + #Ref<0.0.0.719>
159 + 4> :timer.sleep(2500), flush(). :timer.sleep(3000), flush().
160 + ok
161 + 5> :timer.sleep(3000), flush().
162 + Shell got {timeout,#Ref<0.0.0.719>,hello_world}
163 + ok
164 +
165 + Showing that `ExBackoff.fire/1` generates a new timer, and returns the timer
166 + reference. This reference can be manipulated with `erlang:cancel_timer(Ref)`
167 + and `erlang:read_timer(Ref)`.
168 +
169 + The shell then sleeps (2000 ms), receives nothing, then sleeps some more (3000
170 + ms) and finally receives the timeout event as a regular Erlang timeout message.
171 +
172 + Do note that Backoff will *not* track the timer references given there can be
173 + enough use cases with multiple timers, event cancellation, and plenty of other
174 + things that can happen with them. Backoff makes it easy to fire them for
175 + the right interval, but *it is not* a wrapper around Erlang timers for all
176 + operations.
  @@ -9,4 +9,4 @@
9 9 {<<"maintainers">>,[<<"MC Or">>]}.
10 10 {<<"name">>,<<"exbackoff">>}.
11 11 {<<"requirements">>,[]}.
12 - {<<"version">>,<<"0.0.2">>}.
12 + {<<"version">>,<<"0.0.3">>}.
  @@ -1,6 +1,12 @@
1 - defmodule Exbackoff do
1 + defmodule ExBackoff do
2 2 use Bitwise
3 3
4 + @moduledoc """
5 + ExBackoff is an Elixir library to deal with exponential backoffs and timers
6 + to be used within OTP processes when dealing with cyclical events, such as
7 + reconnections, or generally retrying things.
8 + """
9 +
4 10 @typep max :: pos_integer | :infinity
5 11
6 12 defstruct [start: nil,
  @@ -9,6 +15,12 @@ defmodule Exbackoff do
9 15 type: :normal,
10 16 value: nil,
11 17 dest: nil]
18 +
19 + @typedoc """
20 + struct contain state of the backoff module including the start, current and
21 + max value. Type of the backoff `:normal` or `:jitter`. Value to send and the
22 + destination when need to fire off message whe :timeout
23 + """
12 24 @type backoff :: %__MODULE__{ start: pos_integer,
13 25 max: max,
14 26 current: pos_integer,
  @@ -22,6 +34,9 @@ defmodule Exbackoff do
22 34 @spec increment(pos_integer) :: pos_integer
23 35 def increment(n) when is_integer(n), do: n <<< 1
24 36
37 + @doc """
38 + Increments the value (but won't excess the max value).
39 + """
25 40 @spec increment(pos_integer, pos_integer) :: pos_integer
26 41 def increment(n, max), do: min(increment(n), max)
27 42
  @@ -39,6 +54,9 @@ defmodule Exbackoff do
39 54 n + :random.uniform(width + 1) - 1
40 55 end
41 56
57 + @doc """
58 + Do the random increments. It wont excess the max value
59 + """
42 60 @spec rand_increment(pos_integer, pos_integer) :: pos_integer
43 61 def rand_increment(n, max) do
44 62 # The largest interval for [0.5 * Time, 1.5 * Time] with maximum Max is
  @@ -69,7 +87,7 @@ defmodule Exbackoff do
69 87 """
70 88 @spec init(pos_integer, max, pid | nil, any | nil) :: backoff
71 89 def init(start, max, dest, value) do
72 - %Exbackoff{start: start, current: start, max: max, value: value, dest: dest}
90 + %ExBackoff{start: start, current: start, max: max, value: value, dest: dest}
73 91 end
74 92
75 93 @doc """
  @@ -78,7 +96,7 @@ defmodule Exbackoff do
78 96 is purely a convenience function.
79 97 """
80 98 @spec fire(backoff) :: reference
81 - def fire(%Exbackoff{current: delay, value: value, dest: dest}) do
99 + def fire(%ExBackoff{current: delay, value: value, dest: dest}) do
82 100 :erlang.start_timer(delay, dest, value)
83 101 end
84 102
  @@ -86,35 +104,41 @@ defmodule Exbackoff do
86 104 Reads the current backoff value.
87 105 """
88 106 @spec get(backoff) :: pos_integer
89 - def get(%Exbackoff{current: delay}), do: delay
107 + def get(%ExBackoff{current: delay}), do: delay
90 108
91 109 @doc """
92 110 Swaps between the states of the backoff.
93 111 """
94 112 @spec type(backoff, :normal | :jitter) :: backoff
95 - def type(b = %Exbackoff{}, :jitter), do: %{b | type: :jitter}
96 - def type(b = %Exbackoff{}, :normal), do: %{b | type: :normal}
113 + def type(b = %ExBackoff{}, :jitter), do: %{b | type: :jitter}
114 + def type(b = %ExBackoff{}, :normal), do: %{b | type: :normal}
97 115
116 + @doc """
117 + Increments the value and return the new state with the `new_delay`
118 + """
98 119 @spec fail(backoff) :: {pos_integer, backoff}
99 - def fail(b = %Exbackoff{current: delay, max: :infinity, type: :normal}) do
120 + def fail(b = %ExBackoff{current: delay, max: :infinity, type: :normal}) do
100 121 new_delay = increment(delay)
101 122 {new_delay, %{b | current: new_delay}}
102 123 end
103 - def fail(b = %Exbackoff{current: delay, max: max, type: :normal}) do
124 + def fail(b = %ExBackoff{current: delay, max: max, type: :normal}) do
104 125 new_delay = increment(delay, max)
105 126 {new_delay, %{b | current: new_delay}}
106 127 end
107 - def fail(b = %Exbackoff{current: delay, max: :infinity, type: :jitter}) do
128 + def fail(b = %ExBackoff{current: delay, max: :infinity, type: :jitter}) do
108 129 new_delay = rand_increment(delay)
109 130 {new_delay, %{b | current: new_delay}}
110 131 end
111 - def fail(b = %Exbackoff{current: delay, max: max, type: :jitter}) do
132 + def fail(b = %ExBackoff{current: delay, max: max, type: :jitter}) do
112 133 new_delay = rand_increment(delay, max)
113 134 {new_delay, %{b | current: new_delay}}
114 135 end
115 136
137 + @doc """
138 + resets the values
139 + """
116 140 @spec succeed(backoff) :: {pos_integer, backoff}
117 - def succeed(b = %Exbackoff{start: start}) do
141 + def succeed(b = %ExBackoff{start: start}) do
118 142 {start, %{b | current: start}}
119 143 end
120 144 end
  @@ -1,7 +1,7 @@
1 1 defmodule Exbackoff.Mixfile do
2 2 use Mix.Project
3 3
4 - @version "0.0.2"
4 + @version "0.0.3"
5 5
6 6 def project do
7 7 [app: :exbackoff,
  @@ -25,28 +25,16 @@ defmodule Exbackoff.Mixfile do
25 25 """
26 26 end
27 27
28 - # Configuration for the OTP application
29 - #
30 - # Type "mix help compile.app" for more information
31 28 def application do
32 29 [applications: [:logger]]
33 30 end
34 31
35 - # Dependencies can be Hex packages:
36 - #
37 - # {:mydep, "~> 0.3.0"}
38 - #
39 - # Or git/path repositories:
40 - #
41 - # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
42 - #
43 - # Type "mix help deps" for more examples and options
44 32 defp deps do
45 33 [
46 34 {:excheck, "~> 0.3.2", only: :test},
47 - {:earmark, "~> 0.1", only: :docs},
48 - {:ex_doc, "~> 0.10", only: :docs},
49 - {:inch_ex, "~> 0.4", only: :docs},
35 + {:earmark, "~> 0.2", only: :docs},
36 + {:ex_doc, "~> 0.11", only: :docs},
37 + {:inch_ex, "~> 0.5", only: :docs},
50 38 {:triq, github: "krestenkrab/triq", only: :test}
51 39 ]
52 40 end