fractal-server 2.13.1__py3-none-any.whl → 2.14.0a1__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/history/__init__.py +4 -0
- fractal_server/app/history/image_updates.py +142 -0
- fractal_server/app/history/status_enum.py +16 -0
- fractal_server/app/models/v2/__init__.py +5 -1
- fractal_server/app/models/v2/history.py +53 -0
- fractal_server/app/routes/api/v2/__init__.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions.py +78 -0
- fractal_server/app/routes/api/v2/dataset.py +12 -9
- fractal_server/app/routes/api/v2/history.py +247 -0
- fractal_server/app/routes/api/v2/project.py +25 -0
- fractal_server/app/routes/api/v2/workflow.py +18 -3
- fractal_server/app/routes/api/v2/workflowtask.py +22 -0
- fractal_server/app/runner/executors/base_runner.py +114 -0
- fractal_server/app/runner/{v2/_local → executors/local}/_local_config.py +3 -3
- fractal_server/app/runner/executors/local/_submit_setup.py +54 -0
- fractal_server/app/runner/executors/local/runner.py +200 -0
- fractal_server/app/runner/executors/{slurm → slurm_common}/_batching.py +1 -1
- fractal_server/app/runner/executors/{slurm → slurm_common}/_slurm_config.py +3 -3
- fractal_server/app/runner/{v2/_slurm_ssh → executors/slurm_common}/_submit_setup.py +13 -12
- fractal_server/app/runner/{v2/_slurm_common → executors/slurm_common}/get_slurm_config.py +9 -15
- fractal_server/app/runner/executors/{slurm/ssh → slurm_ssh}/_executor_wait_thread.py +1 -1
- fractal_server/app/runner/executors/{slurm/ssh → slurm_ssh}/_slurm_job.py +1 -1
- fractal_server/app/runner/executors/{slurm/ssh → slurm_ssh}/executor.py +13 -14
- fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_check_jobs_status.py +11 -9
- fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_executor_wait_thread.py +3 -3
- fractal_server/app/runner/executors/{slurm/sudo → slurm_sudo}/_subprocess_run_as_user.py +2 -68
- fractal_server/app/runner/executors/slurm_sudo/runner.py +632 -0
- fractal_server/app/runner/task_files.py +70 -96
- fractal_server/app/runner/v2/__init__.py +5 -19
- fractal_server/app/runner/v2/_local.py +84 -0
- fractal_server/app/runner/v2/{_slurm_ssh/__init__.py → _slurm_ssh.py} +10 -13
- fractal_server/app/runner/v2/{_slurm_sudo/__init__.py → _slurm_sudo.py} +10 -12
- fractal_server/app/runner/v2/runner.py +93 -28
- fractal_server/app/runner/v2/runner_functions.py +85 -62
- fractal_server/app/runner/v2/runner_functions_low_level.py +20 -20
- fractal_server/app/schemas/v2/dataset.py +0 -17
- fractal_server/app/schemas/v2/history.py +23 -0
- fractal_server/config.py +2 -2
- fractal_server/migrations/versions/8223fcef886c_image_status.py +63 -0
- fractal_server/migrations/versions/87cd72a537a2_add_historyitem_table.py +68 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/METADATA +1 -1
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/RECORD +53 -47
- fractal_server/app/routes/api/v2/status.py +0 -168
- fractal_server/app/runner/executors/slurm/sudo/executor.py +0 -1281
- fractal_server/app/runner/v2/_local/__init__.py +0 -132
- fractal_server/app/runner/v2/_local/_submit_setup.py +0 -52
- fractal_server/app/runner/v2/_local/executor.py +0 -100
- fractal_server/app/runner/v2/_slurm_sudo/_submit_setup.py +0 -83
- fractal_server/app/runner/v2/handle_failed_job.py +0 -59
- /fractal_server/app/runner/executors/{slurm → local}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{slurm/ssh → slurm_common}/__init__.py +0 -0
- /fractal_server/app/runner/executors/{_job_states.py → slurm_common/_job_states.py} +0 -0
- /fractal_server/app/runner/executors/{slurm → slurm_common}/remote.py +0 -0
- /fractal_server/app/runner/executors/{slurm → slurm_common}/utils_executors.py +0 -0
- /fractal_server/app/runner/executors/{slurm/sudo → slurm_ssh}/__init__.py +0 -0
- /fractal_server/app/runner/{v2/_slurm_common → executors/slurm_sudo}/__init__.py +0 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/LICENSE +0 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/WHEEL +0 -0
- {fractal_server-2.13.1.dist-info → fractal_server-2.14.0a1.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,8 @@ from pathlib import Path
|
|
2
2
|
from typing import Optional
|
3
3
|
from typing import Union
|
4
4
|
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
5
7
|
from fractal_server.string_tools import sanitize_string
|
6
8
|
|
7
9
|
|
@@ -17,108 +19,80 @@ def task_subfolder_name(order: Union[int, str], task_name: str) -> str:
|
|
17
19
|
return f"{order}_{task_name_slug}"
|
18
20
|
|
19
21
|
|
20
|
-
class TaskFiles:
|
22
|
+
class TaskFiles(BaseModel):
|
21
23
|
"""
|
22
|
-
Group all file paths pertaining to a task
|
23
|
-
|
24
|
-
Attributes:
|
25
|
-
workflow_dir_local:
|
26
|
-
Server-owned directory to store all task-execution-related relevant
|
27
|
-
files. Note: users cannot write directly to this folder.
|
28
|
-
workflow_dir_remote:
|
29
|
-
User-side directory with the same scope as `workflow_dir_local`,
|
30
|
-
and where a user can write.
|
31
|
-
subfolder_name:
|
32
|
-
Name of task-specific subfolder
|
33
|
-
remote_subfolder:
|
34
|
-
Path to user-side task-specific subfolder
|
35
|
-
task_name:
|
36
|
-
Name of the task
|
37
|
-
task_order:
|
38
|
-
Positional order of the task within a workflow.
|
39
|
-
component:
|
40
|
-
Specific component to run the task for (relevant for tasks to be
|
41
|
-
executed in parallel over many components).
|
42
|
-
file_prefix:
|
43
|
-
Prefix for all task-related files.
|
44
|
-
args:
|
45
|
-
Path for input json file.
|
46
|
-
metadiff:
|
47
|
-
Path for output json file with metadata update.
|
48
|
-
out:
|
49
|
-
Path for task-execution stdout.
|
50
|
-
err:
|
51
|
-
Path for task-execution stderr.
|
24
|
+
Group all file paths pertaining to a task FIXME
|
52
25
|
"""
|
53
26
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
27
|
+
# Parent directory
|
28
|
+
root_dir_local: Path
|
29
|
+
root_dir_remote: Path
|
30
|
+
|
31
|
+
# Per-wftask
|
58
32
|
task_name: str
|
59
|
-
task_order:
|
33
|
+
task_order: int
|
34
|
+
|
35
|
+
# Per-single-component
|
60
36
|
component: Optional[str] = None
|
61
37
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
workflow_dir_local: Path,
|
73
|
-
workflow_dir_remote: Path,
|
74
|
-
task_name: str,
|
75
|
-
task_order: Optional[int] = None,
|
76
|
-
component: Optional[str] = None,
|
77
|
-
):
|
78
|
-
self.workflow_dir_local = workflow_dir_local
|
79
|
-
self.workflow_dir_remote = workflow_dir_remote
|
80
|
-
self.task_order = task_order
|
81
|
-
self.task_name = task_name
|
82
|
-
self.component = component
|
83
|
-
|
84
|
-
if self.component is not None:
|
85
|
-
component_safe = sanitize_string(str(self.component))
|
86
|
-
component_safe = f"_par_{component_safe}"
|
87
|
-
else:
|
88
|
-
component_safe = ""
|
89
|
-
|
90
|
-
if self.task_order is not None:
|
91
|
-
order = str(self.task_order)
|
92
|
-
else:
|
93
|
-
order = "0"
|
94
|
-
self.file_prefix = f"{order}{component_safe}"
|
95
|
-
self.subfolder_name = task_subfolder_name(
|
96
|
-
order=order, task_name=self.task_name
|
97
|
-
)
|
98
|
-
self.remote_subfolder = self.workflow_dir_remote / self.subfolder_name
|
99
|
-
self.args = self.remote_subfolder / f"{self.file_prefix}.args.json"
|
100
|
-
self.out = self.remote_subfolder / f"{self.file_prefix}.out"
|
101
|
-
self.err = self.remote_subfolder / f"{self.file_prefix}.err"
|
102
|
-
self.log = self.remote_subfolder / f"{self.file_prefix}.log"
|
103
|
-
self.metadiff = (
|
104
|
-
self.remote_subfolder / f"{self.file_prefix}.metadiff.json"
|
38
|
+
def _check_component(self):
|
39
|
+
if self.component is None:
|
40
|
+
raise ValueError("`component` cannot be None")
|
41
|
+
|
42
|
+
@property
|
43
|
+
def subfolder_name(self) -> str:
|
44
|
+
order = str(self.task_order or 0)
|
45
|
+
return task_subfolder_name(
|
46
|
+
order=order,
|
47
|
+
task_name=self.task_name,
|
105
48
|
)
|
106
49
|
|
50
|
+
@property
|
51
|
+
def wftask_subfolder_remote(self) -> Path:
|
52
|
+
return self.root_dir_remote / self.subfolder_name
|
107
53
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
)
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
54
|
+
@property
|
55
|
+
def wftask_subfolder_local(self) -> Path:
|
56
|
+
return self.root_dir_local / self.subfolder_name
|
57
|
+
|
58
|
+
@property
|
59
|
+
def log_file_local(self) -> str:
|
60
|
+
self._check_component()
|
61
|
+
return (
|
62
|
+
self.wftask_subfolder_local / f"{self.component}-log.txt"
|
63
|
+
).as_posix()
|
64
|
+
|
65
|
+
@property
|
66
|
+
def log_file_remote(self) -> str:
|
67
|
+
self._check_component()
|
68
|
+
return (
|
69
|
+
self.wftask_subfolder_remote / f"{self.component}-log.txt"
|
70
|
+
).as_posix()
|
71
|
+
|
72
|
+
@property
|
73
|
+
def args_file_local(self) -> str:
|
74
|
+
self._check_component()
|
75
|
+
return (
|
76
|
+
self.wftask_subfolder_local / f"{self.component}-args.json"
|
77
|
+
).as_posix()
|
78
|
+
|
79
|
+
@property
|
80
|
+
def args_file_remote(self) -> str:
|
81
|
+
self._check_component()
|
82
|
+
return (
|
83
|
+
self.wftask_subfolder_remote / f"{self.component}-args.json"
|
84
|
+
).as_posix()
|
85
|
+
|
86
|
+
@property
|
87
|
+
def metadiff_file_local(self) -> str:
|
88
|
+
self._check_component()
|
89
|
+
return (
|
90
|
+
self.wftask_subfolder_local / f"{self.component}-metadiff.json"
|
91
|
+
).as_posix()
|
92
|
+
|
93
|
+
@property
|
94
|
+
def metadiff_file_remote(self) -> str:
|
95
|
+
self._check_component()
|
96
|
+
return (
|
97
|
+
self.wftask_subfolder_remote / f"{self.component}-metadiff.json"
|
98
|
+
).as_posix()
|
@@ -27,13 +27,12 @@ from ...models.v2 import WorkflowV2
|
|
27
27
|
from ...schemas.v2 import JobStatusTypeV2
|
28
28
|
from ..exceptions import JobExecutionError
|
29
29
|
from ..exceptions import TaskExecutionError
|
30
|
-
from ..executors.
|
30
|
+
from ..executors.slurm_sudo._subprocess_run_as_user import _mkdir_as_user
|
31
31
|
from ..filenames import WORKFLOW_LOG_FILENAME
|
32
32
|
from ..task_files import task_subfolder_name
|
33
33
|
from ._local import process_workflow as local_process_workflow
|
34
34
|
from ._slurm_ssh import process_workflow as slurm_ssh_process_workflow
|
35
35
|
from ._slurm_sudo import process_workflow as slurm_sudo_process_workflow
|
36
|
-
from .handle_failed_job import mark_last_wftask_as_failed
|
37
36
|
from fractal_server import __VERSION__
|
38
37
|
from fractal_server.app.models import UserSettings
|
39
38
|
|
@@ -201,7 +200,7 @@ def submit_workflow(
|
|
201
200
|
f"{settings.FRACTAL_RUNNER_BACKEND}."
|
202
201
|
)
|
203
202
|
|
204
|
-
# Create all tasks subfolders
|
203
|
+
# Create all tasks subfolders # FIXME: do this with Runner
|
205
204
|
for order in range(job.first_task_index, job.last_task_index + 1):
|
206
205
|
this_wftask = workflow.task_list[order]
|
207
206
|
task_name = this_wftask.task.name
|
@@ -219,10 +218,7 @@ def submit_workflow(
|
|
219
218
|
folder=str(WORKFLOW_DIR_REMOTE / subfolder_name),
|
220
219
|
user=slurm_user,
|
221
220
|
)
|
222
|
-
|
223
|
-
# Create local subfolder (with standard permission set)
|
224
|
-
(WORKFLOW_DIR_LOCAL / subfolder_name).mkdir()
|
225
|
-
logger.info("Skip remote-subfolder creation")
|
221
|
+
|
226
222
|
except Exception as e:
|
227
223
|
error_type = type(e).__name__
|
228
224
|
fail_job(
|
@@ -345,10 +341,6 @@ def submit_workflow(
|
|
345
341
|
logger.debug(f'FAILED workflow "{workflow.name}", TaskExecutionError.')
|
346
342
|
logger.info(f'Workflow "{workflow.name}" failed (TaskExecutionError).')
|
347
343
|
|
348
|
-
mark_last_wftask_as_failed(
|
349
|
-
dataset_id=dataset_id,
|
350
|
-
logger_name=logger_name,
|
351
|
-
)
|
352
344
|
exception_args_string = "\n".join(e.args)
|
353
345
|
log_msg = (
|
354
346
|
f"TASK ERROR: "
|
@@ -361,10 +353,7 @@ def submit_workflow(
|
|
361
353
|
except JobExecutionError as e:
|
362
354
|
logger.debug(f'FAILED workflow "{workflow.name}", JobExecutionError.')
|
363
355
|
logger.info(f'Workflow "{workflow.name}" failed (JobExecutionError).')
|
364
|
-
|
365
|
-
dataset_id=dataset_id,
|
366
|
-
logger_name=logger_name,
|
367
|
-
)
|
356
|
+
|
368
357
|
fail_job(
|
369
358
|
db=db_sync,
|
370
359
|
job=job,
|
@@ -378,10 +367,7 @@ def submit_workflow(
|
|
378
367
|
except Exception:
|
379
368
|
logger.debug(f'FAILED workflow "{workflow.name}", unknown error.')
|
380
369
|
logger.info(f'Workflow "{workflow.name}" failed (unkwnon error).')
|
381
|
-
|
382
|
-
dataset_id=dataset_id,
|
383
|
-
logger_name=logger_name,
|
384
|
-
)
|
370
|
+
|
385
371
|
current_traceback = traceback.format_exc()
|
386
372
|
fail_job(
|
387
373
|
db=db_sync,
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from ...models.v2 import DatasetV2
|
5
|
+
from ...models.v2 import WorkflowV2
|
6
|
+
from ..executors.local._submit_setup import _local_submit_setup
|
7
|
+
from ..executors.local.runner import LocalRunner
|
8
|
+
from ..set_start_and_last_task_index import set_start_and_last_task_index
|
9
|
+
from .runner import execute_tasks_v2
|
10
|
+
from fractal_server.images.models import AttributeFiltersType
|
11
|
+
|
12
|
+
|
13
|
+
def process_workflow(
|
14
|
+
*,
|
15
|
+
workflow: WorkflowV2,
|
16
|
+
dataset: DatasetV2,
|
17
|
+
workflow_dir_local: Path,
|
18
|
+
workflow_dir_remote: Optional[Path] = None,
|
19
|
+
first_task_index: Optional[int] = None,
|
20
|
+
last_task_index: Optional[int] = None,
|
21
|
+
logger_name: str,
|
22
|
+
job_attribute_filters: AttributeFiltersType,
|
23
|
+
user_id: int,
|
24
|
+
**kwargs,
|
25
|
+
) -> None:
|
26
|
+
"""
|
27
|
+
Run a workflow through
|
28
|
+
|
29
|
+
Args:
|
30
|
+
workflow:
|
31
|
+
The workflow to be run
|
32
|
+
dataset:
|
33
|
+
Initial dataset.
|
34
|
+
workflow_dir_local:
|
35
|
+
Working directory for this run.
|
36
|
+
workflow_dir_remote:
|
37
|
+
Working directory for this run, on the user side. This argument is
|
38
|
+
present for compatibility with the standard backend interface, but
|
39
|
+
for the `local` backend it cannot be different from
|
40
|
+
`workflow_dir_local`.
|
41
|
+
first_task_index:
|
42
|
+
Positional index of the first task to execute; if `None`, start
|
43
|
+
from `0`.
|
44
|
+
last_task_index:
|
45
|
+
Positional index of the last task to execute; if `None`, proceed
|
46
|
+
until the last task.
|
47
|
+
logger_name: Logger name
|
48
|
+
user_id:
|
49
|
+
|
50
|
+
Raises:
|
51
|
+
TaskExecutionError: wrapper for errors raised during tasks' execution
|
52
|
+
(positive exit codes).
|
53
|
+
JobExecutionError: wrapper for errors raised by the tasks' executors
|
54
|
+
(negative exit codes).
|
55
|
+
"""
|
56
|
+
|
57
|
+
if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
|
58
|
+
raise NotImplementedError(
|
59
|
+
"Local backend does not support different directories "
|
60
|
+
f"{workflow_dir_local=} and {workflow_dir_remote=}"
|
61
|
+
)
|
62
|
+
|
63
|
+
# Set values of first_task_index and last_task_index
|
64
|
+
num_tasks = len(workflow.task_list)
|
65
|
+
first_task_index, last_task_index = set_start_and_last_task_index(
|
66
|
+
num_tasks,
|
67
|
+
first_task_index=first_task_index,
|
68
|
+
last_task_index=last_task_index,
|
69
|
+
)
|
70
|
+
|
71
|
+
with LocalRunner(root_dir_local=workflow_dir_local) as runner:
|
72
|
+
execute_tasks_v2(
|
73
|
+
wf_task_list=workflow.task_list[
|
74
|
+
first_task_index : (last_task_index + 1)
|
75
|
+
],
|
76
|
+
dataset=dataset,
|
77
|
+
runner=runner,
|
78
|
+
workflow_dir_local=workflow_dir_local,
|
79
|
+
workflow_dir_remote=workflow_dir_local,
|
80
|
+
logger_name=logger_name,
|
81
|
+
submit_setup_call=_local_submit_setup,
|
82
|
+
job_attribute_filters=job_attribute_filters,
|
83
|
+
user_id=user_id,
|
84
|
+
)
|
@@ -19,14 +19,14 @@ Executor objects.
|
|
19
19
|
from pathlib import Path
|
20
20
|
from typing import Optional
|
21
21
|
|
22
|
-
from
|
23
|
-
from
|
24
|
-
from
|
25
|
-
from
|
26
|
-
from
|
27
|
-
from
|
28
|
-
from ..
|
29
|
-
from .
|
22
|
+
from ....ssh._fabric import FractalSSH
|
23
|
+
from ...models.v2 import DatasetV2
|
24
|
+
from ...models.v2 import WorkflowV2
|
25
|
+
from ..exceptions import JobExecutionError
|
26
|
+
from ..executors.slurm_common._submit_setup import _slurm_submit_setup
|
27
|
+
from ..executors.slurm_ssh.executor import FractalSlurmSSHExecutor
|
28
|
+
from ..set_start_and_last_task_index import set_start_and_last_task_index
|
29
|
+
from .runner import execute_tasks_v2
|
30
30
|
from fractal_server.images.models import AttributeFiltersType
|
31
31
|
from fractal_server.logger import set_logger
|
32
32
|
|
@@ -46,10 +46,7 @@ def process_workflow(
|
|
46
46
|
fractal_ssh: FractalSSH,
|
47
47
|
worker_init: Optional[str] = None,
|
48
48
|
user_id: int,
|
49
|
-
#
|
50
|
-
user_cache_dir: Optional[str] = None,
|
51
|
-
slurm_user: Optional[str] = None,
|
52
|
-
slurm_account: Optional[str] = None,
|
49
|
+
**kwargs, # not used
|
53
50
|
) -> None:
|
54
51
|
"""
|
55
52
|
Process workflow (SLURM backend public interface)
|
@@ -89,7 +86,7 @@ def process_workflow(
|
|
89
86
|
first_task_index : (last_task_index + 1)
|
90
87
|
],
|
91
88
|
dataset=dataset,
|
92
|
-
|
89
|
+
runner=executor,
|
93
90
|
workflow_dir_local=workflow_dir_local,
|
94
91
|
workflow_dir_remote=workflow_dir_remote,
|
95
92
|
logger_name=logger_name,
|
@@ -19,12 +19,12 @@ Executor objects.
|
|
19
19
|
from pathlib import Path
|
20
20
|
from typing import Optional
|
21
21
|
|
22
|
-
from
|
23
|
-
from
|
24
|
-
from
|
25
|
-
from
|
26
|
-
from ..
|
27
|
-
from .
|
22
|
+
from ...models.v2 import DatasetV2
|
23
|
+
from ...models.v2 import WorkflowV2
|
24
|
+
from ..executors.slurm_common._submit_setup import _slurm_submit_setup
|
25
|
+
from ..executors.slurm_sudo.runner import RunnerSlurmSudo
|
26
|
+
from ..set_start_and_last_task_index import set_start_and_last_task_index
|
27
|
+
from .runner import execute_tasks_v2
|
28
28
|
from fractal_server.images.models import AttributeFiltersType
|
29
29
|
|
30
30
|
|
@@ -65,13 +65,11 @@ def process_workflow(
|
|
65
65
|
if isinstance(worker_init, str):
|
66
66
|
worker_init = worker_init.split("\n")
|
67
67
|
|
68
|
-
with
|
69
|
-
debug=True,
|
70
|
-
keep_logs=True,
|
68
|
+
with RunnerSlurmSudo(
|
71
69
|
slurm_user=slurm_user,
|
72
70
|
user_cache_dir=user_cache_dir,
|
73
|
-
|
74
|
-
|
71
|
+
root_dir_local=workflow_dir_local,
|
72
|
+
root_dir_remote=workflow_dir_remote,
|
75
73
|
common_script_lines=worker_init,
|
76
74
|
slurm_account=slurm_account,
|
77
75
|
) as executor:
|
@@ -80,7 +78,7 @@ def process_workflow(
|
|
80
78
|
first_task_index : (last_task_index + 1)
|
81
79
|
],
|
82
80
|
dataset=dataset,
|
83
|
-
|
81
|
+
runner=executor,
|
84
82
|
workflow_dir_local=workflow_dir_local,
|
85
83
|
workflow_dir_remote=workflow_dir_remote,
|
86
84
|
logger_name=logger_name,
|
@@ -1,5 +1,5 @@
|
|
1
|
+
import json
|
1
2
|
import logging
|
2
|
-
from concurrent.futures import ThreadPoolExecutor
|
3
3
|
from copy import copy
|
4
4
|
from copy import deepcopy
|
5
5
|
from pathlib import Path
|
@@ -18,11 +18,14 @@ from .runner_functions import run_v2_task_non_parallel
|
|
18
18
|
from .runner_functions import run_v2_task_parallel
|
19
19
|
from .task_interface import TaskOutput
|
20
20
|
from fractal_server.app.db import get_sync_db
|
21
|
+
from fractal_server.app.history.status_enum import HistoryItemImageStatus
|
21
22
|
from fractal_server.app.models.v2 import AccountingRecord
|
22
23
|
from fractal_server.app.models.v2 import DatasetV2
|
24
|
+
from fractal_server.app.models.v2 import HistoryItemV2
|
25
|
+
from fractal_server.app.models.v2 import ImageStatus
|
26
|
+
from fractal_server.app.models.v2 import TaskGroupV2
|
23
27
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
24
|
-
from fractal_server.app.
|
25
|
-
from fractal_server.app.schemas.v2.workflowtask import WorkflowTaskStatusTypeV2
|
28
|
+
from fractal_server.app.runner.executors.base_runner import BaseRunner
|
26
29
|
from fractal_server.images.models import AttributeFiltersType
|
27
30
|
from fractal_server.images.tools import merge_type_filters
|
28
31
|
|
@@ -31,7 +34,7 @@ def execute_tasks_v2(
|
|
31
34
|
*,
|
32
35
|
wf_task_list: list[WorkflowTaskV2],
|
33
36
|
dataset: DatasetV2,
|
34
|
-
|
37
|
+
runner: BaseRunner,
|
35
38
|
user_id: int,
|
36
39
|
workflow_dir_local: Path,
|
37
40
|
workflow_dir_remote: Optional[Path] = None,
|
@@ -43,8 +46,8 @@ def execute_tasks_v2(
|
|
43
46
|
|
44
47
|
if not workflow_dir_local.exists():
|
45
48
|
logger.warning(
|
46
|
-
f"Now creating {workflow_dir_local}, "
|
47
|
-
"
|
49
|
+
f"Now creating {workflow_dir_local}, but it "
|
50
|
+
"should have already happened."
|
48
51
|
)
|
49
52
|
workflow_dir_local.mkdir()
|
50
53
|
|
@@ -60,66 +63,116 @@ def execute_tasks_v2(
|
|
60
63
|
|
61
64
|
# PRE TASK EXECUTION
|
62
65
|
|
63
|
-
#
|
66
|
+
# Filter images by types and attributes (in two steps)
|
64
67
|
type_filters = copy(current_dataset_type_filters)
|
65
68
|
type_filters_patch = merge_type_filters(
|
66
69
|
task_input_types=task.input_types,
|
67
70
|
wftask_type_filters=wftask.type_filters,
|
68
71
|
)
|
69
72
|
type_filters.update(type_filters_patch)
|
70
|
-
|
73
|
+
type_filtered_images = filter_image_list(
|
71
74
|
images=tmp_images,
|
72
75
|
type_filters=type_filters,
|
76
|
+
attribute_filters=None,
|
77
|
+
)
|
78
|
+
filtered_images = filter_image_list(
|
79
|
+
images=type_filtered_images,
|
80
|
+
type_filters=None,
|
73
81
|
attribute_filters=job_attribute_filters,
|
74
82
|
)
|
75
83
|
|
76
|
-
#
|
84
|
+
# Create history item
|
77
85
|
with next(get_sync_db()) as db:
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
86
|
+
workflowtask_dump = dict(
|
87
|
+
**wftask.model_dump(exclude={"task"}),
|
88
|
+
task=wftask.task.model_dump(),
|
89
|
+
)
|
90
|
+
# Exclude timestamps since they'd need to be serialized properly
|
91
|
+
task_group = db.get(TaskGroupV2, wftask.task.taskgroupv2_id)
|
92
|
+
task_group_dump = task_group.model_dump(
|
93
|
+
exclude={
|
94
|
+
"timestamp_created",
|
95
|
+
"timestamp_last_used",
|
96
|
+
}
|
97
|
+
)
|
98
|
+
parameters_hash = str(
|
99
|
+
hash(
|
100
|
+
json.dumps(
|
101
|
+
[workflowtask_dump, task_group_dump],
|
102
|
+
sort_keys=True,
|
103
|
+
indent=None,
|
104
|
+
).encode("utf-8")
|
105
|
+
)
|
106
|
+
)
|
107
|
+
images = {
|
108
|
+
image["zarr_url"]: HistoryItemImageStatus.SUBMITTED
|
109
|
+
for image in filtered_images
|
110
|
+
}
|
111
|
+
history_item = HistoryItemV2(
|
112
|
+
dataset_id=dataset.id,
|
113
|
+
workflowtask_id=wftask.id,
|
114
|
+
workflowtask_dump=workflowtask_dump,
|
115
|
+
task_group_dump=task_group_dump,
|
116
|
+
parameters_hash=parameters_hash,
|
117
|
+
num_available_images=len(type_filtered_images),
|
118
|
+
num_current_images=len(filtered_images),
|
119
|
+
images=images,
|
120
|
+
)
|
121
|
+
db.add(history_item)
|
122
|
+
for image in filtered_images:
|
123
|
+
db.merge(
|
124
|
+
ImageStatus(
|
125
|
+
zarr_url=image["zarr_url"],
|
126
|
+
workflowtask_id=wftask.id,
|
127
|
+
dataset_id=dataset.id,
|
128
|
+
parameters_hash=parameters_hash,
|
129
|
+
status=HistoryItemImageStatus.SUBMITTED,
|
130
|
+
logfile="/placeholder",
|
131
|
+
)
|
132
|
+
)
|
90
133
|
db.commit()
|
134
|
+
db.refresh(history_item)
|
135
|
+
history_item_id = history_item.id
|
136
|
+
|
91
137
|
# TASK EXECUTION (V2)
|
92
138
|
if task.type == "non_parallel":
|
93
|
-
|
139
|
+
(
|
140
|
+
current_task_output,
|
141
|
+
num_tasks,
|
142
|
+
exceptions,
|
143
|
+
) = run_v2_task_non_parallel(
|
94
144
|
images=filtered_images,
|
95
145
|
zarr_dir=zarr_dir,
|
96
146
|
wftask=wftask,
|
97
147
|
task=task,
|
98
148
|
workflow_dir_local=workflow_dir_local,
|
99
149
|
workflow_dir_remote=workflow_dir_remote,
|
100
|
-
executor=
|
150
|
+
executor=runner,
|
101
151
|
submit_setup_call=submit_setup_call,
|
152
|
+
history_item_id=history_item_id,
|
102
153
|
)
|
103
154
|
elif task.type == "parallel":
|
104
|
-
current_task_output, num_tasks = run_v2_task_parallel(
|
155
|
+
current_task_output, num_tasks, exceptions = run_v2_task_parallel(
|
105
156
|
images=filtered_images,
|
106
157
|
wftask=wftask,
|
107
158
|
task=task,
|
108
159
|
workflow_dir_local=workflow_dir_local,
|
109
160
|
workflow_dir_remote=workflow_dir_remote,
|
110
|
-
executor=
|
161
|
+
executor=runner,
|
111
162
|
submit_setup_call=submit_setup_call,
|
163
|
+
history_item_id=history_item_id,
|
112
164
|
)
|
113
165
|
elif task.type == "compound":
|
114
|
-
current_task_output, num_tasks = run_v2_task_compound(
|
166
|
+
current_task_output, num_tasks, exceptions = run_v2_task_compound(
|
115
167
|
images=filtered_images,
|
116
168
|
zarr_dir=zarr_dir,
|
117
169
|
wftask=wftask,
|
118
170
|
task=task,
|
119
171
|
workflow_dir_local=workflow_dir_local,
|
120
172
|
workflow_dir_remote=workflow_dir_remote,
|
121
|
-
executor=
|
173
|
+
executor=runner,
|
122
174
|
submit_setup_call=submit_setup_call,
|
175
|
+
history_item_id=history_item_id,
|
123
176
|
)
|
124
177
|
else:
|
125
178
|
raise ValueError(f"Unexpected error: Invalid {task.type=}.")
|
@@ -145,6 +198,8 @@ def execute_tasks_v2(
|
|
145
198
|
# Update image list
|
146
199
|
num_new_images = 0
|
147
200
|
current_task_output.check_zarr_urls_are_unique()
|
201
|
+
# FIXME: Introduce for loop over task outputs, and processe them sequentially
|
202
|
+
# each failure should lead to an update of the specific image status
|
148
203
|
for image_obj in current_task_output.image_list_updates:
|
149
204
|
image = image_obj.model_dump()
|
150
205
|
# Edit existing image
|
@@ -270,7 +325,6 @@ def execute_tasks_v2(
|
|
270
325
|
# information
|
271
326
|
with next(get_sync_db()) as db:
|
272
327
|
db_dataset = db.get(DatasetV2, dataset.id)
|
273
|
-
db_dataset.history[-1]["status"] = WorkflowTaskStatusTypeV2.DONE
|
274
328
|
db_dataset.type_filters = current_dataset_type_filters
|
275
329
|
db_dataset.images = tmp_images
|
276
330
|
for attribute_name in [
|
@@ -291,4 +345,15 @@ def execute_tasks_v2(
|
|
291
345
|
db.add(record)
|
292
346
|
db.commit()
|
293
347
|
|
348
|
+
if exceptions != {}:
|
349
|
+
logger.error(
|
350
|
+
f'END {wftask.order}-th task (name="{task_name}") '
|
351
|
+
"- ERROR."
|
352
|
+
)
|
353
|
+
# Raise first error
|
354
|
+
for key, value in exceptions.items():
|
355
|
+
raise JobExecutionError(
|
356
|
+
info=(f"An error occurred.\nOriginal error:\n{value}")
|
357
|
+
)
|
358
|
+
|
294
359
|
logger.debug(f'END {wftask.order}-th task (name="{task_name}")')
|