Python decorator for backoff retries in class

Question:

I have a class named Client which tries to connect to a server via Websockets. I tried to make a decorator to retry if it couldn’t connect to the server but I get the error

ValueError: a coroutine was expected, got <__main__.Client object at 0x000001BE86D71930>

The code is this:

def retry(fnc):
    def f_retry(self):
        tries = 2
        delay = 2
        backoff = 2
        while tries > 1:
            try:
                return self.main()
            except Exception as e:
                msg = "%s, Retrying in %d seconds..." % (str(e), delay)
                print(msg)
                time.sleep(delay)
                tries -= 1
                delay *= backoff
        return self.main()

    return f_retry

class Client():
    def __init__(self):
        self.name= None
        
    @retry
    async def main(self):
        try : 
            async with websockets.connect(
                'ws://localhost:8000',
            ) as ws:
                logging.info("Connected to the server")
        except Exception as e:
            print(e)

if __name__ == '__main__':
    client = Client()
    asyncio.run(client.main())

This algorithm is inspired from This other question

Asked By: Humberto Adhemar

||

Answers:

Because you are decorating an async function, you need to make your wrapper async and await the result of the function:

import asyncio
from functools import wrap
def retry(fnc):
    @wraps(fnc)
    async def f_retry(*args, **kwargs):
        tries = 2
        delay = 2
        backoff = 2
        while tries > 1:
            try:
                return await fnc(*args, **kwargs)
            except Exception as e:
                msg = "%s, Retrying in %d seconds..." % (str(e), delay)
                print(msg)
                await asyncio.sleep(delay)
                tries -= 1
                delay *= backoff
        return await fnc(*args, **kwargs)

    return f_retry

I’ve fixed a few more things:

  1. Use functools.wraps
    functools.wraps helps decorators behave more like the functions they wrap.
  2. Make it care about the argument
    Instead of just using self.main, this is now more versatile, can be used for methods and functions with any arguments, and any name.
  3. Fix RecursionError
    Previously, it would call self.main, which was itself. Now it calls the passed function.
Answered By: pigrammer
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.