Current section
6 Versions
Jump to
Current section
6 Versions
Compare versions
4
files changed
+206
additions
-38
deletions
| @@ -1,20 +1,176 @@ | |
| 1 | - # Exbackoff |
| 1 | + # ExBackoff |
| 2 2 | |
| 3 | - **TODO: Add description** |
| 3 | + [](https://travis-ci.org/mingchuno/exbackoff) |
| 4 | + [](https://hex.pm/packages/exbackoff) |
| 5 | + [](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 |