25

I am using an API and sometimes it returns some odd status codes which could be fixed by simply retrying the same request. I am using aiohttp to do submit requests to this api asynchronously.

I am also using the backoff library to retry requests, however it appears that requests are still not being retried upon 401 status responses.

   @backoff.on_exception(backoff.expo, aiohttp.ClientError, max_tries=11, max_time=60)
    async def get_user_timeline(self, session, user_id, count, max_id, trim_user, include_rts, tweet_mode):

        params = {
            'user_id': user_id,
            'trim_user': trim_user,
            'include_rts': include_rts,
            'tweet_mode': tweet_mode,
            'count': count
        }


        if (max_id and max_id != -1):
            params.update({'max_id': max_id})

        headers = {
            'Authorization': 'Bearer {}'.format(self.access_token)    
        }

        users_lookup_url = "/1.1/statuses/user_timeline.json"

        url = self.base_url + users_lookup_url
        
        async with session.get(url, params=params, headers=headers) as response:
            result = await response.json()
            response = {
                'result': result,
                'status': response.status,
                'headers': response.headers
            }
            return response

I would like all requests to be retired up to 10 times if the response has a status code other than 200 or 429.

mac13k
  • 2,423
  • 23
  • 34
Kay
  • 17,906
  • 63
  • 162
  • 270

4 Answers4

23

I made a simple library, that can help you:
https://github.com/inyutin/aiohttp_retry

Code like this should solve your problem:

from aiohttp import ClientSession
from aiohttp_retry import RetryClient

statuses = {x for x in range(100, 600)}
statuses.remove(200)
statuses.remove(429)

async with ClientSession() as client:
    retry_client = RetryClient(client)
    async with retry_client.get("https://google.com", retry_attempts=10, retry_for_statuses=statuses) as response:
        text = await response.text()
        print(text)
    await retry_client.close()

Instead google.com use your own url

inyutin
  • 406
  • 3
  • 9
20

By default aiohttp doesn't raise exception for non-200 status. You should change it passing raise_for_status=True (doc):

async with session.get(url, params=params, headers=headers, raise_for_status=True) as response:

It should raise exception for any statuses 400 or higher and thus trigger backoff.

Codes 2xx shouldn't be probably retried since these aren't errors.


Anyway if you still want to raise for "other than 200 or 429" you can do it manually:

if response.status not in (200, 429,):
     raise aiohttp.ClientResponseError()
Mikhail Gerasimov
  • 36,989
  • 16
  • 116
  • 159
1

Mikhail's answer cover how to raise exception for status codes 4XX and 5XX, but if you want your coroutine to be retried for these status codes, take a look at async_retrying library - https://pypi.org/project/async_retrying/

A simple example is given below.

import asyncio
from aiohttp import ClientSession
from async_retrying import retry


@retry(attempts=2)
async def hit_url(url, session: ClientSession):
    async with session.get(url) as response:
        print("Calling URL : %s", url)
        await response.text()
        return response.status


async def main():
    urls = [
        "https://google.com",
        "https://yahoo.com"
    ]
    api_calls = []
    async with ClientSession(raise_for_status=True) as session:
        for url in urls:
            api_calls.append(hit_url(session=session, url=url))
    await asyncio.gather(*api_calls, return_exceptions=False)

asyncio.run(main())
Akhil
  • 498
  • 2
  • 6
  • 22
0

Maybe it's too old, but for any people wondering how to build such solution

RequestData and ErrorResponseData are your custom classes, it's not something built in

class DataAPI:
    def __init__(self, api_data_converter: APIDataConverter):
        self.api_data_converter = api_data_converter

    async def _bound_fetch(self, request_data: RequestData, session):
        try:
            async with session.get(request_data.url, raise_for_status=True) as response:
                return ResponseData(await response.text())
        except aiohttp.ClientConnectionError as e:
            Logging.log_exception('Connection error: {}'.format(str(e)))
            return ErrorResponseData(url=request_data.url, request_data=request_data)
        except Exception as e:
            Logging.log_exception('Data API error: {}'.format(str(e)))
            return ErrorResponseData(url=request_data.url, request_data=request_data)

    async def _run_requests(self, request_data: List[RequestData]):
        for rd in request_data:
            Logging.log_info('Request: {}'.format(rd.url))
        async with aiohttp.ClientSession(timeout=ClientTimeout(total=80)) as session:
            tasks = []
            for rd in request_data:
                task = asyncio.ensure_future(self._bound_fetch(rd, session))
                tasks.append(task)
            responses = asyncio.gather(*tasks)
            return await responses

    def get_data(self, request_data: List[RequestData]):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        skipped = request_data
        responses: List[ResponseData] = []
        for _ in range(2): # specify your retry count instead of 2
            interm_responses = loop.run_until_complete(asyncio.ensure_future(self._run_requests(skipped)))
            skipped = []
            for resp in interm_responses:
                if isinstance(resp, ErrorResponseData):
                    skipped.append(resp.request_data)
                else:
                    responses.append(resp)
            if not skipped:
                break

        if skipped:
            Logging.log_critical('Failed urls remaining')

        for resp in responses:
            data = self.api_data_converter.convert(resp.response)
            if not data:
                Logging.log_exception('Data API error')
            dt = dateutil.parser.parse(data[-1]['dt'])
            resp.response = data
            resp.last_candle_dt = dt
        return responses
JaktensTid
  • 272
  • 2
  • 5
  • 20