diff --git a/backoff/__init__.py b/backoff/__init__.py index 196274d..eb7bfa6 100644 --- a/backoff/__init__.py +++ b/backoff/__init__.py @@ -14,13 +14,14 @@ """ from backoff._decorator import on_exception, on_predicate from backoff._jitter import full_jitter, random_jitter -from backoff._wait_gen import constant, expo, fibo, runtime +from backoff._wait_gen import constant, expo, fibo, runtime, decay __all__ = [ 'on_predicate', 'on_exception', 'constant', 'expo', + 'decay', 'fibo', 'runtime', 'full_jitter', diff --git a/backoff/_wait_gen.py b/backoff/_wait_gen.py index cc9c885..33b37b8 100644 --- a/backoff/_wait_gen.py +++ b/backoff/_wait_gen.py @@ -1,6 +1,7 @@ # coding:utf-8 import itertools +import math from typing import Any, Callable, Generator, Iterable, Optional, Union @@ -31,6 +32,35 @@ def expo( yield max_value +def decay( + initial_value: float = 1, + decay_factor: float = 1, + min_value: Optional[float] = None +) -> Generator[float, Any, None]: + + """Generator for exponential decay[1]: + + Args: + initial_value: initial quantity + decay_factor: exponential decay constant. + min_value: The minimum value to yield. Once the value in the + true exponential sequence is lower than this, the value + of min_value will forever after be yielded. + + [1] https://en.wikipedia.org/wiki/Exponential_decay + """ + # Advance past initial .send() call + yield # type: ignore[misc] + t = 0 + while True: + a = initial_value * math.e ** (-t * decay_factor) + if min_value is None or a > min_value: + yield a + t += 1 + else: + yield min_value + + def fibo(max_value: Optional[int] = None) -> Generator[int, None, None]: """Generator for fibonaccial decay. diff --git a/tests/test_wait_gen.py b/tests/test_wait_gen.py index b72482d..a658712 100644 --- a/tests/test_wait_gen.py +++ b/tests/test_wait_gen.py @@ -1,5 +1,34 @@ # coding:utf-8 import backoff +import math + + +def test_decay(): + gen = backoff.decay() + gen.send(None) + for i in range(10): + assert math.e ** -i == next(gen) + + +def test_decay_init100(): + gen = backoff.decay(initial_value=100) + gen.send(None) + for i in range(10): + assert 100 * math.e ** -i == next(gen) + + +def test_decay_init100_decay3(): + gen = backoff.decay(initial_value=100, decay_factor=3) + gen.send(None) + for i in range(10): + assert 100 * math.e ** (-i * 3) == next(gen) + + +def test_decay_init100_decay3_min5(): + gen = backoff.decay(initial_value=100, decay_factor=3, min_value=5) + gen.send(None) + for i in range(10): + assert max(100 * math.e ** (-i * 3), 5) == next(gen) def test_expo():