fractal-server 2.11.0a10__py3-none-any.whl → 2.12.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.
- fractal_server/__init__.py +1 -1
- fractal_server/app/models/__init__.py +0 -2
- fractal_server/app/models/linkuserproject.py +0 -9
- fractal_server/app/models/v2/dataset.py +0 -4
- fractal_server/app/models/v2/workflowtask.py +0 -4
- fractal_server/app/routes/aux/_job.py +1 -3
- fractal_server/app/runner/executors/slurm/ssh/executor.py +9 -6
- fractal_server/app/runner/executors/slurm/sudo/executor.py +1 -5
- fractal_server/app/runner/filenames.py +0 -2
- fractal_server/app/runner/shutdown.py +3 -27
- fractal_server/app/schemas/_validators.py +0 -19
- fractal_server/config.py +1 -15
- fractal_server/main.py +1 -12
- fractal_server/migrations/versions/1eac13a26c83_drop_v1_tables.py +67 -0
- fractal_server/migrations/versions/af8673379a5c_drop_old_filter_columns.py +54 -0
- fractal_server/string_tools.py +0 -21
- fractal_server/tasks/utils.py +0 -28
- {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0.dist-info}/METADATA +1 -1
- {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0.dist-info}/RECORD +22 -66
- fractal_server/app/models/v1/__init__.py +0 -13
- fractal_server/app/models/v1/dataset.py +0 -71
- fractal_server/app/models/v1/job.py +0 -101
- fractal_server/app/models/v1/project.py +0 -29
- fractal_server/app/models/v1/state.py +0 -34
- fractal_server/app/models/v1/task.py +0 -85
- fractal_server/app/models/v1/workflow.py +0 -133
- fractal_server/app/routes/admin/v1.py +0 -377
- fractal_server/app/routes/api/v1/__init__.py +0 -26
- fractal_server/app/routes/api/v1/_aux_functions.py +0 -478
- fractal_server/app/routes/api/v1/dataset.py +0 -554
- fractal_server/app/routes/api/v1/job.py +0 -195
- fractal_server/app/routes/api/v1/project.py +0 -475
- fractal_server/app/routes/api/v1/task.py +0 -203
- fractal_server/app/routes/api/v1/task_collection.py +0 -239
- fractal_server/app/routes/api/v1/workflow.py +0 -355
- fractal_server/app/routes/api/v1/workflowtask.py +0 -187
- fractal_server/app/runner/async_wrap_v1.py +0 -27
- fractal_server/app/runner/v1/__init__.py +0 -415
- fractal_server/app/runner/v1/_common.py +0 -620
- fractal_server/app/runner/v1/_local/__init__.py +0 -186
- fractal_server/app/runner/v1/_local/_local_config.py +0 -105
- fractal_server/app/runner/v1/_local/_submit_setup.py +0 -48
- fractal_server/app/runner/v1/_local/executor.py +0 -100
- fractal_server/app/runner/v1/_slurm/__init__.py +0 -312
- fractal_server/app/runner/v1/_slurm/_submit_setup.py +0 -81
- fractal_server/app/runner/v1/_slurm/get_slurm_config.py +0 -163
- fractal_server/app/runner/v1/common.py +0 -117
- fractal_server/app/runner/v1/handle_failed_job.py +0 -141
- fractal_server/app/schemas/v1/__init__.py +0 -37
- fractal_server/app/schemas/v1/applyworkflow.py +0 -161
- fractal_server/app/schemas/v1/dataset.py +0 -165
- fractal_server/app/schemas/v1/dumps.py +0 -64
- fractal_server/app/schemas/v1/manifest.py +0 -126
- fractal_server/app/schemas/v1/project.py +0 -66
- fractal_server/app/schemas/v1/state.py +0 -18
- fractal_server/app/schemas/v1/task.py +0 -167
- fractal_server/app/schemas/v1/task_collection.py +0 -110
- fractal_server/app/schemas/v1/workflow.py +0 -212
- fractal_server/data_migrations/2_11_0.py +0 -168
- fractal_server/tasks/v1/_TaskCollectPip.py +0 -103
- fractal_server/tasks/v1/__init__.py +0 -0
- fractal_server/tasks/v1/background_operations.py +0 -352
- fractal_server/tasks/v1/endpoint_operations.py +0 -156
- fractal_server/tasks/v1/get_collection_data.py +0 -14
- fractal_server/tasks/v1/utils.py +0 -67
- {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.11.0a10.dist-info → fractal_server-2.12.0.dist-info}/entry_points.txt +0 -0
@@ -1,620 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
Common utilities and routines for runner backends (private API)
|
3
|
-
|
4
|
-
This module includes utilities and routines that are of use to implement
|
5
|
-
runner backends and that should not be exposed outside of the runner
|
6
|
-
subsystem.
|
7
|
-
"""
|
8
|
-
import json
|
9
|
-
import shutil
|
10
|
-
import subprocess # nosec
|
11
|
-
import traceback
|
12
|
-
from concurrent.futures import Executor
|
13
|
-
from copy import deepcopy
|
14
|
-
from functools import partial
|
15
|
-
from pathlib import Path
|
16
|
-
from shlex import split as shlex_split
|
17
|
-
from typing import Any
|
18
|
-
from typing import Callable
|
19
|
-
from typing import Optional
|
20
|
-
|
21
|
-
from ....config import get_settings
|
22
|
-
from ....logger import get_logger
|
23
|
-
from ....syringe import Inject
|
24
|
-
from ...models.v1 import Task
|
25
|
-
from ...models.v1 import WorkflowTask
|
26
|
-
from ...schemas.v1 import WorkflowTaskStatusTypeV1
|
27
|
-
from ..exceptions import JobExecutionError
|
28
|
-
from ..exceptions import TaskExecutionError
|
29
|
-
from .common import TaskParameters
|
30
|
-
from .common import write_args_file
|
31
|
-
from fractal_server.app.runner.filenames import HISTORY_FILENAME_V1
|
32
|
-
from fractal_server.app.runner.filenames import METADATA_FILENAME_V1
|
33
|
-
from fractal_server.app.runner.task_files import get_task_file_paths
|
34
|
-
from fractal_server.string_tools import validate_cmd
|
35
|
-
|
36
|
-
|
37
|
-
def no_op_submit_setup_call(
|
38
|
-
*,
|
39
|
-
wftask: WorkflowTask,
|
40
|
-
workflow_dir_local: Path,
|
41
|
-
workflow_dir_remote: Path,
|
42
|
-
) -> dict:
|
43
|
-
"""
|
44
|
-
Default (no-operation) interface of submit_setup_call.
|
45
|
-
"""
|
46
|
-
return {}
|
47
|
-
|
48
|
-
|
49
|
-
def _task_needs_image_list(_task: Task) -> bool:
|
50
|
-
"""
|
51
|
-
Whether a task requires `metadata["image"]` in its `args.json` file.
|
52
|
-
|
53
|
-
For details see
|
54
|
-
https://github.com/fractal-analytics-platform/fractal-server/issues/1237
|
55
|
-
|
56
|
-
Args:
|
57
|
-
_task: The task to be checked.
|
58
|
-
"""
|
59
|
-
settings = Inject(get_settings)
|
60
|
-
exception_task_names = settings.FRACTAL_RUNNER_TASKS_INCLUDE_IMAGE.split(
|
61
|
-
";"
|
62
|
-
)
|
63
|
-
if _task.name in exception_task_names:
|
64
|
-
return True
|
65
|
-
else:
|
66
|
-
return False
|
67
|
-
|
68
|
-
|
69
|
-
def _call_command_wrapper(cmd: str, stdout: Path, stderr: Path) -> None:
|
70
|
-
"""
|
71
|
-
Call a command and write its stdout and stderr to files
|
72
|
-
|
73
|
-
Raises:
|
74
|
-
TaskExecutionError: If the `subprocess.run` call returns a positive
|
75
|
-
exit code
|
76
|
-
JobExecutionError: If the `subprocess.run` call returns a negative
|
77
|
-
exit code (e.g. due to the subprocess receiving a
|
78
|
-
TERM or KILL signal)
|
79
|
-
"""
|
80
|
-
|
81
|
-
validate_cmd(cmd)
|
82
|
-
# Verify that task command is executable
|
83
|
-
if shutil.which(shlex_split(cmd)[0]) is None:
|
84
|
-
msg = (
|
85
|
-
f'Command "{shlex_split(cmd)[0]}" is not valid. '
|
86
|
-
"Hint: make sure that it is executable."
|
87
|
-
)
|
88
|
-
raise TaskExecutionError(msg)
|
89
|
-
|
90
|
-
fp_stdout = open(stdout, "w")
|
91
|
-
fp_stderr = open(stderr, "w")
|
92
|
-
try:
|
93
|
-
result = subprocess.run( # nosec
|
94
|
-
shlex_split(cmd),
|
95
|
-
stderr=fp_stderr,
|
96
|
-
stdout=fp_stdout,
|
97
|
-
)
|
98
|
-
except Exception as e:
|
99
|
-
raise e
|
100
|
-
finally:
|
101
|
-
fp_stdout.close()
|
102
|
-
fp_stderr.close()
|
103
|
-
|
104
|
-
if result.returncode > 0:
|
105
|
-
with stderr.open("r") as fp_stderr:
|
106
|
-
err = fp_stderr.read()
|
107
|
-
raise TaskExecutionError(err)
|
108
|
-
elif result.returncode < 0:
|
109
|
-
raise JobExecutionError(
|
110
|
-
info=f"Task failed with returncode={result.returncode}"
|
111
|
-
)
|
112
|
-
|
113
|
-
|
114
|
-
def call_single_task(
|
115
|
-
*,
|
116
|
-
wftask: WorkflowTask,
|
117
|
-
task_pars: TaskParameters,
|
118
|
-
workflow_dir_local: Path,
|
119
|
-
workflow_dir_remote: Optional[Path] = None,
|
120
|
-
logger_name: Optional[str] = None,
|
121
|
-
) -> TaskParameters:
|
122
|
-
"""
|
123
|
-
Call a single task
|
124
|
-
|
125
|
-
This assembles the runner arguments (input_paths, output_path, ...) and
|
126
|
-
wftask arguments (i.e., arguments that are specific to the WorkflowTask,
|
127
|
-
such as message or index in the dummy task), writes them to file, call the
|
128
|
-
task executable command passing the arguments file as an input and
|
129
|
-
assembles the output.
|
130
|
-
|
131
|
-
**Note**: This function is directly submitted to a
|
132
|
-
`concurrent.futures`-compatible executor, as in
|
133
|
-
|
134
|
-
some_future = executor.submit(call_single_task, ...)
|
135
|
-
|
136
|
-
If the executor then impersonates another user (as in the
|
137
|
-
`FractalSlurmExecutor`), this function is run by that user. For this
|
138
|
-
reason, it should not write any file to `workflow_dir_local`, or it may
|
139
|
-
yield permission errors.
|
140
|
-
|
141
|
-
Args:
|
142
|
-
wftask:
|
143
|
-
The workflow task to be called. This includes task specific
|
144
|
-
arguments via the wftask.args attribute.
|
145
|
-
task_pars:
|
146
|
-
The parameters required to run the task which are not specific to
|
147
|
-
the task, e.g., I/O paths.
|
148
|
-
workflow_dir_local:
|
149
|
-
The server-side working directory for workflow execution.
|
150
|
-
workflow_dir_remote:
|
151
|
-
The user-side working directory for workflow execution (only
|
152
|
-
relevant for multi-user executors). If `None`, it is set to be
|
153
|
-
equal to `workflow_dir_remote`.
|
154
|
-
logger_name:
|
155
|
-
Name of the logger
|
156
|
-
|
157
|
-
Returns:
|
158
|
-
out_task_parameters:
|
159
|
-
A TaskParameters in which the previous output becomes the input
|
160
|
-
and where metadata is the metadata dictionary returned by the task
|
161
|
-
being called.
|
162
|
-
|
163
|
-
Raises:
|
164
|
-
TaskExecutionError: If the wrapped task raises a task-related error.
|
165
|
-
This function is responsible of adding debugging
|
166
|
-
information to the TaskExecutionError, such as task
|
167
|
-
order and name.
|
168
|
-
JobExecutionError: If the wrapped task raises a job-related error.
|
169
|
-
"""
|
170
|
-
|
171
|
-
logger = get_logger(logger_name)
|
172
|
-
|
173
|
-
if not workflow_dir_remote:
|
174
|
-
workflow_dir_remote = workflow_dir_local
|
175
|
-
|
176
|
-
task_files = get_task_file_paths(
|
177
|
-
workflow_dir_local=workflow_dir_local,
|
178
|
-
workflow_dir_remote=workflow_dir_remote,
|
179
|
-
task_order=wftask.order,
|
180
|
-
task_name=wftask.task.name,
|
181
|
-
)
|
182
|
-
|
183
|
-
# write args file (by assembling task_pars and wftask.args)
|
184
|
-
write_args_file(
|
185
|
-
task_pars.dict(exclude={"history"}),
|
186
|
-
wftask.args or {},
|
187
|
-
path=task_files.args,
|
188
|
-
)
|
189
|
-
|
190
|
-
# assemble full command
|
191
|
-
cmd = (
|
192
|
-
f"{wftask.task.command} -j {task_files.args} "
|
193
|
-
f"--metadata-out {task_files.metadiff}"
|
194
|
-
)
|
195
|
-
|
196
|
-
try:
|
197
|
-
_call_command_wrapper(
|
198
|
-
cmd, stdout=task_files.out, stderr=task_files.err
|
199
|
-
)
|
200
|
-
except TaskExecutionError as e:
|
201
|
-
e.workflow_task_order = wftask.order
|
202
|
-
e.workflow_task_id = wftask.id
|
203
|
-
e.task_name = wftask.task.name
|
204
|
-
raise e
|
205
|
-
|
206
|
-
# This try/except block covers the case of a task that ran successfully but
|
207
|
-
# did not write the expected metadiff file (ref fractal-server issue #854).
|
208
|
-
try:
|
209
|
-
with task_files.metadiff.open("r") as f_metadiff:
|
210
|
-
diff_metadata = json.load(f_metadiff)
|
211
|
-
except FileNotFoundError as e:
|
212
|
-
logger.warning(
|
213
|
-
f"Skip collection of updated metadata. Original error: {str(e)}"
|
214
|
-
)
|
215
|
-
diff_metadata = {}
|
216
|
-
|
217
|
-
# Cover the case where the task wrote `null`, rather than a valid
|
218
|
-
# dictionary (ref fractal-server issue #878).
|
219
|
-
if diff_metadata is None:
|
220
|
-
diff_metadata = {}
|
221
|
-
|
222
|
-
# Prepare updated_metadata
|
223
|
-
updated_metadata = task_pars.metadata.copy()
|
224
|
-
updated_metadata.update(diff_metadata)
|
225
|
-
# Prepare updated_history (note: the expected type for history items is
|
226
|
-
# defined in `_DatasetHistoryItem`)
|
227
|
-
wftask_dump = wftask.model_dump(exclude={"task"})
|
228
|
-
wftask_dump["task"] = wftask.task.model_dump()
|
229
|
-
new_history_item = dict(
|
230
|
-
workflowtask=wftask_dump,
|
231
|
-
status=WorkflowTaskStatusTypeV1.DONE,
|
232
|
-
parallelization=None,
|
233
|
-
)
|
234
|
-
updated_history = task_pars.history.copy()
|
235
|
-
updated_history.append(new_history_item)
|
236
|
-
|
237
|
-
# Assemble a TaskParameter object
|
238
|
-
out_task_parameters = TaskParameters(
|
239
|
-
input_paths=[task_pars.output_path],
|
240
|
-
output_path=task_pars.output_path,
|
241
|
-
metadata=updated_metadata,
|
242
|
-
history=updated_history,
|
243
|
-
)
|
244
|
-
|
245
|
-
return out_task_parameters
|
246
|
-
|
247
|
-
|
248
|
-
def call_single_parallel_task(
|
249
|
-
component: str,
|
250
|
-
*,
|
251
|
-
wftask: WorkflowTask,
|
252
|
-
task_pars: TaskParameters,
|
253
|
-
workflow_dir_local: Path,
|
254
|
-
workflow_dir_remote: Optional[Path] = None,
|
255
|
-
) -> Any:
|
256
|
-
"""
|
257
|
-
Call a single instance of a parallel task
|
258
|
-
|
259
|
-
Parallel tasks need to run in several instances across the parallelization
|
260
|
-
parameters. This function is responsible of running each single one of
|
261
|
-
those instances.
|
262
|
-
|
263
|
-
Note:
|
264
|
-
This function is directly submitted to a
|
265
|
-
`concurrent.futures`-compatible executor, roughly as in
|
266
|
-
|
267
|
-
some_future = executor.map(call_single_parallel_task, ...)
|
268
|
-
|
269
|
-
If the executor then impersonates another user (as in the
|
270
|
-
`FractalSlurmExecutor`), this function is run by that user.
|
271
|
-
|
272
|
-
Args:
|
273
|
-
component:
|
274
|
-
The parallelization parameter.
|
275
|
-
wftask:
|
276
|
-
The task to execute.
|
277
|
-
task_pars:
|
278
|
-
The parameters to pass on to the task.
|
279
|
-
workflow_dir_local:
|
280
|
-
The server-side working directory for workflow execution.
|
281
|
-
workflow_dir_remote:
|
282
|
-
The user-side working directory for workflow execution (only
|
283
|
-
relevant for multi-user executors).
|
284
|
-
|
285
|
-
Returns:
|
286
|
-
The `json.load`-ed contents of the metadiff output file, or `None` if
|
287
|
-
the file is missing.
|
288
|
-
|
289
|
-
Raises:
|
290
|
-
TaskExecutionError: If the wrapped task raises a task-related error.
|
291
|
-
This function is responsible of adding debugging
|
292
|
-
information to the TaskExecutionError, such as task
|
293
|
-
order and name.
|
294
|
-
JobExecutionError: If the wrapped task raises a job-related error.
|
295
|
-
RuntimeError: If the `workflow_dir_local` is falsy.
|
296
|
-
"""
|
297
|
-
if not workflow_dir_local:
|
298
|
-
raise RuntimeError
|
299
|
-
if not workflow_dir_remote:
|
300
|
-
workflow_dir_remote = workflow_dir_local
|
301
|
-
|
302
|
-
task_files = get_task_file_paths(
|
303
|
-
workflow_dir_local=workflow_dir_local,
|
304
|
-
workflow_dir_remote=workflow_dir_remote,
|
305
|
-
task_order=wftask.order,
|
306
|
-
task_name=wftask.task.name,
|
307
|
-
component=component,
|
308
|
-
)
|
309
|
-
|
310
|
-
# write args file (by assembling task_pars, wftask.args and component)
|
311
|
-
write_args_file(
|
312
|
-
task_pars.dict(exclude={"history"}),
|
313
|
-
wftask.args or {},
|
314
|
-
dict(component=component),
|
315
|
-
path=task_files.args,
|
316
|
-
)
|
317
|
-
|
318
|
-
# assemble full command
|
319
|
-
cmd = (
|
320
|
-
f"{wftask.task.command} -j {task_files.args} "
|
321
|
-
f"--metadata-out {task_files.metadiff}"
|
322
|
-
)
|
323
|
-
|
324
|
-
try:
|
325
|
-
_call_command_wrapper(
|
326
|
-
cmd, stdout=task_files.out, stderr=task_files.err
|
327
|
-
)
|
328
|
-
except TaskExecutionError as e:
|
329
|
-
e.workflow_task_order = wftask.order
|
330
|
-
e.workflow_task_id = wftask.id
|
331
|
-
e.task_name = wftask.task.name
|
332
|
-
raise e
|
333
|
-
|
334
|
-
# JSON-load metadiff file and return its contents (or None)
|
335
|
-
try:
|
336
|
-
with task_files.metadiff.open("r") as f:
|
337
|
-
this_meta_update = json.load(f)
|
338
|
-
except FileNotFoundError:
|
339
|
-
this_meta_update = None
|
340
|
-
|
341
|
-
return this_meta_update
|
342
|
-
|
343
|
-
|
344
|
-
def trim_TaskParameters(
|
345
|
-
task_params: TaskParameters,
|
346
|
-
_task: Task,
|
347
|
-
) -> TaskParameters:
|
348
|
-
"""
|
349
|
-
Return a smaller copy of a TaskParameter object.
|
350
|
-
|
351
|
-
Remove metadata["image"] key/value pair - see issues 1237 and 1242.
|
352
|
-
(https://github.com/fractal-analytics-platform/fractal-server/issues/1237)
|
353
|
-
This applies only to parallel tasks with names different from the ones
|
354
|
-
defined in `_task_needs_image_list`.
|
355
|
-
"""
|
356
|
-
task_params_slim = deepcopy(task_params)
|
357
|
-
if not _task_needs_image_list(_task) and _task.is_parallel:
|
358
|
-
if "image" in task_params_slim.metadata.keys():
|
359
|
-
task_params_slim.metadata.pop("image")
|
360
|
-
task_params_slim.history = []
|
361
|
-
return task_params_slim
|
362
|
-
|
363
|
-
|
364
|
-
def call_parallel_task(
|
365
|
-
*,
|
366
|
-
executor: Executor,
|
367
|
-
wftask: WorkflowTask,
|
368
|
-
task_pars_depend: TaskParameters,
|
369
|
-
workflow_dir_local: Path,
|
370
|
-
workflow_dir_remote: Optional[Path] = None,
|
371
|
-
submit_setup_call: Callable = no_op_submit_setup_call,
|
372
|
-
logger_name: Optional[str] = None,
|
373
|
-
) -> TaskParameters:
|
374
|
-
"""
|
375
|
-
Collect results from the parallel instances of a parallel task
|
376
|
-
|
377
|
-
Prepare and submit for execution all the single calls of a parallel task,
|
378
|
-
and return a single TaskParameters instance to be passed on to the
|
379
|
-
next task.
|
380
|
-
|
381
|
-
**NOTE**: this function is executed by the same user that runs
|
382
|
-
`fractal-server`, and therefore may not have access to some of user's
|
383
|
-
files.
|
384
|
-
|
385
|
-
Args:
|
386
|
-
executor:
|
387
|
-
The `concurrent.futures.Executor`-compatible executor that will
|
388
|
-
run the task.
|
389
|
-
wftask:
|
390
|
-
The parallel task to run.
|
391
|
-
task_pars_depend:
|
392
|
-
The task parameters to be passed on to the parallel task.
|
393
|
-
workflow_dir_local:
|
394
|
-
The server-side working directory for workflow execution.
|
395
|
-
workflow_dir_remote:
|
396
|
-
The user-side working directory for workflow execution (only
|
397
|
-
relevant for multi-user executors).
|
398
|
-
submit_setup_call:
|
399
|
-
An optional function that computes configuration parameters for
|
400
|
-
the executor.
|
401
|
-
logger_name:
|
402
|
-
Name of the logger
|
403
|
-
|
404
|
-
Returns:
|
405
|
-
out_task_parameters:
|
406
|
-
The output task parameters of the parallel task execution, ready to
|
407
|
-
be passed on to the next task.
|
408
|
-
"""
|
409
|
-
logger = get_logger(logger_name)
|
410
|
-
|
411
|
-
if not workflow_dir_remote:
|
412
|
-
workflow_dir_remote = workflow_dir_local
|
413
|
-
|
414
|
-
try:
|
415
|
-
component_list = task_pars_depend.metadata[
|
416
|
-
wftask.parallelization_level
|
417
|
-
]
|
418
|
-
except KeyError:
|
419
|
-
keys = list(task_pars_depend.metadata.keys())
|
420
|
-
raise RuntimeError(
|
421
|
-
"WorkflowTask parallelization_level "
|
422
|
-
f"('{wftask.parallelization_level}') is missing "
|
423
|
-
f"in metadata keys ({keys})."
|
424
|
-
)
|
425
|
-
|
426
|
-
# Backend-specific configuration
|
427
|
-
try:
|
428
|
-
extra_setup = submit_setup_call(
|
429
|
-
wftask=wftask,
|
430
|
-
workflow_dir_local=workflow_dir_local,
|
431
|
-
workflow_dir_remote=workflow_dir_remote,
|
432
|
-
)
|
433
|
-
except Exception as e:
|
434
|
-
tb = "".join(traceback.format_tb(e.__traceback__))
|
435
|
-
raise RuntimeError(
|
436
|
-
f"{type(e)} error in {submit_setup_call=}\n"
|
437
|
-
f"Original traceback:\n{tb}"
|
438
|
-
)
|
439
|
-
|
440
|
-
# Preliminary steps
|
441
|
-
actual_task_pars_depend = trim_TaskParameters(
|
442
|
-
task_pars_depend, wftask.task
|
443
|
-
)
|
444
|
-
|
445
|
-
partial_call_task = partial(
|
446
|
-
call_single_parallel_task,
|
447
|
-
wftask=wftask,
|
448
|
-
task_pars=actual_task_pars_depend,
|
449
|
-
workflow_dir_local=workflow_dir_local,
|
450
|
-
workflow_dir_remote=workflow_dir_remote,
|
451
|
-
)
|
452
|
-
|
453
|
-
# Submit tasks for execution. Note that `for _ in map_iter:
|
454
|
-
# pass` explicitly calls the .result() method for each future, and
|
455
|
-
# therefore is blocking until the task are complete.
|
456
|
-
map_iter = executor.map(partial_call_task, component_list, **extra_setup)
|
457
|
-
|
458
|
-
# Wait for execution of parallel tasks, and aggregate updated metadata (ref
|
459
|
-
# https://github.com/fractal-analytics-platform/fractal-server/issues/802).
|
460
|
-
# NOTE: Even if we remove the need of aggregating metadata, we must keep
|
461
|
-
# the iteration over `map_iter` (e.g. as in `for _ in map_iter: pass`), to
|
462
|
-
# make this call blocking. This is required *also* because otherwise the
|
463
|
-
# shutdown of a FractalSlurmExecutor while running map() may not work
|
464
|
-
aggregated_metadata_update: dict[str, Any] = {}
|
465
|
-
for this_meta_update in map_iter:
|
466
|
-
# Cover the case where the task wrote `null`, rather than a
|
467
|
-
# valid dictionary (ref fractal-server issue #878), or where the
|
468
|
-
# metadiff file was missing.
|
469
|
-
if this_meta_update is None:
|
470
|
-
this_meta_update = {}
|
471
|
-
# Include this_meta_update into aggregated_metadata_update
|
472
|
-
for key, val in this_meta_update.items():
|
473
|
-
aggregated_metadata_update.setdefault(key, []).append(val)
|
474
|
-
if aggregated_metadata_update:
|
475
|
-
logger.warning(
|
476
|
-
"Aggregating parallel-taks updated metadata (with keys "
|
477
|
-
f"{list(aggregated_metadata_update.keys())}).\n"
|
478
|
-
"This feature is experimental and it may change in "
|
479
|
-
"future releases."
|
480
|
-
)
|
481
|
-
|
482
|
-
# Prepare updated_metadata
|
483
|
-
updated_metadata = task_pars_depend.metadata.copy()
|
484
|
-
updated_metadata.update(aggregated_metadata_update)
|
485
|
-
|
486
|
-
# Prepare updated_history (note: the expected type for history items is
|
487
|
-
# defined in `_DatasetHistoryItem`)
|
488
|
-
wftask_dump = wftask.model_dump(exclude={"task"})
|
489
|
-
wftask_dump["task"] = wftask.task.model_dump()
|
490
|
-
new_history_item = dict(
|
491
|
-
workflowtask=wftask_dump,
|
492
|
-
status=WorkflowTaskStatusTypeV1.DONE,
|
493
|
-
parallelization=dict(
|
494
|
-
parallelization_level=wftask.parallelization_level,
|
495
|
-
component_list=component_list,
|
496
|
-
),
|
497
|
-
)
|
498
|
-
updated_history = task_pars_depend.history.copy()
|
499
|
-
updated_history.append(new_history_item)
|
500
|
-
|
501
|
-
# Assemble a TaskParameter object
|
502
|
-
out_task_parameters = TaskParameters(
|
503
|
-
input_paths=[task_pars_depend.output_path],
|
504
|
-
output_path=task_pars_depend.output_path,
|
505
|
-
metadata=updated_metadata,
|
506
|
-
history=updated_history,
|
507
|
-
)
|
508
|
-
|
509
|
-
return out_task_parameters
|
510
|
-
|
511
|
-
|
512
|
-
def execute_tasks(
|
513
|
-
*,
|
514
|
-
executor: Executor,
|
515
|
-
task_list: list[WorkflowTask],
|
516
|
-
task_pars: TaskParameters,
|
517
|
-
workflow_dir_local: Path,
|
518
|
-
workflow_dir_remote: Optional[Path] = None,
|
519
|
-
submit_setup_call: Callable = no_op_submit_setup_call,
|
520
|
-
logger_name: str,
|
521
|
-
) -> TaskParameters:
|
522
|
-
"""
|
523
|
-
Submit a list of WorkflowTasks for execution
|
524
|
-
|
525
|
-
**Note:** At the end of each task, write current metadata to
|
526
|
-
`workflow_dir_local / METADATA_FILENAME`, so that they can be read as part
|
527
|
-
of the `get_job` endpoint.
|
528
|
-
|
529
|
-
Arguments:
|
530
|
-
executor:
|
531
|
-
The `concurrent.futures.Executor`-compatible executor that will
|
532
|
-
run the task.
|
533
|
-
task_list:
|
534
|
-
The list of wftasks to be run
|
535
|
-
task_pars:
|
536
|
-
The task parameters to be passed on to the first task of the list.
|
537
|
-
workflow_dir_local:
|
538
|
-
The server-side working directory for workflow execution.
|
539
|
-
workflow_dir_remote:
|
540
|
-
The user-side working directory for workflow execution (only
|
541
|
-
relevant for multi-user executors). If `None`, it is set to be
|
542
|
-
equal to `workflow_dir_local`.
|
543
|
-
submit_setup_call:
|
544
|
-
An optional function that computes configuration parameters for
|
545
|
-
the executor.
|
546
|
-
logger_name:
|
547
|
-
Name of the logger
|
548
|
-
|
549
|
-
Returns:
|
550
|
-
current_task_pars:
|
551
|
-
A TaskParameters object which constitutes the output of the last
|
552
|
-
task in the list.
|
553
|
-
"""
|
554
|
-
if not workflow_dir_remote:
|
555
|
-
workflow_dir_remote = workflow_dir_local
|
556
|
-
|
557
|
-
logger = get_logger(logger_name)
|
558
|
-
|
559
|
-
current_task_pars = task_pars.copy()
|
560
|
-
|
561
|
-
for this_wftask in task_list:
|
562
|
-
logger.debug(
|
563
|
-
f"SUBMIT {this_wftask.order}-th task "
|
564
|
-
f'(name="{this_wftask.task.name}")'
|
565
|
-
)
|
566
|
-
if this_wftask.is_parallel:
|
567
|
-
current_task_pars = call_parallel_task(
|
568
|
-
executor=executor,
|
569
|
-
wftask=this_wftask,
|
570
|
-
task_pars_depend=current_task_pars,
|
571
|
-
workflow_dir_local=workflow_dir_local,
|
572
|
-
workflow_dir_remote=workflow_dir_remote,
|
573
|
-
submit_setup_call=submit_setup_call,
|
574
|
-
logger_name=logger_name,
|
575
|
-
)
|
576
|
-
else:
|
577
|
-
# Call backend-specific submit_setup_call
|
578
|
-
try:
|
579
|
-
extra_setup = submit_setup_call(
|
580
|
-
wftask=this_wftask,
|
581
|
-
workflow_dir_local=workflow_dir_local,
|
582
|
-
workflow_dir_remote=workflow_dir_remote,
|
583
|
-
)
|
584
|
-
except Exception as e:
|
585
|
-
tb = "".join(traceback.format_tb(e.__traceback__))
|
586
|
-
raise RuntimeError(
|
587
|
-
f"{type(e)} error in {submit_setup_call=}\n"
|
588
|
-
f"Original traceback:\n{tb}"
|
589
|
-
)
|
590
|
-
# NOTE: executor.submit(call_single_task, ...) is non-blocking,
|
591
|
-
# i.e. the returned future may have `this_wftask_future.done() =
|
592
|
-
# False`. We make it blocking right away, by calling `.result()`
|
593
|
-
# NOTE: do not use trim_TaskParameters for non-parallel tasks,
|
594
|
-
# since the `task_pars` argument in `call_single_task` is also used
|
595
|
-
# as a basis for new `metadata`.
|
596
|
-
this_wftask_future = executor.submit(
|
597
|
-
call_single_task,
|
598
|
-
wftask=this_wftask,
|
599
|
-
task_pars=current_task_pars,
|
600
|
-
workflow_dir_local=workflow_dir_local,
|
601
|
-
workflow_dir_remote=workflow_dir_remote,
|
602
|
-
logger_name=logger_name,
|
603
|
-
**extra_setup,
|
604
|
-
)
|
605
|
-
# Wait for the future result (blocking)
|
606
|
-
current_task_pars = this_wftask_future.result()
|
607
|
-
logger.debug(
|
608
|
-
f"END {this_wftask.order}-th task "
|
609
|
-
f'(name="{this_wftask.task.name}")'
|
610
|
-
)
|
611
|
-
|
612
|
-
# Write most recent metadata to METADATA_FILENAME
|
613
|
-
with open(workflow_dir_local / METADATA_FILENAME_V1, "w") as f:
|
614
|
-
json.dump(current_task_pars.metadata, f, indent=2)
|
615
|
-
|
616
|
-
# Write most recent metadata to HISTORY_FILENAME
|
617
|
-
with open(workflow_dir_local / HISTORY_FILENAME_V1, "w") as f:
|
618
|
-
json.dump(current_task_pars.history, f, indent=2)
|
619
|
-
|
620
|
-
return current_task_pars
|