toil 6.1.0a1__py3-none-any.whl → 8.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toil/__init__.py +122 -315
- toil/batchSystems/__init__.py +1 -0
- toil/batchSystems/abstractBatchSystem.py +173 -89
- toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
- toil/batchSystems/awsBatch.py +244 -135
- toil/batchSystems/cleanup_support.py +26 -16
- toil/batchSystems/contained_executor.py +31 -28
- toil/batchSystems/gridengine.py +86 -50
- toil/batchSystems/htcondor.py +166 -89
- toil/batchSystems/kubernetes.py +632 -382
- toil/batchSystems/local_support.py +20 -15
- toil/batchSystems/lsf.py +134 -81
- toil/batchSystems/lsfHelper.py +13 -11
- toil/batchSystems/mesos/__init__.py +41 -29
- toil/batchSystems/mesos/batchSystem.py +290 -151
- toil/batchSystems/mesos/executor.py +79 -50
- toil/batchSystems/mesos/test/__init__.py +31 -23
- toil/batchSystems/options.py +46 -28
- toil/batchSystems/registry.py +53 -19
- toil/batchSystems/singleMachine.py +296 -125
- toil/batchSystems/slurm.py +603 -138
- toil/batchSystems/torque.py +47 -33
- toil/bus.py +186 -76
- toil/common.py +664 -368
- toil/cwl/__init__.py +1 -1
- toil/cwl/cwltoil.py +1136 -483
- toil/cwl/utils.py +17 -22
- toil/deferred.py +63 -42
- toil/exceptions.py +5 -3
- toil/fileStores/__init__.py +5 -5
- toil/fileStores/abstractFileStore.py +140 -60
- toil/fileStores/cachingFileStore.py +717 -269
- toil/fileStores/nonCachingFileStore.py +116 -87
- toil/job.py +1225 -368
- toil/jobStores/abstractJobStore.py +416 -266
- toil/jobStores/aws/jobStore.py +863 -477
- toil/jobStores/aws/utils.py +201 -120
- toil/jobStores/conftest.py +3 -2
- toil/jobStores/fileJobStore.py +292 -154
- toil/jobStores/googleJobStore.py +140 -74
- toil/jobStores/utils.py +36 -15
- toil/leader.py +668 -272
- toil/lib/accelerators.py +115 -18
- toil/lib/aws/__init__.py +74 -31
- toil/lib/aws/ami.py +122 -87
- toil/lib/aws/iam.py +284 -108
- toil/lib/aws/s3.py +31 -0
- toil/lib/aws/session.py +214 -39
- toil/lib/aws/utils.py +287 -231
- toil/lib/bioio.py +13 -5
- toil/lib/compatibility.py +11 -6
- toil/lib/conversions.py +104 -47
- toil/lib/docker.py +131 -103
- toil/lib/ec2.py +361 -199
- toil/lib/ec2nodes.py +174 -106
- toil/lib/encryption/_dummy.py +5 -3
- toil/lib/encryption/_nacl.py +10 -6
- toil/lib/encryption/conftest.py +1 -0
- toil/lib/exceptions.py +26 -7
- toil/lib/expando.py +5 -3
- toil/lib/ftp_utils.py +217 -0
- toil/lib/generatedEC2Lists.py +127 -19
- toil/lib/humanize.py +6 -2
- toil/lib/integration.py +341 -0
- toil/lib/io.py +141 -15
- toil/lib/iterables.py +4 -2
- toil/lib/memoize.py +12 -8
- toil/lib/misc.py +66 -21
- toil/lib/objects.py +2 -2
- toil/lib/resources.py +68 -15
- toil/lib/retry.py +126 -81
- toil/lib/threading.py +299 -82
- toil/lib/throttle.py +16 -15
- toil/options/common.py +843 -409
- toil/options/cwl.py +175 -90
- toil/options/runner.py +50 -0
- toil/options/wdl.py +73 -17
- toil/provisioners/__init__.py +117 -46
- toil/provisioners/abstractProvisioner.py +332 -157
- toil/provisioners/aws/__init__.py +70 -33
- toil/provisioners/aws/awsProvisioner.py +1145 -715
- toil/provisioners/clusterScaler.py +541 -279
- toil/provisioners/gceProvisioner.py +282 -179
- toil/provisioners/node.py +155 -79
- toil/realtimeLogger.py +34 -22
- toil/resource.py +137 -75
- toil/server/app.py +128 -62
- toil/server/celery_app.py +3 -1
- toil/server/cli/wes_cwl_runner.py +82 -53
- toil/server/utils.py +54 -28
- toil/server/wes/abstract_backend.py +64 -26
- toil/server/wes/amazon_wes_utils.py +21 -15
- toil/server/wes/tasks.py +121 -63
- toil/server/wes/toil_backend.py +142 -107
- toil/server/wsgi_app.py +4 -3
- toil/serviceManager.py +58 -22
- toil/statsAndLogging.py +224 -70
- toil/test/__init__.py +282 -183
- toil/test/batchSystems/batchSystemTest.py +460 -210
- toil/test/batchSystems/batch_system_plugin_test.py +90 -0
- toil/test/batchSystems/test_gridengine.py +173 -0
- toil/test/batchSystems/test_lsf_helper.py +67 -58
- toil/test/batchSystems/test_slurm.py +110 -49
- toil/test/cactus/__init__.py +0 -0
- toil/test/cactus/test_cactus_integration.py +56 -0
- toil/test/cwl/cwlTest.py +496 -287
- toil/test/cwl/measure_default_memory.cwl +12 -0
- toil/test/cwl/not_run_required_input.cwl +29 -0
- toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
- toil/test/cwl/seqtk_seq.cwl +1 -1
- toil/test/docs/scriptsTest.py +69 -46
- toil/test/jobStores/jobStoreTest.py +427 -264
- toil/test/lib/aws/test_iam.py +118 -50
- toil/test/lib/aws/test_s3.py +16 -9
- toil/test/lib/aws/test_utils.py +5 -6
- toil/test/lib/dockerTest.py +118 -141
- toil/test/lib/test_conversions.py +113 -115
- toil/test/lib/test_ec2.py +58 -50
- toil/test/lib/test_integration.py +104 -0
- toil/test/lib/test_misc.py +12 -5
- toil/test/mesos/MesosDataStructuresTest.py +23 -10
- toil/test/mesos/helloWorld.py +7 -6
- toil/test/mesos/stress.py +25 -20
- toil/test/options/__init__.py +13 -0
- toil/test/options/options.py +42 -0
- toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
- toil/test/provisioners/clusterScalerTest.py +440 -250
- toil/test/provisioners/clusterTest.py +166 -44
- toil/test/provisioners/gceProvisionerTest.py +174 -100
- toil/test/provisioners/provisionerTest.py +25 -13
- toil/test/provisioners/restartScript.py +5 -4
- toil/test/server/serverTest.py +188 -141
- toil/test/sort/restart_sort.py +137 -68
- toil/test/sort/sort.py +134 -66
- toil/test/sort/sortTest.py +91 -49
- toil/test/src/autoDeploymentTest.py +141 -101
- toil/test/src/busTest.py +20 -18
- toil/test/src/checkpointTest.py +8 -2
- toil/test/src/deferredFunctionTest.py +49 -35
- toil/test/src/dockerCheckTest.py +32 -24
- toil/test/src/environmentTest.py +135 -0
- toil/test/src/fileStoreTest.py +539 -272
- toil/test/src/helloWorldTest.py +7 -4
- toil/test/src/importExportFileTest.py +61 -31
- toil/test/src/jobDescriptionTest.py +46 -21
- toil/test/src/jobEncapsulationTest.py +2 -0
- toil/test/src/jobFileStoreTest.py +74 -50
- toil/test/src/jobServiceTest.py +187 -73
- toil/test/src/jobTest.py +121 -71
- toil/test/src/miscTests.py +19 -18
- toil/test/src/promisedRequirementTest.py +82 -36
- toil/test/src/promisesTest.py +7 -6
- toil/test/src/realtimeLoggerTest.py +10 -6
- toil/test/src/regularLogTest.py +71 -37
- toil/test/src/resourceTest.py +80 -49
- toil/test/src/restartDAGTest.py +36 -22
- toil/test/src/resumabilityTest.py +9 -2
- toil/test/src/retainTempDirTest.py +45 -14
- toil/test/src/systemTest.py +12 -8
- toil/test/src/threadingTest.py +44 -25
- toil/test/src/toilContextManagerTest.py +10 -7
- toil/test/src/userDefinedJobArgTypeTest.py +8 -5
- toil/test/src/workerTest.py +73 -23
- toil/test/utils/toilDebugTest.py +103 -33
- toil/test/utils/toilKillTest.py +4 -5
- toil/test/utils/utilsTest.py +245 -106
- toil/test/wdl/wdltoil_test.py +818 -149
- toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
- toil/toilState.py +120 -35
- toil/utils/toilConfig.py +13 -4
- toil/utils/toilDebugFile.py +44 -27
- toil/utils/toilDebugJob.py +214 -27
- toil/utils/toilDestroyCluster.py +11 -6
- toil/utils/toilKill.py +8 -3
- toil/utils/toilLaunchCluster.py +256 -140
- toil/utils/toilMain.py +37 -16
- toil/utils/toilRsyncCluster.py +32 -14
- toil/utils/toilSshCluster.py +49 -22
- toil/utils/toilStats.py +356 -273
- toil/utils/toilStatus.py +292 -139
- toil/utils/toilUpdateEC2Instances.py +3 -1
- toil/version.py +12 -12
- toil/wdl/utils.py +5 -5
- toil/wdl/wdltoil.py +3913 -1033
- toil/worker.py +367 -184
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
- toil-8.0.0.dist-info/METADATA +173 -0
- toil-8.0.0.dist-info/RECORD +253 -0
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
- toil-6.1.0a1.dist-info/METADATA +0 -125
- toil-6.1.0a1.dist-info/RECORD +0 -237
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
toil/lib/retry.py
CHANGED
|
@@ -131,35 +131,30 @@ import sqlite3
|
|
|
131
131
|
import time
|
|
132
132
|
import traceback
|
|
133
133
|
import urllib.error
|
|
134
|
+
from collections.abc import Generator, Iterable, Sequence
|
|
134
135
|
from contextlib import contextmanager
|
|
135
|
-
from typing import
|
|
136
|
-
Callable,
|
|
137
|
-
ContextManager,
|
|
138
|
-
Generator,
|
|
139
|
-
Iterable,
|
|
140
|
-
List,
|
|
141
|
-
Optional,
|
|
142
|
-
Sequence,
|
|
143
|
-
Tuple,
|
|
144
|
-
Type,
|
|
145
|
-
Union)
|
|
136
|
+
from typing import Any, Callable, ContextManager, Optional, TypeVar, Union
|
|
146
137
|
|
|
147
138
|
import requests.exceptions
|
|
148
139
|
import urllib3.exceptions
|
|
149
140
|
|
|
150
|
-
SUPPORTED_HTTP_ERRORS = [
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
141
|
+
SUPPORTED_HTTP_ERRORS = [
|
|
142
|
+
http.client.HTTPException,
|
|
143
|
+
urllib.error.HTTPError,
|
|
144
|
+
urllib3.exceptions.HTTPError,
|
|
145
|
+
requests.exceptions.HTTPError,
|
|
146
|
+
]
|
|
154
147
|
|
|
155
148
|
try:
|
|
156
149
|
import kubernetes.client.rest
|
|
150
|
+
|
|
157
151
|
SUPPORTED_HTTP_ERRORS.append(kubernetes.client.rest.ApiException)
|
|
158
152
|
except ModuleNotFoundError:
|
|
159
153
|
kubernetes = None
|
|
160
154
|
|
|
161
155
|
try:
|
|
162
156
|
import botocore.exceptions
|
|
157
|
+
|
|
163
158
|
SUPPORTED_HTTP_ERRORS.append(botocore.exceptions.ClientError)
|
|
164
159
|
except ModuleNotFoundError:
|
|
165
160
|
botocore = None
|
|
@@ -175,12 +170,14 @@ class ErrorCondition:
|
|
|
175
170
|
whether to retry.
|
|
176
171
|
"""
|
|
177
172
|
|
|
178
|
-
def __init__(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
error: Optional[Any] = None,
|
|
176
|
+
error_codes: list[int] = None,
|
|
177
|
+
boto_error_codes: list[str] = None,
|
|
178
|
+
error_message_must_include: str = None,
|
|
179
|
+
retry_on_this_condition: bool = True,
|
|
180
|
+
):
|
|
184
181
|
"""
|
|
185
182
|
Initialize this ErrorCondition.
|
|
186
183
|
|
|
@@ -224,13 +221,18 @@ class ErrorCondition:
|
|
|
224
221
|
)
|
|
225
222
|
|
|
226
223
|
|
|
224
|
+
# There is a better way to type hint this with python 3.10
|
|
225
|
+
# https://stackoverflow.com/a/68290080
|
|
226
|
+
RT = TypeVar("RT")
|
|
227
|
+
|
|
228
|
+
|
|
227
229
|
def retry(
|
|
228
|
-
intervals: Optional[
|
|
230
|
+
intervals: Optional[list] = None,
|
|
229
231
|
infinite_retries: bool = False,
|
|
230
|
-
errors: Optional[Sequence[Union[ErrorCondition,
|
|
231
|
-
log_message: Optional[
|
|
232
|
-
prepare: Optional[
|
|
233
|
-
) -> Callable[[
|
|
232
|
+
errors: Optional[Sequence[Union[ErrorCondition, type[Exception]]]] = None,
|
|
233
|
+
log_message: Optional[tuple[Callable, str]] = None,
|
|
234
|
+
prepare: Optional[list[Callable]] = None,
|
|
235
|
+
) -> Callable[[Callable[..., RT]], Callable[..., RT]]:
|
|
234
236
|
"""
|
|
235
237
|
Retry a function if it fails with any Exception defined in "errors".
|
|
236
238
|
|
|
@@ -263,7 +265,9 @@ def retry(
|
|
|
263
265
|
errors = errors if errors else [Exception]
|
|
264
266
|
|
|
265
267
|
error_conditions = {error for error in errors if isinstance(error, ErrorCondition)}
|
|
266
|
-
retriable_errors = {
|
|
268
|
+
retriable_errors = {
|
|
269
|
+
error for error in errors if not isinstance(error, ErrorCondition)
|
|
270
|
+
}
|
|
267
271
|
|
|
268
272
|
if log_message:
|
|
269
273
|
post_message_function = log_message[0]
|
|
@@ -272,7 +276,10 @@ def retry(
|
|
|
272
276
|
# if a generic error exists (with no restrictions),
|
|
273
277
|
# delete more specific error_condition instances of it
|
|
274
278
|
for error_condition in error_conditions:
|
|
275
|
-
if
|
|
279
|
+
if (
|
|
280
|
+
error_condition.retry_on_this_condition
|
|
281
|
+
and error_condition.error in retriable_errors
|
|
282
|
+
):
|
|
276
283
|
error_conditions.remove(error_condition)
|
|
277
284
|
|
|
278
285
|
# if a more specific error exists that isn't in the general set,
|
|
@@ -281,9 +288,9 @@ def retry(
|
|
|
281
288
|
if error_condition.retry_on_this_condition:
|
|
282
289
|
retriable_errors.add(error_condition.error)
|
|
283
290
|
|
|
284
|
-
def decorate(func):
|
|
291
|
+
def decorate(func: Callable[..., RT]) -> Callable[..., RT]:
|
|
285
292
|
@functools.wraps(func)
|
|
286
|
-
def call(*args, **kwargs):
|
|
293
|
+
def call(*args, **kwargs) -> RT:
|
|
287
294
|
intervals_remaining = copy.deepcopy(intervals)
|
|
288
295
|
while True:
|
|
289
296
|
try:
|
|
@@ -303,13 +310,17 @@ def retry(
|
|
|
303
310
|
raise
|
|
304
311
|
|
|
305
312
|
interval = intervals_remaining.pop(0)
|
|
306
|
-
logger.warning(
|
|
313
|
+
logger.warning(
|
|
314
|
+
f"Error in {func}: {e}. Retrying after {interval} s..."
|
|
315
|
+
)
|
|
307
316
|
time.sleep(interval)
|
|
308
317
|
if prepare is not None:
|
|
309
318
|
for prep_function in prepare:
|
|
310
319
|
# Reset state for next attempt
|
|
311
320
|
prep_function(*args, **kwargs)
|
|
321
|
+
|
|
312
322
|
return call
|
|
323
|
+
|
|
313
324
|
return decorate
|
|
314
325
|
|
|
315
326
|
|
|
@@ -320,17 +331,18 @@ def return_status_code(e):
|
|
|
320
331
|
|
|
321
332
|
if botocore:
|
|
322
333
|
if isinstance(e, botocore.exceptions.ClientError):
|
|
323
|
-
return e.response.get(
|
|
334
|
+
return e.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
|
|
324
335
|
|
|
325
336
|
if isinstance(e, requests.exceptions.HTTPError):
|
|
326
337
|
return e.response.status_code
|
|
327
|
-
elif isinstance(e, http.client.HTTPException) or
|
|
328
|
-
|
|
338
|
+
elif isinstance(e, http.client.HTTPException) or isinstance(
|
|
339
|
+
e, urllib3.exceptions.HTTPError
|
|
340
|
+
):
|
|
329
341
|
return e.status
|
|
330
342
|
elif isinstance(e, urllib.error.HTTPError):
|
|
331
343
|
return e.code
|
|
332
344
|
else:
|
|
333
|
-
raise ValueError(f
|
|
345
|
+
raise ValueError(f"Unsupported error type; cannot grok status code: {e}.")
|
|
334
346
|
|
|
335
347
|
|
|
336
348
|
def get_error_code(e: Exception) -> str:
|
|
@@ -339,21 +351,21 @@ def get_error_code(e: Exception) -> str:
|
|
|
339
351
|
|
|
340
352
|
Returns empty string for other errors.
|
|
341
353
|
"""
|
|
342
|
-
if hasattr(e,
|
|
354
|
+
if hasattr(e, "error_code") and isinstance(e.error_code, str):
|
|
343
355
|
# A Boto 2 error
|
|
344
356
|
return e.error_code
|
|
345
|
-
if hasattr(e,
|
|
357
|
+
if hasattr(e, "code") and isinstance(e.code, str):
|
|
346
358
|
# A (different?) Boto 2 error
|
|
347
359
|
return e.code
|
|
348
|
-
elif hasattr(e,
|
|
360
|
+
elif hasattr(e, "response") and hasattr(e.response, "get"):
|
|
349
361
|
# A Boto 3 error
|
|
350
|
-
code = e.response.get(
|
|
362
|
+
code = e.response.get("Error", {}).get("Code")
|
|
351
363
|
if isinstance(code, str):
|
|
352
364
|
return code
|
|
353
365
|
else:
|
|
354
|
-
return
|
|
366
|
+
return ""
|
|
355
367
|
else:
|
|
356
|
-
return
|
|
368
|
+
return ""
|
|
357
369
|
|
|
358
370
|
|
|
359
371
|
def get_error_message(e: Exception) -> str:
|
|
@@ -363,18 +375,18 @@ def get_error_message(e: Exception) -> str:
|
|
|
363
375
|
Note that error message conditions also check more than this; this function
|
|
364
376
|
does not fall back to the traceback for incompatible types.
|
|
365
377
|
"""
|
|
366
|
-
if hasattr(e,
|
|
378
|
+
if hasattr(e, "error_message") and isinstance(e.error_message, str):
|
|
367
379
|
# A Boto 2 error
|
|
368
380
|
return e.error_message
|
|
369
|
-
elif hasattr(e,
|
|
381
|
+
elif hasattr(e, "response") and hasattr(e.response, "get"):
|
|
370
382
|
# A Boto 3 error
|
|
371
|
-
message = e.response.get(
|
|
383
|
+
message = e.response.get("Error", {}).get("Message")
|
|
372
384
|
if isinstance(message, str):
|
|
373
385
|
return message
|
|
374
386
|
else:
|
|
375
|
-
return
|
|
387
|
+
return ""
|
|
376
388
|
else:
|
|
377
|
-
return
|
|
389
|
+
return ""
|
|
378
390
|
|
|
379
391
|
|
|
380
392
|
def get_error_status(e: Exception) -> int:
|
|
@@ -388,22 +400,23 @@ def get_error_status(e: Exception) -> int:
|
|
|
388
400
|
|
|
389
401
|
Returns 0 from other errors.
|
|
390
402
|
"""
|
|
403
|
+
|
|
391
404
|
def numify(x):
|
|
392
405
|
"""Make sure a value is an integer."""
|
|
393
406
|
return int(str(x).strip())
|
|
394
407
|
|
|
395
|
-
if hasattr(e,
|
|
408
|
+
if hasattr(e, "status"):
|
|
396
409
|
# A Boto 2 error, kubernetes.client.rest.ApiException,
|
|
397
410
|
# http.client.HTTPException, or urllib3.exceptions.HTTPError
|
|
398
411
|
return numify(e.status)
|
|
399
|
-
elif hasattr(e,
|
|
400
|
-
if hasattr(e.response,
|
|
412
|
+
elif hasattr(e, "response"):
|
|
413
|
+
if hasattr(e.response, "status_code"):
|
|
401
414
|
# A requests.exceptions.HTTPError
|
|
402
415
|
return numify(e.response.status_code)
|
|
403
|
-
elif hasattr(e.response,
|
|
416
|
+
elif hasattr(e.response, "get"):
|
|
404
417
|
# A Boto 3 error
|
|
405
|
-
return numify(e.response.get(
|
|
406
|
-
elif hasattr(e,
|
|
418
|
+
return numify(e.response.get("ResponseMetadata", {}).get("HTTPStatusCode"))
|
|
419
|
+
elif hasattr(e, "code"):
|
|
407
420
|
# A urllib.error.HTTPError
|
|
408
421
|
return numify(e.code)
|
|
409
422
|
else:
|
|
@@ -416,16 +429,17 @@ def get_error_body(e: Exception) -> str:
|
|
|
416
429
|
|
|
417
430
|
Returns the code and message if the error does not have a body.
|
|
418
431
|
"""
|
|
419
|
-
if hasattr(e,
|
|
432
|
+
if hasattr(e, "body"):
|
|
420
433
|
# A Boto 2 error
|
|
421
434
|
if isinstance(e.body, bytes):
|
|
422
435
|
# Decode the body first
|
|
423
|
-
return e.body.decode(
|
|
436
|
+
return e.body.decode("utf-8")
|
|
424
437
|
elif isinstance(e.body, str):
|
|
425
438
|
return e.body
|
|
426
439
|
|
|
427
440
|
# Anything else
|
|
428
|
-
return f
|
|
441
|
+
return f"{get_error_code(e)}: {get_error_message(e)}"
|
|
442
|
+
|
|
429
443
|
|
|
430
444
|
def meets_error_message_condition(e: Exception, error_message: Optional[str]):
|
|
431
445
|
if error_message:
|
|
@@ -437,7 +451,9 @@ def meets_error_message_condition(e: Exception, error_message: Optional[str]):
|
|
|
437
451
|
if isinstance(e, botocore.exceptions.ClientError):
|
|
438
452
|
return error_message in str(e)
|
|
439
453
|
|
|
440
|
-
if isinstance(e, http.client.HTTPException) or isinstance(
|
|
454
|
+
if isinstance(e, http.client.HTTPException) or isinstance(
|
|
455
|
+
e, urllib3.exceptions.HTTPError
|
|
456
|
+
):
|
|
441
457
|
return error_message in e.reason
|
|
442
458
|
elif isinstance(e, sqlite3.OperationalError):
|
|
443
459
|
return error_message in str(e)
|
|
@@ -445,7 +461,7 @@ def meets_error_message_condition(e: Exception, error_message: Optional[str]):
|
|
|
445
461
|
return error_message in e.msg
|
|
446
462
|
elif isinstance(e, requests.exceptions.HTTPError):
|
|
447
463
|
return error_message in e.raw
|
|
448
|
-
elif hasattr(e,
|
|
464
|
+
elif hasattr(e, "msg"):
|
|
449
465
|
return error_message in e.msg
|
|
450
466
|
else:
|
|
451
467
|
return error_message in traceback.format_exc()
|
|
@@ -453,7 +469,7 @@ def meets_error_message_condition(e: Exception, error_message: Optional[str]):
|
|
|
453
469
|
return True
|
|
454
470
|
|
|
455
471
|
|
|
456
|
-
def meets_error_code_condition(e: Exception, error_codes: Optional[
|
|
472
|
+
def meets_error_code_condition(e: Exception, error_codes: Optional[list[int]]):
|
|
457
473
|
"""These are expected to be normal HTTP error codes, like 404 or 500."""
|
|
458
474
|
if error_codes:
|
|
459
475
|
status_code = get_error_status(e)
|
|
@@ -462,7 +478,9 @@ def meets_error_code_condition(e: Exception, error_codes: Optional[List[int]]):
|
|
|
462
478
|
return True
|
|
463
479
|
|
|
464
480
|
|
|
465
|
-
def meets_boto_error_code_condition(
|
|
481
|
+
def meets_boto_error_code_condition(
|
|
482
|
+
e: Exception, boto_error_codes: Optional[list[str]]
|
|
483
|
+
):
|
|
466
484
|
"""These are expected to be AWS's custom error aliases, like 'BucketNotFound' or 'AccessDenied'."""
|
|
467
485
|
if boto_error_codes:
|
|
468
486
|
status_code = get_error_code(e)
|
|
@@ -475,26 +493,44 @@ def error_meets_conditions(e, error_conditions):
|
|
|
475
493
|
condition_met = False
|
|
476
494
|
for error in error_conditions:
|
|
477
495
|
if isinstance(e, error.error):
|
|
478
|
-
if
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
496
|
+
if (
|
|
497
|
+
error.error_codes
|
|
498
|
+
or error.boto_error_codes
|
|
499
|
+
or error.error_message_must_include
|
|
500
|
+
):
|
|
501
|
+
error_message_condition_met = meets_error_message_condition(
|
|
502
|
+
e, error.error_message_must_include
|
|
503
|
+
)
|
|
504
|
+
error_code_condition_met = meets_error_code_condition(
|
|
505
|
+
e, error.error_codes
|
|
506
|
+
)
|
|
507
|
+
boto_error_code_condition_met = meets_boto_error_code_condition(
|
|
508
|
+
e, error.boto_error_codes
|
|
509
|
+
)
|
|
510
|
+
if (
|
|
511
|
+
error_message_condition_met
|
|
512
|
+
and error_code_condition_met
|
|
513
|
+
and boto_error_code_condition_met
|
|
514
|
+
):
|
|
483
515
|
if not error.retry_on_this_condition:
|
|
484
516
|
return False
|
|
485
517
|
condition_met = True
|
|
486
518
|
return condition_met
|
|
487
519
|
|
|
520
|
+
|
|
488
521
|
DEFAULT_DELAYS = (0, 1, 1, 4, 16, 64)
|
|
489
522
|
DEFAULT_TIMEOUT = 300
|
|
490
523
|
|
|
524
|
+
E = TypeVar("E", bound=Exception) # so mypy understands passed through types
|
|
525
|
+
|
|
526
|
+
|
|
491
527
|
# TODO: Replace the use of this with retry()
|
|
492
528
|
# The aws provisioner and jobstore need a large refactoring to be boto3 compliant, so this is
|
|
493
529
|
# still used there to avoid the duplication of future work
|
|
494
530
|
def old_retry(
|
|
495
531
|
delays: Iterable[float] = DEFAULT_DELAYS,
|
|
496
532
|
timeout: float = DEFAULT_TIMEOUT,
|
|
497
|
-
predicate: Callable[[
|
|
533
|
+
predicate: Callable[[E], bool] = lambda e: False,
|
|
498
534
|
) -> Generator[ContextManager, None, None]:
|
|
499
535
|
"""
|
|
500
536
|
Deprecated.
|
|
@@ -567,39 +603,48 @@ def old_retry(
|
|
|
567
603
|
>>> i
|
|
568
604
|
1
|
|
569
605
|
"""
|
|
606
|
+
if timeout is None:
|
|
607
|
+
timeout = DEFAULT_TIMEOUT
|
|
570
608
|
if timeout > 0:
|
|
571
|
-
go = [
|
|
609
|
+
go = [None]
|
|
572
610
|
|
|
573
611
|
@contextmanager
|
|
574
|
-
def repeated_attempt(
|
|
612
|
+
def repeated_attempt(delay):
|
|
575
613
|
try:
|
|
576
614
|
yield
|
|
577
615
|
except Exception as e:
|
|
578
|
-
if time.time(
|
|
579
|
-
if predicate(
|
|
580
|
-
logger.info(
|
|
581
|
-
time.sleep(
|
|
616
|
+
if time.time() + delay < expiration:
|
|
617
|
+
if predicate(e):
|
|
618
|
+
logger.info("Got %s, trying again in %is.", e, delay)
|
|
619
|
+
time.sleep(delay)
|
|
582
620
|
else:
|
|
583
|
-
logger.error(
|
|
621
|
+
logger.error(
|
|
622
|
+
"Got a %s: %s which is not retriable according to %s",
|
|
623
|
+
type(e),
|
|
624
|
+
e,
|
|
625
|
+
predicate,
|
|
626
|
+
)
|
|
584
627
|
raise
|
|
585
628
|
else:
|
|
586
|
-
logger.error(
|
|
629
|
+
logger.error("Got %s and no time is left to retry", e)
|
|
587
630
|
raise
|
|
588
631
|
else:
|
|
589
|
-
go.pop(
|
|
632
|
+
go.pop()
|
|
590
633
|
|
|
591
|
-
delays = iter(
|
|
592
|
-
expiration = time.time(
|
|
593
|
-
delay = next(
|
|
634
|
+
delays = iter(delays)
|
|
635
|
+
expiration = time.time() + timeout
|
|
636
|
+
delay = next(delays)
|
|
594
637
|
while go:
|
|
595
|
-
yield repeated_attempt(
|
|
596
|
-
delay = next(
|
|
638
|
+
yield repeated_attempt(delay)
|
|
639
|
+
delay = next(delays, delay)
|
|
597
640
|
else:
|
|
641
|
+
|
|
598
642
|
@contextmanager
|
|
599
|
-
def single_attempt(
|
|
643
|
+
def single_attempt():
|
|
600
644
|
yield
|
|
601
645
|
|
|
602
|
-
yield single_attempt(
|
|
646
|
+
yield single_attempt()
|
|
647
|
+
|
|
603
648
|
|
|
604
649
|
# Decorator to retry tests that fail. Needs to be called with
|
|
605
650
|
# prepare=[tearDown, setUp] if the test class has tear down and set up that
|