pax_global_header00006660000000000000000000000064136531352020014512gustar00rootroot0000000000000052 comment=b8ca3e58074959e0bb5cf937752c39396e4fa8cd alexferl-justbackoff-b8ca3e5/000077500000000000000000000000001365313520200163075ustar00rootroot00000000000000alexferl-justbackoff-b8ca3e5/.gitignore000066400000000000000000000014231365313520200202770ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ venv/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ #Ipython Notebook .ipynb_checkpoints # PyCharm .idea alexferl-justbackoff-b8ca3e5/.pre-commit-config.yaml000066400000000000000000000002551365313520200225720ustar00rootroot00000000000000repos: - repo: local hooks: - id: black name: black stages: [commit] language: system entry: venv/bin/black -t py37 --exclude venv . types: [python] alexferl-justbackoff-b8ca3e5/.travis.yml000066400000000000000000000005201365313520200204150ustar00rootroot00000000000000language: python sudo: required dist: bionic python: - "3.6" - "3.7" - "3.8" cache: pip before_install: - python --version - pip install -U pip - pip install -U pytest - pip install -U pytest-cov - pip install -U codecov install: - pip install ".[test]" . script: pytest --cov=./justbackoff after_success: - codecov alexferl-justbackoff-b8ca3e5/LICENSE000066400000000000000000000020741365313520200173170ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2020 Alexandre Ferland Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. alexferl-justbackoff-b8ca3e5/Makefile000066400000000000000000000016361365313520200177550ustar00rootroot00000000000000.PHONY: help build clean update test lint VENV_NAME?=venv VENV_ACTIVATE=. $(VENV_NAME)/bin/activate PYTHON=${VENV_NAME}/bin/python3 .DEFAULT: help help: @echo "make build" @echo " prepare development environment, use only once" @echo "make clean" @echo " delete development environment" @echo "make update" @echo " update dependencies" @echo "make test" @echo " run tests" @echo "make lint" @echo " run black" build: make venv venv: $(VENV_NAME)/bin/activate $(VENV_NAME)/bin/activate: test -d $(VENV_NAME) || virtualenv -p python3 $(VENV_NAME) ${PYTHON} -m pip install -U pip ${PYTHON} -m pip install -r dev_requirements.txt $(VENV_NAME)/bin/pre-commit install touch $(VENV_NAME)/bin/activate clean: rm -rf venv update: ${PYTHON} -m pip install -r dev_requirements.txt test: venv ${PYTHON} -m pytest lint: venv $(VENV_NAME)/bin/black -t py37 --exclude $(VENV_NAME) . alexferl-justbackoff-b8ca3e5/README.md000066400000000000000000000041261365313520200175710ustar00rootroot00000000000000# justbackoff [![Build Status](https://travis-ci.org/admiralobvious/justbackoff.svg?branch=master)](https://travis-ci.org/admiralobvious/justbackoff) [![codecov](https://codecov.io/gh/admiralobvious/justbackoff/branch/master/graph/badge.svg)](https://codecov.io/gh/admiralobvious/justbackoff) A simple backoff algorithm for Python >3.6. ### Install ```shell script $ pip install justbackoff ``` ### Usage Backoff is a counter. It starts at `min_ms`. After every call to `duration()`, it is multiplied by `factor`. It is capped at `max_ms`. It returns to `min_ms` on every call to `reset()`. `jitter` adds randomness ([see below](#example-using-jitter)). --- #### Simple example ``` python from justbackoff import Backoff b = Backoff(min_ms=100, max_ms=10000, factor=2, jitter=False) print(b.duration()) print(b.duration()) print(b.duration()) print("Reset!") b.reset() print(b.duration()) ``` ``` shell script 0.1 0.2 0.4 Reset! 0.1 ``` --- #### Example using `socket` package ``` python import socket import time from justbackoff import Backoff sock = socket.socket() b = Backoff() while True: try: sock.connect(("127.0.0.1", 1337)) except Exception as e: d = b.duration() print("{}, reconnecting in {} seconds".format(e, d)) time.sleep(d) continue b.reset() sock.send("Hello, world!") sock.close() ``` --- #### Example using `jitter` Enabling `jitter` adds some randomization to the backoff durations. [See Amazon's writeup of performance gains using jitter](http://www.awsarchitectureblog.com/2015/03/backoff.html). Seeding is not necessary but doing so gives repeatable results. ```python import random from justbackoff import Backoff b = Backoff(min_ms=100, max_ms=10000, factor=2, jitter=True) random.seed(42) print(b.duration()) print(b.duration()) print(b.duration()) print("Reset!") b.reset() print(b.duration()) print(b.duration()) print(b.duration()) ``` ``` shell script 0.1 0.102501075522 0.182508795511 Reset! 0.1 0.173647121416 0.303009846227 ``` #### Credits Ported from Go [backoff](https://github.com/jpillora/backoff) alexferl-justbackoff-b8ca3e5/dev_requirements.txt000066400000000000000000000000571365313520200224330ustar00rootroot00000000000000black==19.10b0 pre-commit==2.3.0 pytest==5.4.1 alexferl-justbackoff-b8ca3e5/justbackoff/000077500000000000000000000000001365313520200206105ustar00rootroot00000000000000alexferl-justbackoff-b8ca3e5/justbackoff/__init__.py000066400000000000000000000066541365313520200227340ustar00rootroot00000000000000import random def to_seconds(milliseconds: float) -> float: """ :func:`to_seconds` Converts :param:`milliseconds` to seconds. :param milliseconds: number of milliseconds you to convert to seconds :type milliseconds: float, int :return: seconds :rtype: float """ return milliseconds / 1000.0 def to_ms(seconds: float) -> float: """ :func:`to_ms` Converts :param:`seconds` to milliseconds. :param seconds: number of seconds you want to convert to milliseconds :type seconds: float, int :return: milliseconds :rtype: float """ return seconds * 1000.0 class Backoff: def __init__( self, min_ms: float = 100.0, max_ms: float = 10000.0, factor: float = 2.0, jitter: bool = False, ): """ :class:`Backoff` is a counter. It starts at :attr:`min_ms`. After every call to :meth:`duration` it is multiplied by :attr:`factor`. It is capped at :attr:`max_ms`. It returns to :attr:`min_ms` on every call to :meth:`reset`. :class:`Backoff` is not thread-safe, but the :meth:`for_attempt` method can be used concurrently if non-zero values for :attr:`factor`, :attr:`max_ms`, and :attr:`min_ms` are set on the :class:`Backoff` shared among threads. :param min_ms: min_ms is the minimum value of the counter :param max_ms: max_ms is the maximum value of the counter :param factor: factor is the multiplying factor for each increment step :param bool jitter: jitter eases contention by randomizing backoff steps :type min_ms: float, int :type max_ms: float, int :type factor: float, int :type jitter: bool """ self.min_ms = float(min_ms) self.max_ms = float(max_ms) self.factor = float(factor) self.jitter = jitter self.cur_attempt = 0.0 def duration(self) -> float: """ Returns the current value of the counter and then multiplies it by :attr:`factor` :rtype: float """ d = self.for_attempt(self.cur_attempt) self.cur_attempt += 1 return d def for_attempt(self, attempt: float) -> float: """ :meth:`for_attempt` returns the duration for a specific attempt. This is useful if you have a large number of independent backoffs, but don't want to use unnecessary memory storing the backoff parameters per backoff. The first attempt should be 0. :meth:`for_attempt` is thread-safe if non-zero values for :attr:`factor`, :attr:`max_ms`, and :attr:`min_ms` are set before any calls to :meth:`for_attempt` are made. :param attempt: the attempt you want to return duration for :type attempt: float :return: duration in seconds :rtype: float """ dur = float(self.min_ms * pow(self.factor, attempt)) if self.jitter: dur = random.random() * (dur - self.min_ms) + self.min_ms if dur > self.max_ms: return to_seconds(self.max_ms) return to_seconds(dur) def reset(self): """ Resets the number of attempts to :attr:`min_ms`. """ self.cur_attempt = round(to_seconds(self.min_ms)) def attempt(self) -> float: """ Returns the current backoff attempt. :rtype: float """ return self.cur_attempt alexferl-justbackoff-b8ca3e5/setup.cfg000066400000000000000000000000261365313520200201260ustar00rootroot00000000000000[aliases] test=pytest alexferl-justbackoff-b8ca3e5/setup.py000066400000000000000000000016741365313520200200310ustar00rootroot00000000000000""" justbackoff ---------------- Simple backoff algorithm in Python """ from setuptools import setup setup( name="justbackoff", version="0.6.0", url="https://github.com/admiralobvious/justbackoff", license="MIT", author="Alexandre Ferland", author_email="aferlandqc@gmail.com", description="Simple backoff algorithm in Python", long_description=__doc__, packages=["justbackoff"], zip_safe=False, include_package_data=True, setup_requires=["pytest-runner>=5.2"], tests_require=["pytest>=5.4.1"], platforms="any", classifiers=[ "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Topic :: Software Development :: Libraries :: Python Modules", ], ) alexferl-justbackoff-b8ca3e5/tests/000077500000000000000000000000001365313520200174515ustar00rootroot00000000000000alexferl-justbackoff-b8ca3e5/tests/__init__.py000066400000000000000000000000001365313520200215500ustar00rootroot00000000000000alexferl-justbackoff-b8ca3e5/tests/test_backoff.py000066400000000000000000000054231365313520200224610ustar00rootroot00000000000000import unittest from justbackoff import Backoff, to_seconds class CustomAssertions: @staticmethod def assert_between(actual: float, low: float, high: float): if actual < low: raise AssertionError("Got {}, expecting >= {}".format(actual, low)) if actual > high: msg = "Got {}, expecting <= {}".format(actual, high) raise AssertionError(msg) class TestBackoff(unittest.TestCase, CustomAssertions): def setUp(self): self.b = Backoff(min_ms=100.0, max_ms=10000.0, factor=2.0) def test_defaults(self): self.assertEqual(self.b.duration(), to_seconds(100.0)) self.assertEqual(self.b.duration(), to_seconds(200.0)) self.assertEqual(self.b.duration(), to_seconds(400.0)) self.b.reset() self.assertEqual(self.b.duration(), to_seconds(100.0)) def test_factor(self): b = Backoff(min_ms=100, max_ms=10000, factor=1.5) self.assertEqual(b.duration(), to_seconds(100.0)) self.assertEqual(b.duration(), to_seconds(150.0)) self.assertEqual(b.duration(), to_seconds(225.0)) b.reset() self.assertEqual(b.duration(), to_seconds(100.0)) def test_for_attempt(self): self.assertEqual(self.b.for_attempt(0), to_seconds(100.0)) self.assertEqual(self.b.for_attempt(1), to_seconds(200.0)) self.assertEqual(self.b.for_attempt(2), to_seconds(400.0)) self.b.reset() self.assertEqual(self.b.for_attempt(0), to_seconds(100.0)) def test_get_attempt(self): self.assertEqual(self.b.attempt(), 0) self.assertEqual(self.b.duration(), to_seconds(100.0)) self.assertEqual(self.b.attempt(), 1) self.assertEqual(self.b.duration(), to_seconds(200.0)) self.assertEqual(self.b.attempt(), 2) self.assertEqual(self.b.duration(), to_seconds(400.0)) self.assertEqual(self.b.attempt(), 3) self.b.reset() self.assertEqual(self.b.attempt(), 0) self.assertEqual(self.b.duration(), to_seconds(100.0)) self.assertEqual(self.b.attempt(), 1) def test_jitter(self): b = Backoff(min_ms=100.0, max_ms=10000.0, factor=2.0, jitter=True) self.assertEqual(b.duration(), to_seconds(100.0)) self.assert_between(b.duration(), to_seconds(100.0), to_seconds(200.0)) self.assert_between(b.duration(), to_seconds(100.0), to_seconds(400.0)) b.reset() self.assertEqual(b.duration(), to_seconds(100.0)) def test_integers(self): b = Backoff(min_ms=100, max_ms=10000, factor=2) self.assertEqual(b.duration(), to_seconds(100.0)) self.assertEqual(b.duration(), to_seconds(200.0)) self.assertEqual(b.duration(), to_seconds(400.0)) b.reset() self.assertEqual(b.duration(), to_seconds(100.0))