-
-
Save evilensky/9d659bde4008435f388e7b7107a855b7 to your computer and use it in GitHub Desktop.
async python http requests using exponential backoff, jitter, and event logging
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
This module provides functionality for making HTTP requests. It leverages the `aiohttp` | |
library for asynchronous HTTP requests, and the `backoff` library to implement exponential | |
backoff in case of failed requests. | |
The module defines a child logger for logging purposes and implements two methods, `on_backoff` | |
and `on_giveup`, which log information about the retry attempts and when the retry attempts are | |
given up respectively. | |
The `http_request` function is the primary function of the module, making an HTTP request with the | |
provided parameters. If the request fails due to an `aiohttp.ClientError`, the function will retry | |
the request using an exponential backoff strategy, up to a maximum of 5 times or a total of 60 | |
seconds. | |
The function logs the result of the HTTP request, either indicating success and the status code of | |
the response or raising an exception if the response cannot be parsed as JSON or if the response | |
status code indicates a client error. | |
""" | |
import json | |
import logging | |
import aiohttp | |
import backoff | |
# set up child logger for module | |
logger = logging.getLogger(__name__) | |
def on_backoff(backoff_event): | |
""" | |
Logs a warning message whenever a backoff event occurs due to a failed HTTP request. | |
Parameters | |
---------- | |
backoff_event : dict | |
A dictionary containing detailed information about the backoff event. | |
It includes the following keys: | |
'wait' (the delay before the next retry), | |
'tries' (the number of attempts made), | |
'exception' (the exception that caused the backoff), | |
'target' (the function where the exception was raised), | |
'args' (the arguments passed to the target function), | |
'kwargs' (the keyword arguments passed to the target function), | |
and 'elapsed' (the time elapsed since the first attempt). | |
""" | |
logger.warning( | |
"http backoff event", | |
extra={ | |
"Retrying in, seconds": {backoff_event["wait"]}, | |
"Attempt number": {backoff_event["tries"]}, | |
"Exception": {backoff_event["exception"]}, | |
"Target function": {backoff_event["target"].__name__}, | |
"Args": {backoff_event["args"]}, | |
"Kwargs": {backoff_event["kwargs"]}, | |
"Elapsed time": {backoff_event["elapsed"]}, | |
}, | |
) | |
def on_giveup(giveup_event): | |
""" | |
Logs an error message when a series of HTTP requests fail and the retry attempts are given up. | |
Parameters | |
---------- | |
giveup_event : dict | |
A dictionary containing detailed information about the event when retries are given up. | |
It includes the following keys: | |
'tries' (the number of attempts made), | |
'exception' (the exception that caused the retries to be given up), | |
'target' (the function where the exception was raised), | |
'args' (the arguments passed to the target function), | |
'kwargs' (the keyword arguments passed to the target function), | |
and 'elapsed' (the time elapsed since the first attempt). | |
""" | |
logger.error( | |
"http giveup event", | |
extra={ | |
"Giving up after retries exceeded": giveup_event["tries"], | |
"Exception": giveup_event["exception"], | |
"Target function": giveup_event["target"].__name__, | |
"Args": giveup_event["args"], | |
"Kwargs": giveup_event["kwargs"], | |
"Elapsed time": giveup_event["elapsed"], | |
}, | |
) | |
@backoff.on_exception( | |
backoff.expo, | |
aiohttp.ClientError, | |
jitter=backoff.random_jitter, | |
max_tries=5, | |
max_time=60, | |
on_backoff=on_backoff, | |
on_giveup=on_giveup, | |
) | |
async def http_request(verb, url, query_params, headers, payload): | |
""" | |
Performs an HTTP request, retrying on `aiohttp.ClientError` exceptions with an exponential | |
backoff strategy. | |
Parameters | |
---------- | |
verb : str | |
The HTTP method for the request, such as 'GET', 'POST', etc. | |
url : str | |
The URL for the HTTP request. | |
query_params : dict | |
The query parameters to be included in the request. | |
headers : dict | |
The headers to be included in the request. | |
payload : dict | |
The payload (body) of the request, which will be sent as JSON. | |
Returns | |
------- | |
None | |
Raises | |
------ | |
ValueError | |
If the response from the HTTP request cannot be parsed as JSON. | |
aiohttp.ClientResponseError | |
If the HTTP request returns a response with a status code indicating a client error. | |
Note | |
---- | |
The function will automatically retry the request if an `aiohttp.ClientError` is raised. | |
It uses an exponential backoff strategy with a maximum of 5 tries and a total retry duration | |
of 60 seconds. The 'on_backoff' function will be called after each failed attempt, and the | |
'on_giveup' function will be called if all retry attempts fail. | |
""" | |
async with aiohttp.ClientSession() as session: | |
async with session.request( | |
method=verb, url=url, params=query_params, headers=headers, json=payload | |
) as response: | |
if response.ok: | |
try: | |
data = await response.json() | |
return data | |
except json.JSONDecodeError as json_decode_error: | |
raise ValueError( | |
"Failed to parse response JSON" | |
) from json_decode_error | |
else: | |
raise aiohttp.ClientResponseError( | |
response.request_info, | |
response.history, | |
status=response.status, | |
message=response.reason, | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment