View Source OnceMore.DelayStreams (OnceMore v1.0.0)

Functions to produce or transform streams of delay values.

You can import the module for convenient access:

import OnceMore.DelayStreams

Common delay patterns can be composed using stream transformations:

# Exponential growth with 50ms initial delay, randomized, capped at 1 second, stop after 5 attempts
50
|> exponential_backoff()
|> randomize(0.2)
|> cap(1_000)
|> Stream.take(5)

# Linear growth with 100ms initial delay, with jitter, stop after 30 seconds
100
|> linear_backoff(50)
|> jitter()
|> expiry(30_000)

Infinite retries

Most functions in this module produce infinite sequences.

Always bound them with either Stream.take/2 or expiry/3 to prevent infinite retries:

# Limit by number of attempts
exponential_backoff() |> Stream.take(5)

# Limit by total time budget
constant_backoff(100) |> expiry(30_000)

Delay streams are not special!

Any Enumerable of non-negative integers can be used as delays:

 # Fixed delays with a list
 OnceMore.retry(fn -> ... end, &should_retry?/1, [100, 200, 300])

 # Increasing delays with a range
 OnceMore.retry(fn -> ... end, &should_retry?/1, 100..500//100)

The design and implementation of these delay streams are derived from ElixirRetry project made by Safwan Kamarrudin and other contributors.

Summary

Functions

Returns a stream that is the same as delays except that the delays never exceed max.

Returns a constant stream of delays.

Returns a delay stream that is the same as delays except it limits the total life span of the stream to time_budget.

Returns a stream of delays that increase exponentially.

Returns a stream in which each element of delays is randomly adjusted to a number between 1 and the original delay.

Returns a stream of delays that increase linearly.

Returns a stream in which each element of delays is randomly adjusted no more than proportion of the delay.

Types

factor()

@type factor() :: pos_integer() | float()

Functions

cap(delays, max)

Returns a stream that is the same as delays except that the delays never exceed max.

This allow capping the delay between attempts to some max value.

Examples

# Uncapped 
iex> 100 |> linear_backoff(100) |> Enum.take(5)
[100, 200, 300, 400, 500]
# Capped 
iex> 100 |> linear_backoff(100) |> cap(250) |> Enum.take(5)
[100, 200, 250, 250, 250]

constant_backoff(delay \\ 100)

@spec constant_backoff(OnceMore.delay()) :: OnceMore.delays()

Returns a constant stream of delays.

Examples

iex> Enum.take(constant_backoff(), 5)
[100, 100, 100, 100, 100]

iex> 250 |> constant_backoff() |> Enum.take(5)
[250, 250, 250, 250, 250]

expiry(delays, time_budget, min_delay \\ 100)

Returns a delay stream that is the same as delays except it limits the total life span of the stream to time_budget.

This calculation takes the execution time of the block being retried into account.

The execution of the code within the block will not be interrupted, so the total time of execution may run over the time_budget depending on how long a single try will take.

Optionally, you can specify a minimum delay so the smallest value doesn't go below the threshold.

Examples

100
|> constant_backoff()
|> expiry(500)
|> Stream.each(&Process.sleep/1)
|> Enum.sum()
500

exponential_backoff(initial_delay \\ 10, factor \\ 2)

@spec exponential_backoff(OnceMore.delay(), factor()) :: OnceMore.delays()

Returns a stream of delays that increase exponentially.

Resulting values are rounded with Kernel.round/1 in case floating point factor is used.

Examples

iex> exponential_backoff() |> Enum.take(5)
[10, 20, 40, 80, 160]

iex> 100 |> exponential_backoff(1.5) |> Enum.take(5)
[100, 150, 225, 338, 507]

jitter(delays)

@spec jitter(OnceMore.delays()) :: OnceMore.delays()

Returns a stream in which each element of delays is randomly adjusted to a number between 1 and the original delay.

Examples

# Without jitter
iex> 10 |> linear_backoff(10) |> Enum.take(5)
[10, 20, 30, 40, 50]

# With jitter
10 |> linear_backoff(10) |> jitter() |> Enum.take(5)
[8, 14, 28, 27, 15]

linear_backoff(initial_delay, factor)

@spec linear_backoff(OnceMore.delay(), factor()) :: OnceMore.delays()

Returns a stream of delays that increase linearly.

Resulting values are rounded with Kernel.round/1 in case floating point factor is used.

Examples

iex> 10 |> linear_backoff(10) |> Enum.take(5)
[10, 20, 30, 40, 50]

iex> 100 |> linear_backoff(50) |> Enum.take(5)
[100, 150, 200, 250, 300]

randomize(delays, proportion \\ 0.1)

@spec randomize(OnceMore.delays(), float()) :: OnceMore.delays()

Returns a stream in which each element of delays is randomly adjusted no more than proportion of the delay.

Examples

100 |> linear_backoff(50) |> randomize() |> Enum.take(5)
[102, 141, 203, 226, 272]

100 |> linear_backoff(50) |> randomize(0.5) |> Enum.take(5)
[130, 135, 106, 317, 191]