|
11 | 11 | import pytest |
12 | 12 |
|
13 | 13 | from modelseed_api.services.genome_annotator import ( |
| 14 | + _BACKOFF_SECONDS, |
14 | 15 | _DNA_STAGES, |
| 16 | + _MAX_RETRIES, |
15 | 17 | _PROTEIN_STAGES, |
16 | 18 | _parse_fasta_records, |
17 | 19 | annotate_fasta, |
@@ -178,3 +180,88 @@ def call(self, method, params): |
178 | 180 |
|
179 | 181 | with pytest.raises(ValueError, match="no.*functional roles"): |
180 | 182 | 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