Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2eb5e68
Changed time.sleep to condition.wait()
brookman1 Apr 30, 2020
227e224
fixed syntax, propagated condition, added comments.
brookman1 Apr 30, 2020
d75715d
Changed time.sleep to condition.wait()
brookman1 Apr 30, 2020
9bb1896
Merge branch 'master' of https://github.com/brookman1/retry
brookman1 Apr 30, 2020
c234dd1
New Workflow
brookman1 Apr 30, 2020
6293295
Changed pip install directions in document.
brookman1 Apr 30, 2020
3be1b7f
Fixing tests.
brookman1 Apr 30, 2020
d8b98d5
Update pythonapp.yml
brookman1 Apr 30, 2020
37b5bbf
revert the fix.
brookman1 Apr 30, 2020
928c86a
fixed syntax
brookman1 Apr 30, 2020
1df148e
testing threading.Condition instead of time.sleep
brookman1 Apr 30, 2020
8aa3566
updatin how mock_sleep_time scope works.
brookman1 Apr 30, 2020
ece2b69
update to run test.
brookman1 May 2, 2020
28c1b42
Update test_retry.py
brookman1 May 2, 2020
d71066b
Update test_retry.py
brookman1 May 2, 2020
3b040d8
Changed time.sleep to condition.wait()
brookman1 May 2, 2020
13b9e3f
Changed time.sleep to condition.wait()
brookman1 May 2, 2020
595228b
New Workflow
brookman1 Apr 30, 2020
921adaf
Changed pip install directions in document.
brookman1 Apr 30, 2020
1cc30ea
Fixing tests.
brookman1 Apr 30, 2020
3bf98e1
# This is a combination of 6 commits.
brookman1 Apr 30, 2020
69bc593
Update pythonapp.yml
brookman1 May 2, 2020
f6c6719
Merge branch 'master' of https://github.com/brookman1/retry
brookman1 May 2, 2020
7ffaaee
Changed time.sleep to condition.wait()
brookman1 Apr 30, 2020
2e8f32e
Merge branch 'master' of https://github.com/brookman1/retry
brookman1 May 2, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/pythonapp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Python application

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pip install git+https://github.com/brookman1/retry.git
pytest
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Installation

.. code-block:: bash

$ pip install retry
$ pip install git+https://github.com/brookman1/retry.git


API
Expand Down
28 changes: 20 additions & 8 deletions retry/api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import random
import time
import threading

from functools import partial

Expand All @@ -11,7 +11,7 @@


def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0,
logger=logging_logger):
logger=logging_logger, condition=threading.Condition()):
"""
Executes a function and retries it if it failed.

Expand All @@ -25,6 +25,9 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None,
fixed if a number, random if a range tuple (min, max)
:param logger: logger.warning(fmt, error, delay) will be called on failed attempts.
default: retry.logging_logger. if None, logging is disabled.
:param condition: a threading.Condition that has aquire / release and wait(n) methods.
Used instead of time.sleep to bypass global interpreter lock (GIL).

:returns: the result of the f function.
"""
_tries, _delay = tries, delay
Expand All @@ -38,8 +41,11 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None,

if logger is not None:
logger.warning('%s, retrying in %s seconds...', e, _delay)

time.sleep(_delay)

# the three lines below, sleep _delay seconds.
condition.acquire()
condition.wait(_delay)
condition.release()
_delay *= backoff

if isinstance(jitter, tuple):
Expand All @@ -51,7 +57,8 @@ def __retry_internal(f, exceptions=Exception, tries=-1, delay=0, max_delay=None,
_delay = min(_delay, max_delay)


def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger):
def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, jitter=0, logger=logging_logger,
condition=threading.Condition()):
"""Returns a retry decorator.

:param exceptions: an exception or a tuple of exceptions to catch. default: Exception.
Expand All @@ -63,6 +70,8 @@ def retry(exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1, ji
fixed if a number, random if a range tuple (min, max)
:param logger: logger.warning(fmt, error, delay) will be called on failed attempts.
default: retry.logging_logger. if None, logging is disabled.
:param condition: a threading.Condition that has aquire / release and wait(n) methods.
Used instead of time.sleep to bypass global interpreter lock (GIL).
:returns: a retry decorator.
"""

Expand All @@ -71,14 +80,14 @@ def retry_decorator(f, *fargs, **fkwargs):
args = fargs if fargs else list()
kwargs = fkwargs if fkwargs else dict()
return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter,
logger)
logger, condition=condition)

return retry_decorator


def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, delay=0, max_delay=None, backoff=1,
jitter=0,
logger=logging_logger):
logger=logging_logger, condition=threading.Condition()):
"""
Calls a function and re-executes it if it failed.

Expand All @@ -94,8 +103,11 @@ def retry_call(f, fargs=None, fkwargs=None, exceptions=Exception, tries=-1, dela
fixed if a number, random if a range tuple (min, max)
:param logger: logger.warning(fmt, error, delay) will be called on failed attempts.
default: retry.logging_logger. if None, logging is disabled.
:param condition: a threading.Condition that has aquire / release and wait(n) methods.
Used instead of time.sleep to bypass global interpreter lock (GIL).
:returns: the result of the f function.
"""
args = fargs if fargs else list()
kwargs = fkwargs if fkwargs else dict()
return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger)
return __retry_internal(partial(f, *args, **kwargs), exceptions, tries, delay, max_delay, backoff, jitter, logger,
condition=condition)
68 changes: 38 additions & 30 deletions tests/test_retry.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,52 @@
try:
from unittest.mock import create_autospec
except ImportError:
from mock import create_autospec

