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/server/wes/toil_backend.py
CHANGED
|
@@ -17,18 +17,9 @@ import os
|
|
|
17
17
|
import shutil
|
|
18
18
|
import uuid
|
|
19
19
|
from collections import Counter
|
|
20
|
+
from collections.abc import Generator
|
|
20
21
|
from contextlib import contextmanager
|
|
21
|
-
from typing import
|
|
22
|
-
Callable,
|
|
23
|
-
Dict,
|
|
24
|
-
Generator,
|
|
25
|
-
List,
|
|
26
|
-
Optional,
|
|
27
|
-
TextIO,
|
|
28
|
-
Tuple,
|
|
29
|
-
Type,
|
|
30
|
-
Union,
|
|
31
|
-
overload)
|
|
22
|
+
from typing import Any, Callable, Optional, TextIO, Union, overload
|
|
32
23
|
|
|
33
24
|
from flask import send_from_directory
|
|
34
25
|
from werkzeug.utils import redirect
|
|
@@ -38,22 +29,24 @@ import toil.server.wes.amazon_wes_utils as amazon_wes_utils
|
|
|
38
29
|
from toil.bus import JobStatus, replay_message_bus
|
|
39
30
|
from toil.lib.io import AtomicFileCreate
|
|
40
31
|
from toil.lib.threading import global_mutex
|
|
41
|
-
from toil.server.utils import
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
32
|
+
from toil.server.utils import WorkflowStateMachine, connect_to_workflow_state_store
|
|
33
|
+
from toil.server.wes.abstract_backend import (
|
|
34
|
+
OperationForbidden,
|
|
35
|
+
TaskLog,
|
|
36
|
+
VersionNotImplementedException,
|
|
37
|
+
WESBackend,
|
|
38
|
+
WorkflowConflictException,
|
|
39
|
+
WorkflowExecutionException,
|
|
40
|
+
WorkflowNotFoundException,
|
|
41
|
+
handle_errors,
|
|
42
|
+
)
|
|
51
43
|
from toil.server.wes.tasks import MultiprocessingTaskRunner, TaskRunner
|
|
52
44
|
from toil.version import baseVersion
|
|
53
45
|
|
|
54
46
|
logger = logging.getLogger(__name__)
|
|
55
47
|
logging.basicConfig(level=logging.INFO)
|
|
56
48
|
|
|
49
|
+
|
|
57
50
|
class ToilWorkflow:
|
|
58
51
|
def __init__(self, base_work_dir: str, state_store_url: str, run_id: str):
|
|
59
52
|
"""
|
|
@@ -115,24 +108,31 @@ class ToilWorkflow:
|
|
|
115
108
|
yield None
|
|
116
109
|
|
|
117
110
|
def exists(self) -> bool:
|
|
118
|
-
"""
|
|
111
|
+
"""Return True if the workflow run exists."""
|
|
119
112
|
return self.get_state() != "UNKNOWN"
|
|
120
113
|
|
|
121
114
|
def get_state(self) -> str:
|
|
122
|
-
"""
|
|
115
|
+
"""Return the state of the current run."""
|
|
123
116
|
return self.state_machine.get_current_state()
|
|
124
117
|
|
|
125
|
-
def check_on_run(self, task_runner:
|
|
118
|
+
def check_on_run(self, task_runner: type[TaskRunner]) -> None:
|
|
126
119
|
"""
|
|
127
120
|
Check to make sure nothing has gone wrong in the task runner for this
|
|
128
121
|
workflow. If something has, log, and fail the workflow with an error.
|
|
129
122
|
"""
|
|
130
|
-
if not task_runner.is_ok(self.run_id) and self.get_state() not in [
|
|
131
|
-
|
|
123
|
+
if not task_runner.is_ok(self.run_id) and self.get_state() not in [
|
|
124
|
+
"SYSTEM_ERROR",
|
|
125
|
+
"EXECUTOR_ERROR",
|
|
126
|
+
"COMPLETE",
|
|
127
|
+
"CANCELED",
|
|
128
|
+
]:
|
|
129
|
+
logger.error(
|
|
130
|
+
"Failing run %s because the task to run its leader crashed", self.run_id
|
|
131
|
+
)
|
|
132
132
|
self.state_machine.send_system_error()
|
|
133
133
|
|
|
134
134
|
def set_up_run(self) -> None:
|
|
135
|
-
"""
|
|
135
|
+
"""Set up necessary directories for the run."""
|
|
136
136
|
# Go to queued state
|
|
137
137
|
self.state_machine.send_enqueue()
|
|
138
138
|
|
|
@@ -140,11 +140,13 @@ class ToilWorkflow:
|
|
|
140
140
|
os.makedirs(self.exec_dir, exist_ok=True)
|
|
141
141
|
|
|
142
142
|
def clean_up(self) -> None:
|
|
143
|
-
"""
|
|
143
|
+
"""Clean directory and files related to the run."""
|
|
144
144
|
shutil.rmtree(self.scratch_dir)
|
|
145
145
|
# Don't remove state; state needs to persist forever.
|
|
146
146
|
|
|
147
|
-
def queue_run(
|
|
147
|
+
def queue_run(
|
|
148
|
+
self, task_runner: type[TaskRunner], request: dict[str, Any], options: list[str]
|
|
149
|
+
) -> None:
|
|
148
150
|
"""This workflow should be ready to run. Hand this to the task system."""
|
|
149
151
|
with open(os.path.join(self.scratch_dir, "request.json"), "w") as f:
|
|
150
152
|
# Save the request to disk for get_run_log()
|
|
@@ -152,8 +154,16 @@ class ToilWorkflow:
|
|
|
152
154
|
|
|
153
155
|
try:
|
|
154
156
|
# Run the task. Set the task ID the same as our run ID
|
|
155
|
-
task_runner.run(
|
|
156
|
-
|
|
157
|
+
task_runner.run(
|
|
158
|
+
args=(
|
|
159
|
+
self.base_scratch_dir,
|
|
160
|
+
self.state_store_url,
|
|
161
|
+
self.run_id,
|
|
162
|
+
request,
|
|
163
|
+
options,
|
|
164
|
+
),
|
|
165
|
+
task_id=self.run_id,
|
|
166
|
+
)
|
|
157
167
|
except Exception:
|
|
158
168
|
# Celery or the broker might be down
|
|
159
169
|
self.state_machine.send_system_error()
|
|
@@ -185,23 +195,28 @@ class ToilWorkflow:
|
|
|
185
195
|
Return the path to the standard output log, relative to the run's
|
|
186
196
|
scratch_dir, or None if it doesn't exist.
|
|
187
197
|
"""
|
|
188
|
-
return self._get_scratch_file_path(
|
|
198
|
+
return self._get_scratch_file_path("stdout")
|
|
189
199
|
|
|
190
200
|
def get_stderr_path(self) -> Optional[str]:
|
|
191
201
|
"""
|
|
192
202
|
Return the path to the standard output log, relative to the run's
|
|
193
203
|
scratch_dir, or None if it doesn't exist.
|
|
194
204
|
"""
|
|
195
|
-
return self._get_scratch_file_path(
|
|
205
|
+
return self._get_scratch_file_path("stderr")
|
|
196
206
|
|
|
197
207
|
def get_messages_path(self) -> Optional[str]:
|
|
198
208
|
"""
|
|
199
209
|
Return the path to the bus message log, relative to the run's
|
|
200
210
|
scratch_dir, or None if it doesn't exist.
|
|
201
211
|
"""
|
|
202
|
-
return self._get_scratch_file_path(
|
|
212
|
+
return self._get_scratch_file_path("bus_messages")
|
|
203
213
|
|
|
204
|
-
def get_task_logs(
|
|
214
|
+
def get_task_logs(
|
|
215
|
+
self,
|
|
216
|
+
filter_function: Optional[
|
|
217
|
+
Callable[[TaskLog, JobStatus], Optional[TaskLog]]
|
|
218
|
+
] = None,
|
|
219
|
+
) -> list[dict[str, Union[str, int, None]]]:
|
|
205
220
|
"""
|
|
206
221
|
Return all the task log objects for the individual tasks in the workflow.
|
|
207
222
|
|
|
@@ -226,9 +241,12 @@ class ToilWorkflow:
|
|
|
226
241
|
abs_path = os.path.join(self.scratch_dir, path)
|
|
227
242
|
job_statuses = replay_message_bus(abs_path)
|
|
228
243
|
# Compose log objects from recovered job info.
|
|
229
|
-
logs:
|
|
244
|
+
logs: list[TaskLog] = []
|
|
230
245
|
for job_status in job_statuses.values():
|
|
231
|
-
task: Optional[TaskLog] = {
|
|
246
|
+
task: Optional[TaskLog] = {
|
|
247
|
+
"name": job_status.name,
|
|
248
|
+
"exit_code": job_status.exit_code,
|
|
249
|
+
}
|
|
232
250
|
if filter_function is not None:
|
|
233
251
|
# Convince MyPy the task is set
|
|
234
252
|
assert task is not None
|
|
@@ -236,22 +254,26 @@ class ToilWorkflow:
|
|
|
236
254
|
task = filter_function(task, job_status)
|
|
237
255
|
if task is not None:
|
|
238
256
|
logs.append(task)
|
|
239
|
-
logger.info(
|
|
257
|
+
logger.info("Recovered task logs: %s", logs)
|
|
240
258
|
return logs
|
|
241
259
|
# TODO: times, log files, AWS Batch IDs if any, names from the workflow instead of IDs, commands
|
|
242
260
|
|
|
243
261
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
262
|
class ToilBackend(WESBackend):
|
|
248
263
|
"""
|
|
249
264
|
WES backend implemented for Toil to run CWL, WDL, or Toil workflows. This
|
|
250
265
|
class is responsible for validating and executing submitted workflows.
|
|
251
266
|
"""
|
|
252
267
|
|
|
253
|
-
def __init__(
|
|
254
|
-
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
work_dir: str,
|
|
271
|
+
state_store: Optional[str],
|
|
272
|
+
options: list[str],
|
|
273
|
+
dest_bucket_base: Optional[str],
|
|
274
|
+
bypass_celery: bool = False,
|
|
275
|
+
wes_dialect: str = "standard",
|
|
276
|
+
) -> None:
|
|
255
277
|
"""
|
|
256
278
|
Make a new ToilBackend for serving WES.
|
|
257
279
|
|
|
@@ -274,19 +296,21 @@ class ToilBackend(WESBackend):
|
|
|
274
296
|
acceptable in any dialect.
|
|
275
297
|
"""
|
|
276
298
|
for opt in options:
|
|
277
|
-
if not opt.startswith(
|
|
299
|
+
if not opt.startswith("-"):
|
|
278
300
|
# We don't allow a value to be set across multiple arguments
|
|
279
301
|
# that would need to remain in the same order.
|
|
280
|
-
raise ValueError(f
|
|
302
|
+
raise ValueError(f"Option {opt} does not begin with -")
|
|
281
303
|
super().__init__(options)
|
|
282
304
|
|
|
283
305
|
# How should we generate run IDs? We apply a prefix so that we can tell
|
|
284
306
|
# what things in our work directory suggest that runs exist and what
|
|
285
307
|
# things don't.
|
|
286
|
-
self.run_id_prefix =
|
|
308
|
+
self.run_id_prefix = "run-"
|
|
287
309
|
|
|
288
310
|
# Use this to run Celery tasks so we can swap it out for testing.
|
|
289
|
-
self.task_runner =
|
|
311
|
+
self.task_runner = (
|
|
312
|
+
TaskRunner if not bypass_celery else MultiprocessingTaskRunner
|
|
313
|
+
)
|
|
290
314
|
logger.info("Using task runner: %s", self.task_runner)
|
|
291
315
|
|
|
292
316
|
# Record if we need to limit our WES responses for a particular
|
|
@@ -304,7 +328,7 @@ class ToilBackend(WESBackend):
|
|
|
304
328
|
|
|
305
329
|
if state_store is None:
|
|
306
330
|
# Store workflow metadata under the work_dir.
|
|
307
|
-
self.state_store_url = os.path.join(self.work_dir,
|
|
331
|
+
self.state_store_url = os.path.join(self.work_dir, "state_store")
|
|
308
332
|
else:
|
|
309
333
|
# Use the provided value
|
|
310
334
|
self.state_store_url = state_store
|
|
@@ -331,14 +355,14 @@ class ToilBackend(WESBackend):
|
|
|
331
355
|
pass
|
|
332
356
|
# Assign an ID to the work directory storage.
|
|
333
357
|
work_dir_id = None
|
|
334
|
-
work_dir_id_file = os.path.join(self.work_dir,
|
|
358
|
+
work_dir_id_file = os.path.join(self.work_dir, "id.txt")
|
|
335
359
|
if os.path.exists(work_dir_id_file):
|
|
336
360
|
# An ID is assigned already
|
|
337
361
|
with open(work_dir_id_file) as f:
|
|
338
362
|
work_dir_id = uuid.UUID(f.readline().strip())
|
|
339
363
|
else:
|
|
340
364
|
# We need to try and assign an ID.
|
|
341
|
-
with global_mutex(self.work_dir,
|
|
365
|
+
with global_mutex(self.work_dir, "id-assignment"):
|
|
342
366
|
# We need to synchronize with other processes starting up to
|
|
343
367
|
# make sure we agree on an ID.
|
|
344
368
|
if os.path.exists(work_dir_id_file):
|
|
@@ -350,7 +374,7 @@ class ToilBackend(WESBackend):
|
|
|
350
374
|
with AtomicFileCreate(work_dir_id_file) as temp_file:
|
|
351
375
|
# Still need to be atomic here or people not locking
|
|
352
376
|
# will see an incomplete file.
|
|
353
|
-
with open(temp_file,
|
|
377
|
+
with open(temp_file, "w") as f:
|
|
354
378
|
f.write(str(work_dir_id))
|
|
355
379
|
# Now combine into one ID
|
|
356
380
|
if boot_id is not None:
|
|
@@ -359,14 +383,15 @@ class ToilBackend(WESBackend):
|
|
|
359
383
|
self.server_id = str(work_dir_id)
|
|
360
384
|
logger.info("Using server ID: %s", self.server_id)
|
|
361
385
|
|
|
362
|
-
|
|
363
386
|
self.supported_versions = {
|
|
364
387
|
"py": ["3.7", "3.8", "3.9"],
|
|
365
388
|
"cwl": ["v1.0", "v1.1", "v1.2"],
|
|
366
|
-
"wdl": ["draft-2", "1.0"]
|
|
389
|
+
"wdl": ["draft-2", "1.0"],
|
|
367
390
|
}
|
|
368
391
|
|
|
369
|
-
def _get_run(
|
|
392
|
+
def _get_run(
|
|
393
|
+
self, run_id: str, should_exists: Optional[bool] = None
|
|
394
|
+
) -> ToilWorkflow:
|
|
370
395
|
"""
|
|
371
396
|
Helper method to instantiate a ToilWorkflow object.
|
|
372
397
|
|
|
@@ -387,24 +412,32 @@ class ToilBackend(WESBackend):
|
|
|
387
412
|
# TODO: Implement multiple servers working together.
|
|
388
413
|
owning_server = run.fetch_state("server_id")
|
|
389
414
|
apparent_state = run.get_state()
|
|
390
|
-
if (
|
|
391
|
-
|
|
415
|
+
if (
|
|
416
|
+
apparent_state
|
|
417
|
+
not in ("UNKNOWN", "COMPLETE", "EXECUTOR_ERROR", "SYSTEM_ERROR", "CANCELED")
|
|
418
|
+
and owning_server != self.server_id
|
|
419
|
+
):
|
|
392
420
|
|
|
393
421
|
# This workflow is in a state that suggests it is doing something
|
|
394
422
|
# but it appears to belong to a previous incarnation of the server,
|
|
395
423
|
# and so its Celery is probably gone. Put it into system error
|
|
396
424
|
# state if possible.
|
|
397
|
-
logger.warning(
|
|
398
|
-
|
|
399
|
-
|
|
425
|
+
logger.warning(
|
|
426
|
+
"Run %s in state %s appears to belong to server %s and not us, server %s. "
|
|
427
|
+
"Its server is probably gone. Failing the workflow!",
|
|
428
|
+
run_id,
|
|
429
|
+
apparent_state,
|
|
430
|
+
owning_server,
|
|
431
|
+
self.server_id,
|
|
432
|
+
)
|
|
400
433
|
run.state_machine.send_system_error()
|
|
401
434
|
|
|
402
435
|
# Poll to make sure the run is not broken
|
|
403
436
|
run.check_on_run(self.task_runner)
|
|
404
437
|
return run
|
|
405
438
|
|
|
406
|
-
def get_runs(self) -> Generator[
|
|
407
|
-
"""
|
|
439
|
+
def get_runs(self) -> Generator[tuple[str, str], None, None]:
|
|
440
|
+
"""A generator of a list of run ids and their state."""
|
|
408
441
|
if not os.path.exists(self.work_dir):
|
|
409
442
|
return
|
|
410
443
|
|
|
@@ -423,25 +456,24 @@ class ToilBackend(WESBackend):
|
|
|
423
456
|
return self._get_run(run_id, should_exists=True).get_state()
|
|
424
457
|
|
|
425
458
|
@handle_errors
|
|
426
|
-
def get_service_info(self) ->
|
|
427
|
-
"""
|
|
459
|
+
def get_service_info(self) -> dict[str, Any]:
|
|
460
|
+
"""Get information about the Workflow Execution Service."""
|
|
428
461
|
|
|
429
462
|
state_counts = Counter(state for _, state in self.get_runs())
|
|
430
463
|
|
|
431
464
|
engine_parameters = []
|
|
432
465
|
for option in self.options:
|
|
433
|
-
if
|
|
466
|
+
if "=" not in option: # flags like "--logDebug"
|
|
434
467
|
k, v = option, None
|
|
435
468
|
else:
|
|
436
|
-
k, v = option.split(
|
|
469
|
+
k, v = option.split("=", 1)
|
|
437
470
|
engine_parameters.append((k, v))
|
|
438
471
|
|
|
439
472
|
return {
|
|
440
473
|
"version": baseVersion,
|
|
441
474
|
"workflow_type_versions": {
|
|
442
|
-
k: {
|
|
443
|
-
|
|
444
|
-
} for k, v in self.supported_versions.items()
|
|
475
|
+
k: {"workflow_type_version": v}
|
|
476
|
+
for k, v in self.supported_versions.items()
|
|
445
477
|
},
|
|
446
478
|
"supported_wes_versions": ["1.0.0"],
|
|
447
479
|
"supported_filesystem_protocols": ["file", "http", "https"],
|
|
@@ -449,10 +481,7 @@ class ToilBackend(WESBackend):
|
|
|
449
481
|
# TODO: How can we report --destBucket here, since we pass it only
|
|
450
482
|
# for CWL workflows?
|
|
451
483
|
"default_workflow_engine_parameters": [
|
|
452
|
-
{
|
|
453
|
-
"name": key,
|
|
454
|
-
"default_value": value
|
|
455
|
-
}
|
|
484
|
+
{"name": key, "default_value": value}
|
|
456
485
|
for key, value in engine_parameters
|
|
457
486
|
],
|
|
458
487
|
"system_state_counts": state_counts,
|
|
@@ -460,22 +489,21 @@ class ToilBackend(WESBackend):
|
|
|
460
489
|
}
|
|
461
490
|
|
|
462
491
|
@handle_errors
|
|
463
|
-
def list_runs(
|
|
464
|
-
|
|
492
|
+
def list_runs(
|
|
493
|
+
self, page_size: Optional[int] = None, page_token: Optional[str] = None
|
|
494
|
+
) -> dict[str, Any]:
|
|
495
|
+
"""List the workflow runs."""
|
|
465
496
|
# TODO: implement pagination
|
|
466
497
|
return {
|
|
467
498
|
"workflows": [
|
|
468
|
-
{
|
|
469
|
-
"run_id": run_id,
|
|
470
|
-
"state": state
|
|
471
|
-
} for run_id, state in self.get_runs()
|
|
499
|
+
{"run_id": run_id, "state": state} for run_id, state in self.get_runs()
|
|
472
500
|
],
|
|
473
|
-
"next_page_token": ""
|
|
501
|
+
"next_page_token": "",
|
|
474
502
|
}
|
|
475
503
|
|
|
476
504
|
@handle_errors
|
|
477
|
-
def run_workflow(self) ->
|
|
478
|
-
"""
|
|
505
|
+
def run_workflow(self) -> dict[str, str]:
|
|
506
|
+
"""Run a workflow."""
|
|
479
507
|
run_id = self.run_id_prefix + uuid.uuid4().hex
|
|
480
508
|
run = self._get_run(run_id, should_exists=False)
|
|
481
509
|
|
|
@@ -491,7 +519,11 @@ class ToilBackend(WESBackend):
|
|
|
491
519
|
run.clean_up()
|
|
492
520
|
raise
|
|
493
521
|
|
|
494
|
-
logger.info(
|
|
522
|
+
logger.info(
|
|
523
|
+
"Received workflow run request %s with parameters: %s",
|
|
524
|
+
run_id,
|
|
525
|
+
list(request.keys()),
|
|
526
|
+
)
|
|
495
527
|
|
|
496
528
|
wf_type = request["workflow_type"].lower().strip()
|
|
497
529
|
version = request["workflow_type_version"]
|
|
@@ -514,21 +546,25 @@ class ToilBackend(WESBackend):
|
|
|
514
546
|
workflow_options = list(self.options)
|
|
515
547
|
if wf_type == "cwl" and self.dest_bucket_base:
|
|
516
548
|
# Output to a directory under out base destination bucket URL.
|
|
517
|
-
workflow_options.append(
|
|
549
|
+
workflow_options.append(
|
|
550
|
+
"--destBucket=" + os.path.join(self.dest_bucket_base, run_id)
|
|
551
|
+
)
|
|
518
552
|
# Tell it to dump its messages to a file.
|
|
519
553
|
# TODO: automatically sync file names with accessors somehow.
|
|
520
|
-
workflow_options.append(
|
|
554
|
+
workflow_options.append(
|
|
555
|
+
"--writeMessages=" + os.path.join(run.scratch_dir, "bus_messages")
|
|
556
|
+
)
|
|
521
557
|
|
|
522
|
-
logger.info(
|
|
558
|
+
logger.info(
|
|
559
|
+
f"Putting workflow {run_id} into the queue. Waiting to be picked up..."
|
|
560
|
+
)
|
|
523
561
|
run.queue_run(self.task_runner, request, options=workflow_options)
|
|
524
562
|
|
|
525
|
-
return {
|
|
526
|
-
"run_id": run_id
|
|
527
|
-
}
|
|
563
|
+
return {"run_id": run_id}
|
|
528
564
|
|
|
529
565
|
@handle_errors
|
|
530
|
-
def get_run_log(self, run_id: str) ->
|
|
531
|
-
"""
|
|
566
|
+
def get_run_log(self, run_id: str) -> dict[str, Any]:
|
|
567
|
+
"""Get detailed info about a workflow run."""
|
|
532
568
|
run = self._get_run(run_id, should_exists=True)
|
|
533
569
|
state = run.get_state()
|
|
534
570
|
|
|
@@ -550,7 +586,7 @@ class ToilBackend(WESBackend):
|
|
|
550
586
|
# path under that hostname. So we need to use a relative URL to the
|
|
551
587
|
# logs.
|
|
552
588
|
stdout = f"../../../../toil/wes/v1/logs/{run_id}/stdout"
|
|
553
|
-
stderr =""
|
|
589
|
+
stderr = ""
|
|
554
590
|
if run.get_stderr_path() is not None:
|
|
555
591
|
# We have a standard error link.
|
|
556
592
|
stderr = f"../../../../toil/wes/v1/logs/{run_id}/stderr"
|
|
@@ -564,7 +600,9 @@ class ToilBackend(WESBackend):
|
|
|
564
600
|
filter_function = amazon_wes_utils.task_filter
|
|
565
601
|
else:
|
|
566
602
|
# We can emit any standard-compliant WES tasks
|
|
567
|
-
logger.info(
|
|
603
|
+
logger.info(
|
|
604
|
+
"WES dialect %s does not require transforming tasks", self.wes_dialect
|
|
605
|
+
)
|
|
568
606
|
filter_function = None
|
|
569
607
|
task_logs = run.get_task_logs(filter_function=filter_function)
|
|
570
608
|
|
|
@@ -589,8 +627,8 @@ class ToilBackend(WESBackend):
|
|
|
589
627
|
}
|
|
590
628
|
|
|
591
629
|
@handle_errors
|
|
592
|
-
def cancel_run(self, run_id: str) ->
|
|
593
|
-
"""
|
|
630
|
+
def cancel_run(self, run_id: str) -> dict[str, str]:
|
|
631
|
+
"""Cancel a running workflow."""
|
|
594
632
|
run = self._get_run(run_id, should_exists=True)
|
|
595
633
|
|
|
596
634
|
# Do some preflight checks on the current state.
|
|
@@ -598,31 +636,30 @@ class ToilBackend(WESBackend):
|
|
|
598
636
|
state = run.get_state()
|
|
599
637
|
if state in ("CANCELING", "CANCELED", "COMPLETE"):
|
|
600
638
|
# We don't need to do anything.
|
|
601
|
-
logger.warning(
|
|
639
|
+
logger.warning(
|
|
640
|
+
f"A user is attempting to cancel a workflow in state: '{state}'."
|
|
641
|
+
)
|
|
602
642
|
elif state in ("EXECUTOR_ERROR", "SYSTEM_ERROR"):
|
|
603
643
|
# Something went wrong. Let the user know.
|
|
604
|
-
raise OperationForbidden(
|
|
644
|
+
raise OperationForbidden(
|
|
645
|
+
f"Workflow is in state: '{state}', which cannot be cancelled."
|
|
646
|
+
)
|
|
605
647
|
else:
|
|
606
648
|
# Go to canceling state if allowed
|
|
607
649
|
run.state_machine.send_cancel()
|
|
608
650
|
# Stop the run task if it is there.
|
|
609
651
|
self.task_runner.cancel(run_id)
|
|
610
652
|
|
|
611
|
-
return {
|
|
612
|
-
"run_id": run_id
|
|
613
|
-
}
|
|
653
|
+
return {"run_id": run_id}
|
|
614
654
|
|
|
615
655
|
@handle_errors
|
|
616
|
-
def get_run_status(self, run_id: str) ->
|
|
656
|
+
def get_run_status(self, run_id: str) -> dict[str, str]:
|
|
617
657
|
"""
|
|
618
658
|
Get quick status info about a workflow run, returning a simple result
|
|
619
659
|
with the overall state of the workflow run.
|
|
620
660
|
"""
|
|
621
661
|
|
|
622
|
-
return {
|
|
623
|
-
"run_id": run_id,
|
|
624
|
-
"state": self.get_state(run_id)
|
|
625
|
-
}
|
|
662
|
+
return {"run_id": run_id, "state": self.get_state(run_id)}
|
|
626
663
|
|
|
627
664
|
# Toil custom endpoints that are not part of the GA4GH WES spec
|
|
628
665
|
|
|
@@ -665,6 +702,4 @@ class ToilBackend(WESBackend):
|
|
|
665
702
|
Provide a sensible result for / other than 404.
|
|
666
703
|
"""
|
|
667
704
|
# For now just go to the service info endpoint
|
|
668
|
-
return redirect(
|
|
669
|
-
|
|
670
|
-
|
|
705
|
+
return redirect("ga4gh/wes/v1/service-info", code=302)
|
toil/server/wsgi_app.py
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
-
from typing import Any,
|
|
14
|
+
from typing import Any, Optional
|
|
15
15
|
|
|
16
16
|
from gunicorn.app.base import BaseApplication # type: ignore
|
|
17
17
|
|
|
@@ -29,7 +29,8 @@ class GunicornApplication(BaseApplication): # type: ignore
|
|
|
29
29
|
|
|
30
30
|
For more details, see: https://docs.gunicorn.org/en/latest/custom.html
|
|
31
31
|
"""
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
def __init__(self, app: object, options: Optional[dict[str, Any]] = None):
|
|
33
34
|
self.options = options or {}
|
|
34
35
|
self.application = app
|
|
35
36
|
super().__init__()
|
|
@@ -51,7 +52,7 @@ class GunicornApplication(BaseApplication): # type: ignore
|
|
|
51
52
|
return self.application
|
|
52
53
|
|
|
53
54
|
|
|
54
|
-
def run_app(app: object, options: Optional[
|
|
55
|
+
def run_app(app: object, options: Optional[dict[str, Any]] = None) -> None:
|
|
55
56
|
"""
|
|
56
57
|
Run a Gunicorn WSGI server.
|
|
57
58
|
"""
|