Skip to content

Commit b45e10b

Browse files
jplfariaclaude
andcommitted
fix(genome_annotator): widen RAST retry window for sustained outages
_MAX_RETRIES 3->5, backoff 5+15s (20s total) -> 5+15+60+180s (260s total). Issue #28: tutorial.theseed.org returned 504 on all 3 retry attempts during a sustained outage; 20s wasn't enough to ride it out. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 8b2effe commit b45e10b

2 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/modelseed_api/services/genome_annotator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@
5353
"NewConnectionError",
5454
"NameResolutionError",
5555
})
56-
_MAX_RETRIES = 3
57-
_BACKOFF_SECONDS = (5, 15) # waits between attempts 1->2 and 2->3
56+
_MAX_RETRIES = 5
57+
_BACKOFF_SECONDS = (5, 15, 60, 180) # waits between attempts 1->2, 2->3, 3->4, 4->5
5858

5959
_PROTEIN_STAGES = [
6060
{"name": "annotate_proteins_kmer_v2", "kmer_v2_parameters": {}},

tests/unit/test_genome_annotator.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import pytest
1212

1313
from modelseed_api.services.genome_annotator import (
14+
_BACKOFF_SECONDS,
1415
_DNA_STAGES,
16+
_MAX_RETRIES,
1517
_PROTEIN_STAGES,
1618
_parse_fasta_records,
1719
annotate_fasta,
@@ -178,3 +180,88 @@ def call(self, method, params):
178180

179181
with pytest.raises(ValueError, match="no.*functional roles"):
180182
annotate_fasta(">p1\nMKKLVAVLIVSLAVALSALAVA")
183+
184+
185+
class TestAnnotateFastaRetry:
186+
"""Covers the retry window widened for issue #28 (sustained RAST 504s)."""
187+
188+
def test_retries_on_transient_error_then_succeeds(self, monkeypatch):
189+
import requests
190+
191+
calls = []
192+
193+
class TimeoutThenSuccessClient:
194+
def __init__(self, url, timeout=600):
195+
pass
196+
197+
def call(self, method, params):
198+
calls.append(1)
199+
if len(calls) < _MAX_RETRIES:
200+
raise requests.exceptions.ReadTimeout(
201+
"Read timed out. (read timeout=600)"
202+
)
203+
return [{
204+
"features": [
205+
{"id": "p1", "protein_translation": "MKK",
206+
"function": "Pyruvate kinase"},
207+
],
208+
}]
209+
210+
monkeypatch.setattr(
211+
"modelseed_api.services.genome_annotator.RPCClient",
212+
TimeoutThenSuccessClient,
213+
)
214+
monkeypatch.setattr(
215+
"modelseed_api.services.genome_annotator.time.sleep", lambda _: None
216+
)
217+
218+
ms_genome = annotate_fasta(">p1\nMKKLVAVLIVSLAVALSALAVA")
219+
assert len(calls) == _MAX_RETRIES
220+
assert len(ms_genome.features) == 1
221+
222+
def test_reraises_after_exhausting_retries(self, monkeypatch):
223+
import requests
224+
225+
calls = []
226+
227+
class AlwaysTimeoutClient:
228+
def __init__(self, url, timeout=600):
229+
pass
230+
231+
def call(self, method, params):
232+
calls.append(1)
233+
raise requests.exceptions.ReadTimeout(
234+
"Read timed out. (read timeout=600)"
235+
)
236+
237+
monkeypatch.setattr(
238+
"modelseed_api.services.genome_annotator.RPCClient",
239+
AlwaysTimeoutClient,
240+
)
241+
monkeypatch.setattr(
242+
"modelseed_api.services.genome_annotator.time.sleep", lambda _: None
243+
)
244+
245+
with pytest.raises(requests.exceptions.ReadTimeout):
246+
annotate_fasta(">p1\nMKKLVAVLIVSLAVALSALAVA")
247+
assert len(calls) == _MAX_RETRIES
248+
assert len(_BACKOFF_SECONDS) == _MAX_RETRIES - 1
249+
250+
def test_non_transient_error_not_retried(self, monkeypatch):
251+
calls = []
252+
253+
class AlwaysFailClient:
254+
def __init__(self, url, timeout=600):
255+
pass
256+
257+
def call(self, method, params):
258+
calls.append(1)
259+
raise ValueError("400 Bad Request: genome missing required field")
260+
261+
monkeypatch.setattr(
262+
"modelseed_api.services.genome_annotator.RPCClient", AlwaysFailClient,
263+
)
264+
265+
with pytest.raises(ValueError, match="400 Bad Request"):
266+
annotate_fasta(">p1\nMKKLVAVLIVSLAVALSALAVA")
267+
assert len(calls) == 1 # no retry for non-transient

0 commit comments

Comments
 (0)