try:
from unittest.mock import MagicMock
from unittest import mock
except ImportError:
from mock import create_autospec
from mock import MagicMock
import mock

import time
import threading

import pytest

from retry.api import retry_call
from retry.api import retry
import retry.api

import logging

def test_retry(monkeypatch):
mock_sleep_time = [0]
logging_logger = logging.Logger(__name__)

def mock_sleep(seconds):
mock_sleep_time[0] += seconds
mock_sleep_time = [0]

monkeypatch.setattr(time, 'sleep', mock_sleep)
class mockCondition():
def acquire(self):
logging_logger.warning('in acquire')
return True

def release(self):
logging_logger.warning('in release')

def wait(self, seconds):
logging_logger.warning('in wait')
mock_sleep_time[0] += seconds


def test_retry(monkeypatch):
global mock_sleep_time
mock_sleep_time = [0]

monkeypatch.setattr(retry.api.threading, 'Condition', mockCondition)
hit = [0]

tries = 5
delay = 1
backoff = 2

@retry(tries=tries, delay=delay, backoff=backoff)
@retry.api.retry(tries=tries, delay=delay, backoff=backoff, condition=mockCondition())
def f():
hit[0] += 1
1 / 0

with pytest.raises(ZeroDivisionError):
f()
assert hit[0] == tries
Expand All @@ -46,7 +58,7 @@ def test_tries_inf():
hit = [0]
target = 10

@retry(tries=float('inf'))
@retry.api.retry(tries=float('inf'), condition=mockCondition())
def f():
hit[0] += 1
if hit[0] == target:
Expand All @@ -60,7 +72,7 @@ def test_tries_minus1():
hit = [0]
target = 10

@retry(tries=-1)
@retry.api.retry(tries=-1)
def f():
hit[0] += 1
if hit[0] == target:
Expand All @@ -71,12 +83,10 @@ def f():


def test_max_delay(monkeypatch):
global mock_sleep_time
mock_sleep_time = [0]

def mock_sleep(seconds):
mock_sleep_time[0] += seconds

monkeypatch.setattr(time, 'sleep', mock_sleep)
monkeypatch.setattr(retry.api.threading, 'Condition', mockCondition)

hit = [0]

Expand All @@ -85,7 +95,7 @@ def mock_sleep(seconds):
backoff = 2
max_delay = delay # Never increase delay

@retry(tries=tries, delay=delay, max_delay=max_delay, backoff=backoff)
@retry.api.retry(tries=tries, delay=delay, max_delay=max_delay, backoff=backoff, condition=mockCondition())
def f():
hit[0] += 1
1 / 0
Expand All @@ -97,19 +107,17 @@ def f():


def test_fixed_jitter(monkeypatch):
global mock_sleep_time
mock_sleep_time = [0]

def mock_sleep(seconds):
mock_sleep_time[0] += seconds

monkeypatch.setattr(time, 'sleep', mock_sleep)
monkeypatch.setattr(retry.api.threading, 'Condition', mockCondition)

hit = [0]

tries = 10
jitter = 1

@retry(tries=tries, jitter=jitter)
@retry.api.retry(tries=tries, jitter=jitter, condition=mockCondition())
def f():
hit[0] += 1
1 / 0
Expand All @@ -124,7 +132,7 @@ def test_retry_call():
f_mock = MagicMock(side_effect=RuntimeError)
tries = 2
try:
retry_call(f_mock, exceptions=RuntimeError, tries=tries)
retry.api.retry_call(f_mock, exceptions=RuntimeError, tries=tries, condition=mockCondition())
except RuntimeError:
pass

Expand All @@ -137,7 +145,7 @@ def test_retry_call_2():
tries = 5
result = None
try:
result = retry_call(f_mock, exceptions=RuntimeError, tries=tries)
result = retry.api.retry_call(f_mock, exceptions=RuntimeError, tries=tries, condition=mockCondition())
except RuntimeError:
pass

Expand All @@ -157,7 +165,7 @@ def f(value=0):
result = None
f_mock = MagicMock(spec=f, return_value=return_value)
try:
result = retry_call(f_mock, fargs=[return_value])
result = retry.api.retry_call(f_mock, fargs=[return_value], condition=mockCondition())
except RuntimeError:
pass

Expand All @@ -177,7 +185,7 @@ def f(value=0):
result = None
f_mock = MagicMock(spec=f, return_value=kwargs['value'])
try:
result = retry_call(f_mock, fkwargs=kwargs)
result = retry.api.retry_call(f_mock, fkwargs=kwargs, condition=mockCondition())
except RuntimeError:
pass

Expand Down