Skip to content

Commit b6795f0

Browse files
feat(providers/soup): Implement retries with exponential backoff (#434)
This is needed by the DeepL provider, and they recommend it in their API docs, since the service is expected to return "HTTP 429: too many requests" errors when sending many API requests in a short period of time.
1 parent f3ffb02 commit b6795f0

File tree

2 files changed

+30
-3
lines changed

2 files changed

+30
-3
lines changed

dialect/providers/modules/deepl.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class Provider(SoupProvider):
3535
def __init__(self, **kwargs):
3636
super().__init__(**kwargs)
3737

38+
self.retry_errors = (429,)
3839
self.chars_limit = 5000
3940

4041
# DeepL API Free keys can be identified by the suffix ":fx"
@@ -132,6 +133,8 @@ def check_known_errors(self, status, data):
132133
raise APIKeyInvalid(message)
133134
case 456:
134135
raise ServiceLimitReached(message)
136+
case 429:
137+
raise UnexpectedError("Too many requests!")
135138

136139
if status != 200:
137140
raise UnexpectedError(message)

dialect/providers/soup.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import logging
7+
from asyncio import sleep
78
from typing import Any
89

910
from gi.repository import GLib, Soup
@@ -18,6 +19,11 @@ class SoupProvider(BaseProvider):
1819

1920
def __init__(self, **kwargs):
2021
super().__init__(**kwargs)
22+
23+
self.retry_errors: tuple[int] = tuple()
24+
""" Error codes that should be retried automatically """
25+
self.max_retries = 5
26+
""" Max number of tries """
2127

2228
def encode_data(self, data: Any) -> GLib.Bytes | None:
2329
"""
@@ -123,6 +129,8 @@ async def send_and_read_and_process(
123129
124130
Converts `GLib.Error` to `RequestError`.
125131
132+
It also handles retries for status codes listen in ``self.retry_errors``.
133+
126134
Args:
127135
message: Message to send.
128136
check_common: If response data should be checked for errors using check_known_errors.
@@ -132,11 +140,27 @@ async def send_and_read_and_process(
132140
The JSON deserialized to a python object or bytes if ``json`` is ``False``.
133141
"""
134142

135-
try:
143+
async def send_and_read() -> Any:
136144
if return_json:
137-
response = await self.send_and_read_json(message)
145+
return await self.send_and_read_json(message)
138146
else:
139-
response = await self.send_and_read(message)
147+
return await self.send_and_read(message)
148+
149+
try:
150+
response = await send_and_read()
151+
152+
# Do retries with exponential backoff for errors
153+
if message.get_status() in self.retry_errors:
154+
delay = 1
155+
156+
for _ in range(self.max_retries):
157+
await sleep(delay)
158+
response = await send_and_read()
159+
160+
if message.get_status() in self.retry_errors:
161+
delay *= 2
162+
else:
163+
break
140164

141165
if check_common:
142166
self.check_known_errors(message.get_status(), response)

0 commit comments

Comments
 (0)