Skip to content

Commit 59884e1

Browse files
bbappserveropentor
authored andcommitted
Implement dynamic API throttling with LearningDelay an Exponential Backoff
Introduced a LearningDelay class to manage API throttling dynamically. Updated the DeviantartOAuthAPI to utilize the new delay mechanism for improved handling of rate limits. Recover frame rate limiting with exponential backoff instead of linear.
1 parent 038efda commit 59884e1

1 file changed

Lines changed: 62 additions & 3 deletions

File tree

gallery_dl/extractor/deviantart.py

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1429,6 +1429,44 @@ def items(self):
14291429
###############################################################################
14301430
# API Interfaces ##############################################################
14311431

1432+
class LearningDelay():
1433+
'''
1434+
Deviantart API is throttled dynamically over time
1435+
you can learn the ideal ammount to wait
1436+
'''
1437+
1438+
def __init__(self, initial=1, error_margin=0.01):
1439+
'''
1440+
Inital is the inital time to wait in seconds,
1441+
error_margin is added on top of the esitmate but not used in
1442+
the update. It accounts for fuzzyness in the choice of value
1443+
1444+
TODO: You can store an load this calue across gallery-dl execution.
1445+
'''
1446+
self._min = 0
1447+
self._current = initial
1448+
self._prev = initial + error_margin
1449+
self._error = error_margin
1450+
self._convegered = False
1451+
1452+
def reject(self):
1453+
'''The current estimate was too low, increase it'''
1454+
self._min = self._current
1455+
self._current = self._prev+self._error
1456+
1457+
def update(self):
1458+
'''Try a smaller estimate'''
1459+
if self._convegered:
1460+
return
1461+
self._current = 0.5*(self._current+self._min)
1462+
self._convegered = abs(self._current - self._prev) < self._error
1463+
1464+
@property
1465+
def value(self):
1466+
'''Current guess pluss an engineering margin for rounding errors'''
1467+
return self._current + self._error
1468+
1469+
14321470
class DeviantartOAuthAPI():
14331471
"""Interface for the DeviantArt OAuth API
14341472
@@ -1445,6 +1483,9 @@ def __init__(self, extractor):
14451483

14461484
self.delay = extractor.config("wait-min", 0)
14471485
self.delay_min = max(2, self.delay)
1486+
self.exp_backoff_mul = 2
1487+
self.learned_delay = LearningDelay(initial=self.delay)
1488+
self.is_throttled = False
14481489

14491490
self.mature = extractor.config("mature", "true")
14501491
if not isinstance(self.mature, str):
@@ -1746,7 +1787,10 @@ def _call(self, endpoint, fatal=True, log=True, public=None, **kwargs):
17461787

17471788
while True:
17481789
if self.delay:
1749-
self.extractor.sleep(self.delay, "api")
1790+
if self.is_throttled:
1791+
self.extractor.sleep(self.delay, "api")
1792+
else:
1793+
self.extractor.sleep(self.learned_delay.value, "api")
17501794

17511795
self.authenticate(None if public else self.refresh_token_key)
17521796
kwargs["headers"] = self.headers
@@ -1760,6 +1804,14 @@ def _call(self, endpoint, fatal=True, log=True, public=None, **kwargs):
17601804

17611805
status = response.status_code
17621806
if 200 <= status < 400:
1807+
self.learned_delay.update()
1808+
self.is_throttled = False
1809+
1810+
# Exp backoff shouldn't be but could be significant
1811+
# so nap back to a sane value for linear cooldown
1812+
if self.delay > 10:
1813+
self.delay = 10
1814+
17631815
if self.delay > self.delay_min:
17641816
self.delay -= 1
17651817
return data
@@ -1775,8 +1827,15 @@ def _call(self, endpoint, fatal=True, log=True, public=None, **kwargs):
17751827
self.log.debug(response.text)
17761828
msg = f"API responded with {status} {response.reason}"
17771829
if status == 429:
1778-
if self.delay < 30:
1779-
self.delay += 1
1830+
1831+
# When throttled we use exp backoff
1832+
self.is_throttled = True
1833+
1834+
# Exp backoff is a multiplier so enure sane initial delay
1835+
if self.delay < 1:
1836+
self.delay = 1
1837+
else:
1838+
self.delay *= self.exp_backoff_mul
17801839
self.log.warning("%s. Using %ds delay.", msg, self.delay)
17811840

17821841
if self._warn_429 and self.delay >= 3:

0 commit comments

Comments
 (0)