Coder Social home page Coder Social logo

h2non / riprova Goto Github PK

View Code? Open in Web Editor NEW
116.0 3.0 9.0 141 KB

Versatile async-friendly retry package with multiple backoff strategies

Home Page: http://riprova.rtfd.io

License: MIT License

Makefile 1.84% Python 98.16%
retry backoff python decorator resilient resiliency asyncio async-await

riprova's Introduction

riprova PyPI Coverage Status Documentation Status Quality Versions

riprova (meaning retry in Italian) is a small, general-purpose and versatile Python library that provides retry mechanisms with multiple backoff strategies for any sort of failed operations.

It's domain agnostic, highly customizable, extensible and provides a minimal API that's easy to instrument in any code base via decorators, context managers or raw API consumption.

For a brief introduction about backoff mechanisms for potential failed operations, read this article.

Features

  • Retry decorator for simple and idiomatic consumption.
  • Simple Pythonic programmatic interface.
  • Maximum retry timeout support.
  • Supports error whitelisting and blacklisting.
  • Supports custom error evaluation retry logic (useful to retry only in specific cases).
  • Automatically retry operations on raised exceptions.
  • Supports asynchronous coroutines with both async/await and yield from syntax.
  • Configurable maximum number of retry attempts.
  • Highly configurable supporting max retries, timeouts or retry notifier callback.
  • Built-in backoff strategies: constant, fibonacci and exponential backoffs.
  • Supports sync/async context managers.
  • Pluggable custom backoff strategies.
  • Lightweight library with almost zero embedding cost.
  • Works with Python +2.6, 3.0+ and PyPy.

Backoff strategies

List of built-in backoff strategies.

You can also implement your own one easily. See ConstantBackoff for an implementation reference.

Installation

Using pip package manager (requires pip 1.9+. Upgrade it running: pip install -U pip):

pip install -U riprova

Or install the latest sources from Github:

pip install -e git+git://github.com/h2non/riprova.git#egg=riprova

API

Examples

You can see more featured examples from the documentation site.

Basic usage examples:

import riprova

@riprova.retry
def task():
    """Retry operation if it fails with constant backoff (default)"""

@riprova.retry(backoff=riprova.ConstantBackoff(retries=5))
def task():
    """Retry operation if it fails with custom max number of retry attempts"""

@riprova.retry(backoff=riprova.ExponentialBackOff(factor=0.5))
def task():
    """Retry operation if it fails using exponential backoff"""

@riprova.retry(timeout=10)
def task():
    """Raises a TimeoutError if the retry loop exceeds from 10 seconds"""

def on_retry(err, next_try):
    print('Operation error: {}'.format(err))
    print('Next try in: {}ms'.format(next_try))

@riprova.retry(on_retry=on_retry)
def task():
    """Subscribe via function callback to every retry attempt"""

def evaluator(response):
    # Force retry operation if not a valid response
    if response.status >= 400:
        raise RuntimeError('invalid response status')  # or simple return True
    # Otherwise return False, meaning no retry
    return False

@riprova.retry(evaluator=evaluator)
def task():
    """Use a custom evaluator function to determine if the operation failed or not"""

@riprova.retry
async def task():
    """Asynchronous coroutines are also supported :)"""

Retry failed HTTP requests:

import pook
import requests
from riprova import retry

# Define HTTP mocks to simulate failed requests
pook.get('server.com').times(3).reply(503)
pook.get('server.com').times(1).reply(200).json({'hello': 'world'})


# Retry evaluator function used to determine if the operated failed or not
def evaluator(response):
    if response != 200:
        return Exception('failed request')  # you can also simply return True
    return False


# On retry even subscriptor
def on_retry(err, next_try):
    print('Operation error {}'.format(err))
    print('Next try in {}ms'.format(next_try))


# Register retriable operation
@retry(evaluator=evaluator, on_retry=on_retry)
def fetch(url):
    return requests.get(url)


# Run task that might fail
fetch('http://server.com')

License

MIT - Tomas Aparicio

riprova's People

Contributors

brycedrennan avatar casxt avatar czchen avatar edwardbetts avatar ffix avatar h2non avatar jettscythe avatar jstasiak avatar pokoli avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

riprova's Issues

How to use retrier inside class function with custom on_retry?

Hi, I'd like to use riprova.Retrier inside my custom class. e.g. I want to try to re-login every time say_trier fails or raises an exception. Is this sample code right? Didn't find a nice way to use decorater for say_trier.

