Slow the whole run down when flickr answers 429

Flickr's limit is an hourly window, so backing off one photo for a couple
of minutes and then hitting the next photo at the old rate just fails
again, which is how a full fetch gave up on photos after six attempts. A
429 on a paced request now doubles the pace for every later flickr call,
up to ten seconds, and the pace relaxes back to the configured value only
after twenty untroubled requests. The per-request backoff also starts at
fifteen seconds instead of five, caps at fifteen minutes, and gets eight
attempts, so a single photo can wait out a short window on its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bobandClaude Fable 5.1 committed 2026-09-10 16:37:08 +01:00
1 parent 07db2739ca
commit 36c974e843
3 files changed
+74 -14

No files matched your search

+42 -5
View File
@@ -74,8 +74,9 @@ class FakeOpen:
class TestCase(unittest.TestCase):
"""Silences output and restores the module globals tests move around."""
GLOBALS = ("OPEN", "SLEEP", "ATTEMPTS", "BACKOFF_BASE", "PACE", "QUIET",
"DEBUG", "HTTP_TIMEOUT", "USER_AGENT", "STATS", "DOWNLOAD_CAP")
GLOBALS = ("OPEN", "SLEEP", "ATTEMPTS", "BACKOFF_BASE", "PACE", "PACE_FLOOR",
"PACEHELD", "QUIET", "DEBUG", "HTTP_TIMEOUT", "USER_AGENT", "STATS",
"DOWNLOAD_CAP")
def setUp(self):
self._output = contextlib.ExitStack()
@@ -87,7 +88,8 @@ class TestCase(unittest.TestCase):
self.addCleanup(self._output.close)
self._saved = {k: getattr(rb, k) for k in self.GLOBALS}
rb.SLEEP = lambda seconds: None
rb.PACE = 0.0
rb.PACE = rb.PACE_FLOOR = 0.0
rb.PACEHELD = 0
rb.STATS = dict.fromkeys(rb.STATS, 0)
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
@@ -309,7 +311,9 @@ class Settings(TestCase):
self.assertTrue(conf["fetch"] and conf["sync"])
self.assertFalse(conf["dry_run"])
self.assertIsNone(conf["limit"])
self.assertEqual(conf["attempts"], 6)
self.assertEqual(conf["attempts"], 8)
self.assertEqual(conf["backoff"], 15.0)
self.assertEqual(rb.BACKOFF_CAP, 900.0)
self.assertEqual(conf["flickr_auth"], os.path.expanduser(rb.FLICKR_CONFIG))
def test_defaults_are_literals_not_moving_constants(self):
@@ -319,7 +323,7 @@ class Settings(TestCase):
self.assertEqual(conf["pace"], rb.DEFAULTS["pace"])
self.assertEqual(conf["attempts"], rb.DEFAULTS["attempts"])
self.assertEqual(conf["user_agent"], rb.DEFAULTS["user_agent"])
self.assertEqual((rb.DEFAULTS["pace"], rb.DEFAULTS["attempts"]), (1.0, 6))
self.assertEqual((rb.DEFAULTS["pace"], rb.DEFAULTS["attempts"]), (1.0, 8))
def test_debug_is_a_setting(self):
path = self.write("s.yml", "directory: /d\ndebug: true\n")
@@ -430,6 +434,39 @@ class Retrying(TestCase):
rb.flickrcall("flickr.test.echo", {}, "KEY")
self.assertEqual(waits, [2.0])
def test_429_slows_every_later_flickr_call(self):
waits = []
rb.SLEEP = waits.append
rb.PACE = rb.PACE_FLOOR = 1.0
rb.OPEN = FakeOpen(http_error(429), FakeResponse({"stat": "ok"}),
FakeResponse({"stat": "ok"}))
rb.flickrcall("flickr.test.echo", {}, "KEY")
self.assertEqual(rb.PACE, 2.0)
rb.flickrcall("flickr.test.echo", {}, "KEY")
self.assertEqual(waits[-1], 2.0)
self.assertEqual(rb.PACEHELD, rb.PACE_HOLD - 2)
self.assertIn("pacing", self.stderr.getvalue())
def test_pace_relaxes_back_to_the_configured_floor(self):
rb.PACE_FLOOR = 1.0
rb.PACE = 4.0
rb.OPEN = FakeOpen(*[FakeResponse({"stat": "ok"}) for _ in range(40)])
for _ in range(40):
rb.flickrcall("flickr.test.echo", {}, "KEY")
self.assertEqual(rb.PACE, 1.0)
def test_pace_never_exceeds_the_cap(self):
rb.PACE = rb.PACE_FLOOR = 8.0
rb.OPEN = FakeOpen(http_error(429), FakeResponse({"stat": "ok"}))
rb.flickrcall("flickr.test.echo", {}, "KEY")
self.assertEqual(rb.PACE, rb.PACE_CAP)
def test_429_from_immich_leaves_the_pace_alone(self):
rb.PACE = rb.PACE_FLOOR = 1.0
rb.OPEN = FakeOpen(http_error(429), FakeResponse({"id": "u1"}))
rb.Immich("https://i/api", "k").me()
self.assertEqual(rb.PACE, 1.0)
def test_pace_leaves_immich_and_the_index_alone(self):
waits = []
rb.SLEEP = waits.append