class SayHello(object):
    def login(self):
        print "log in"
        do_login_process()

    def say(self):
        retrier = riprova.Retrier(backoff=riprova.ExponentialBackOff(interval=30), on_retry=self.on_retry)
        retrier.run(self.say_trier)

    def say_trier(self):
        something_may_fail_here()

    def on_retry(self, err, next_try):
        print('Operation error: {}'.format(err))
        print('Next try in: {}ms'.format(next_try))
        self.login()

sa = SayHello()
sa.say()

Cannot retry when `asyncio.TimeoutError` is raised

riprova cannot retry when a method raise asyncio.TimeoutError, even when asyncio.TimeoutError is set in blacklist. The reason is that asyncio.TimeoutError is handle explicitly in https://github.com/h2non/riprova/blob/v0.2.5/riprova/async_retrier.py#L238-L240, thus it cannot trigger retry. Is this an intended behavior?

The following is sample code to reprocedure this issue.

import asyncio
import riprova

BLACKLIST = riprova.ErrorBlacklist([
    asyncio.TimeoutError,
])

@riprova.retry(backoff=riprova.ConstantBackoff(interval=1, retries=3), error_evaluator=BLACKLIST.isretry)
async def func():
    print('func')
    raise asyncio.TimeoutError()

asyncio.get_event_loop().run_until_complete(func())

The func will only run once, instead of three times.

Bug | Inconsistent test

timeout = 200 while interval=100 causes the target function to run either twice or thrice (due to machine speed), which makes the following test inconsistent:

def test_retrier_run_max_timeout(MagicMock):
    iterable = (ValueError, NotImplementedError, RuntimeError, Exception)
    task = MagicMock(side_effect=iterable)

    retrier = Retrier(timeout=200, backoff=ConstantBackoff(interval=100))

    with pytest.raises(RetryTimeoutError):
        retrier.run(task, 2, 4, foo='bar')

    assert task.called
    assert task.call_count >= 1
    task.assert_called_with(2, 4, foo='bar')

    assert retrier.attempts >= 1
    assert isinstance(retrier.error, NotImplementedError)

Sometimes the raised error would be NotImplementedError, while at other times RunTimeError

I changed this test in my pull request but the core problem needs to be fixed.

Can't pass sleep_fn from retry to Retrier

>>> import riprova
>>> import time
>>> def my_sleep(*args, **kwargs): print("my sleep", args, kwargs); time.sleep(*args, **kwargs)
... 
>>> my_sleep(1)
my sleep (1,) {}
>>> def fail(): raise Exception
... 
>>> riprova.retry(backoff=riprova.ConstantBackoff(interval=2, retries=5), sleep_fn=my_sleep)(fail)()
Traceback (most recent call last):
  File "<input>", line 1, in <module>
    riprova.retry(backoff=riprova.ConstantBackoff(interval=2, retries=5), sleep_fn=my_sleep)(fail)()
  File "/home/poh/pro/py/riprova/riprova/retry.py", line 132, in wrapper
    return retry_runner(*args, **kw)
  File "/home/poh/pro/py/riprova/riprova/retrier.py", line 294, in run
    delay = self._get_delay()
  File "/home/poh/pro/py/riprova/riprova/retrier.py", line 246, in _get_delay
    self.error)
  File "<string>", line 2, in raise_from
riprova.exceptions.MaxRetriesExceeded: max retries exceeded
>>> 

The reason is identical names for keyword only arguments for decorator and wrapper:
https://github.com/h2non/riprova/blob/master/riprova/retry.py#L24
https://github.com/h2non/riprova/blob/master/riprova/retry.py#L120

Deprecation warnings

Few deprecation warnings. FYI.

/usr/local/lib/python3.8/site-packages/riprova/async_retrier.py:141: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
self.on_retry = asyncio.coroutine(on_retry) if on_retry else None
/usr/local/lib/python3.8/site-packages/riprova/async_retrier.py:279: DeprecationWarning: The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10.
return (yield from asyncio.wait_for(

Support newer Python versions.

When importing riprova in my python 3.11 project, I see AttributeError: module 'asyncio' has no attribute 'coroutine'. Did you mean: 'coroutines'?
From a quick google search, I can see that in python >=3.5 you're supposed to be using

async def function_name(arguments):
    await function()

Not

@asyncio.coroutine
def function_name(arguments):
    yield from function()

asyncio concurrent asyncio retry does not work.

When calling asyncio.gather with functions decorated retry, the retries would finish earlier than expected.

import asyncio
import logging

import riprova


logging.basicConfig(
    format='[%(asctime)s] %(levelname)s - %(message)s',
    datefmt="%Y-%m-%d %H:%M:%S",
    level=logging.INFO
)
LOG = logging.getLogger()


def do_retry(val):
    LOG.info("do retry %s", val)
    return True


@riprova.retry(
    backoff=riprova.ConstantBackoff(retries=5),
    error_evaluator=do_retry,
)
async def do_stuff(name):
    LOG.info('calling %s', name)
    if name == 'bar':
        await asyncio.sleep(1.0)
    else:
        await asyncio.sleep(0.1)

    raise ValueError('value-error-{}'.format(name))


async def process():
    coros = [do_stuff('foo'), do_stuff('bar')]
    # coros = [do_stuff('foo')]
    await asyncio.gather(*coros, return_exceptions=True)


loop = asyncio.get_event_loop()
loop.run_until_complete(process())
loop.close()

results:

[2018-03-29 19:17:42] INFO - calling bar
[2018-03-29 19:17:42] INFO - calling foo
[2018-03-29 19:17:42] INFO - do retry value-error-foo
[2018-03-29 19:17:42] INFO - calling foo
[2018-03-29 19:17:43] INFO - do retry value-error-foo
[2018-03-29 19:17:43] INFO - calling foo
[2018-03-29 19:17:43] INFO - do retry value-error-foo
[2018-03-29 19:17:43] INFO - calling foo
[2018-03-29 19:17:43] INFO - do retry value-error-foo
[2018-03-29 19:17:43] INFO - calling foo
[2018-03-29 19:17:43] INFO - do retry value-error-foo
[2018-03-29 19:17:43] INFO - do retry value-error-bar
[2018-03-29 19:17:43] INFO - calling foo
[2018-03-29 19:17:43] INFO - do retry value-error-foo

The expected result is that foo and bar should be both called 6x.

Fault result

Don't really know does it fits into your library (I mean, you decide what is relevant), but when I wrote my own «restarter» I use some fault_result argument, which returned as function result after attempts gone. In my case this was made to avoid try/except/pass things, cause function fault was like «Oh, fault! Ok, will call it later anyway, does not matter…». It looks like right now we can't do that without external try/except/pass wrapper.

Retrier catches internal error


 # Get delay before next retry
 delay = self.backoff.next()

 # If backoff is ready
 if delay == Backoff.STOP:
 return raise_from(MaxRetriesExceeded('max retries exceeded'), err)

While messing around with the code I checked err value and was surprised to see ImportError:

err = {ImportError} No module named stackless

The error was PyCharm related, maybe a similar error would be caused if I forgot to pip install requirements?

This code ignores all errors, even if it is caused internally and not by the function.

A possible solution is to blacklist certain errors, such as ImportError

Retry context manager

with riprova.Retrier(backoff=ConstantBackoff()) as retry:
   retry.run(run_failed_task, 'foo', bar=1)

async with riprova.AsyncRetrier(backoff=ConstantBackoff()) as retry:
   await retry.run(run_failed_task, 'foo', bar=1)

can you release the py310 fix pls.

I don't think async works for 3.10, 3.11 with the current release.

  • installing from github doesnot work with the error:
fatal: unable to connect to github.com:
github.com[0: 140.82.121.4]: errno=Operation timed out

Exclude paco

I see nothing wrong with using third party libraries, but in some cases it looks like is-number npm package 😆. It means, that js is poor, since it need such third-party «patches». I don't want to think, that python is poor, since it is not.

  • paco.wraps should be removed cause it's enough to wrap functions with asyncio.coroutine without inspecting it.
>>> def sync(): print("sync")
... 
>>> async def async_(): print("async")
... 
>>> asyncio.get_event_loop().run_until_complete(asyncio.coroutine(sync)())
sync
>>> asyncio.get_event_loop().run_until_complete(asyncio.coroutine(async_)())
async
>>>
  • paco.TimeoutLimit, async_timeout used when you have couple of things inside with block. If you have one coroutine, which you want to limit, you should use asyncio.wait_for and you don't need to split logic for running with or without timeout. If there is no timeout, then your timeout should be None.

I hope this two suggestions are strong enough to exclude paco from dependencies.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.