fractal-server 2.10.6__py3-none-any.whl → 2.11.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/v2/dataset.py +9 -6
- fractal_server/app/models/v2/job.py +5 -0
- fractal_server/app/models/v2/workflowtask.py +5 -8
- fractal_server/app/routes/api/v1/dataset.py +2 -2
- fractal_server/app/routes/api/v2/_aux_functions.py +3 -10
- fractal_server/app/routes/api/v2/_aux_functions_tasks.py +21 -0
- fractal_server/app/routes/api/v2/images.py +30 -7
- fractal_server/app/routes/api/v2/job.py +14 -1
- fractal_server/app/routes/api/v2/status.py +20 -20
- fractal_server/app/routes/api/v2/submit.py +11 -4
- fractal_server/app/routes/api/v2/workflow.py +95 -0
- fractal_server/app/routes/api/v2/workflow_import.py +8 -0
- fractal_server/app/routes/api/v2/workflowtask.py +45 -26
- fractal_server/app/runner/{async_wrap.py → async_wrap_v1.py} +1 -1
- fractal_server/app/runner/executors/slurm/_slurm_config.py +1 -1
- fractal_server/app/runner/executors/slurm/ssh/executor.py +2 -2
- fractal_server/app/runner/filenames.py +2 -4
- fractal_server/app/runner/v1/_common.py +4 -4
- fractal_server/app/runner/v1/_local/__init__.py +2 -2
- fractal_server/app/runner/v1/_slurm/__init__.py +2 -2
- fractal_server/app/runner/v1/handle_failed_job.py +4 -4
- fractal_server/app/runner/v2/__init__.py +12 -66
- fractal_server/app/runner/v2/_local/__init__.py +17 -47
- fractal_server/app/runner/v2/_local_experimental/__init__.py +27 -61
- fractal_server/app/runner/v2/_slurm_ssh/__init__.py +26 -65
- fractal_server/app/runner/v2/_slurm_sudo/__init__.py +24 -66
- fractal_server/app/runner/v2/handle_failed_job.py +31 -130
- fractal_server/app/runner/v2/merge_outputs.py +6 -17
- fractal_server/app/runner/v2/runner.py +51 -89
- fractal_server/app/runner/v2/task_interface.py +0 -2
- fractal_server/app/schemas/_filter_validators.py +43 -0
- fractal_server/app/schemas/_validators.py +13 -2
- fractal_server/app/schemas/v2/dataset.py +85 -12
- fractal_server/app/schemas/v2/dumps.py +6 -8
- fractal_server/app/schemas/v2/job.py +14 -0
- fractal_server/app/schemas/v2/task.py +9 -9
- fractal_server/app/schemas/v2/task_group.py +2 -2
- fractal_server/app/schemas/v2/workflowtask.py +69 -20
- fractal_server/data_migrations/2_11_0.py +168 -0
- fractal_server/images/__init__.py +0 -1
- fractal_server/images/models.py +12 -35
- fractal_server/images/tools.py +53 -14
- fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +96 -0
- fractal_server/utils.py +9 -7
- {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/METADATA +1 -1
- {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/RECORD +50 -47
- {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/LICENSE +0 -0
- {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/WHEEL +0 -0
- {fractal_server-2.10.6.dist-info → fractal_server-2.11.0.dist-info}/entry_points.txt +0 -0
@@ -4,60 +4,16 @@ from typing import Optional
|
|
4
4
|
|
5
5
|
from ....models.v2 import DatasetV2
|
6
6
|
from ....models.v2 import WorkflowV2
|
7
|
-
from ...async_wrap import async_wrap
|
8
7
|
from ...exceptions import JobExecutionError
|
9
8
|
from ...filenames import SHUTDOWN_FILENAME
|
10
9
|
from ...set_start_and_last_task_index import set_start_and_last_task_index
|
11
10
|
from ..runner import execute_tasks_v2
|
12
11
|
from ._submit_setup import _local_submit_setup
|
13
12
|
from .executor import FractalProcessPoolExecutor
|
13
|
+
from fractal_server.images.models import AttributeFiltersType
|
14
14
|
|
15
15
|
|
16
|
-
def
|
17
|
-
*,
|
18
|
-
workflow: WorkflowV2,
|
19
|
-
dataset: DatasetV2,
|
20
|
-
logger_name: str,
|
21
|
-
workflow_dir_local: Path,
|
22
|
-
first_task_index: int,
|
23
|
-
last_task_index: int,
|
24
|
-
) -> dict:
|
25
|
-
"""
|
26
|
-
Internal processing routine
|
27
|
-
|
28
|
-
Schedules the workflow using a `FractalProcessPoolExecutor`.
|
29
|
-
|
30
|
-
Cf.
|
31
|
-
[process_workflow][fractal_server.app.runner.v2._local_experimental.process_workflow]
|
32
|
-
for the call signature.
|
33
|
-
"""
|
34
|
-
with FractalProcessPoolExecutor(
|
35
|
-
shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
|
36
|
-
) as executor:
|
37
|
-
try:
|
38
|
-
new_dataset_attributes = execute_tasks_v2(
|
39
|
-
wf_task_list=workflow.task_list[
|
40
|
-
first_task_index : (last_task_index + 1) # noqa
|
41
|
-
],
|
42
|
-
dataset=dataset,
|
43
|
-
executor=executor,
|
44
|
-
workflow_dir_local=workflow_dir_local,
|
45
|
-
workflow_dir_remote=workflow_dir_local,
|
46
|
-
logger_name=logger_name,
|
47
|
-
submit_setup_call=_local_submit_setup,
|
48
|
-
)
|
49
|
-
except BrokenProcessPool as e:
|
50
|
-
raise JobExecutionError(
|
51
|
-
info=(
|
52
|
-
"Job failed with BrokenProcessPool error, likely due to "
|
53
|
-
f"an executor shutdown.\nOriginal error:\n{e.args[0]}"
|
54
|
-
)
|
55
|
-
)
|
56
|
-
|
57
|
-
return new_dataset_attributes
|
58
|
-
|
59
|
-
|
60
|
-
async def process_workflow(
|
16
|
+
def process_workflow(
|
61
17
|
*,
|
62
18
|
workflow: WorkflowV2,
|
63
19
|
dataset: DatasetV2,
|
@@ -66,12 +22,13 @@ async def process_workflow(
|
|
66
22
|
first_task_index: Optional[int] = None,
|
67
23
|
last_task_index: Optional[int] = None,
|
68
24
|
logger_name: str,
|
25
|
+
job_attribute_filters: AttributeFiltersType,
|
69
26
|
# Slurm-specific
|
70
27
|
user_cache_dir: Optional[str] = None,
|
71
28
|
slurm_user: Optional[str] = None,
|
72
29
|
slurm_account: Optional[str] = None,
|
73
30
|
worker_init: Optional[str] = None,
|
74
|
-
) ->
|
31
|
+
) -> None:
|
75
32
|
"""
|
76
33
|
Run a workflow
|
77
34
|
|
@@ -123,11 +80,6 @@ async def process_workflow(
|
|
123
80
|
(positive exit codes).
|
124
81
|
JobExecutionError: wrapper for errors raised by the tasks' executors
|
125
82
|
(negative exit codes).
|
126
|
-
|
127
|
-
Returns:
|
128
|
-
output_dataset_metadata:
|
129
|
-
The updated metadata for the dataset, as returned by the last task
|
130
|
-
of the workflow
|
131
83
|
"""
|
132
84
|
|
133
85
|
if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
|
@@ -144,12 +96,26 @@ async def process_workflow(
|
|
144
96
|
last_task_index=last_task_index,
|
145
97
|
)
|
146
98
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
99
|
+
with FractalProcessPoolExecutor(
|
100
|
+
shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
|
101
|
+
) as executor:
|
102
|
+
try:
|
103
|
+
execute_tasks_v2(
|
104
|
+
wf_task_list=workflow.task_list[
|
105
|
+
first_task_index : (last_task_index + 1)
|
106
|
+
],
|
107
|
+
dataset=dataset,
|
108
|
+
executor=executor,
|
109
|
+
workflow_dir_local=workflow_dir_local,
|
110
|
+
workflow_dir_remote=workflow_dir_local,
|
111
|
+
logger_name=logger_name,
|
112
|
+
submit_setup_call=_local_submit_setup,
|
113
|
+
job_attribute_filters=job_attribute_filters,
|
114
|
+
)
|
115
|
+
except BrokenProcessPool as e:
|
116
|
+
raise JobExecutionError(
|
117
|
+
info=(
|
118
|
+
"Job failed with BrokenProcessPool error, likely due to "
|
119
|
+
f"an executor shutdown.\nOriginal error:\n{e.args[0]}"
|
120
|
+
)
|
121
|
+
)
|
@@ -17,48 +17,51 @@ This backend runs fractal workflows in a SLURM cluster using Clusterfutures
|
|
17
17
|
Executor objects.
|
18
18
|
"""
|
19
19
|
from pathlib import Path
|
20
|
-
from typing import Any
|
21
20
|
from typing import Optional
|
22
|
-
from typing import Union
|
23
21
|
|
24
22
|
from .....ssh._fabric import FractalSSH
|
25
23
|
from ....models.v2 import DatasetV2
|
26
24
|
from ....models.v2 import WorkflowV2
|
27
|
-
from ...async_wrap import async_wrap
|
28
25
|
from ...exceptions import JobExecutionError
|
29
26
|
from ...executors.slurm.ssh.executor import FractalSlurmSSHExecutor
|
30
27
|
from ...set_start_and_last_task_index import set_start_and_last_task_index
|
31
28
|
from ..runner import execute_tasks_v2
|
32
29
|
from ._submit_setup import _slurm_submit_setup
|
30
|
+
from fractal_server.images.models import AttributeFiltersType
|
33
31
|
from fractal_server.logger import set_logger
|
34
32
|
|
35
|
-
|
36
33
|
logger = set_logger(__name__)
|
37
34
|
|
38
35
|
|
39
|
-
def
|
36
|
+
def process_workflow(
|
40
37
|
*,
|
41
38
|
workflow: WorkflowV2,
|
42
39
|
dataset: DatasetV2,
|
43
|
-
logger_name: str,
|
44
40
|
workflow_dir_local: Path,
|
45
|
-
workflow_dir_remote: Path,
|
46
|
-
first_task_index: int,
|
47
|
-
last_task_index: int,
|
41
|
+
workflow_dir_remote: Optional[Path] = None,
|
42
|
+
first_task_index: Optional[int] = None,
|
43
|
+
last_task_index: Optional[int] = None,
|
44
|
+
logger_name: str,
|
45
|
+
job_attribute_filters: AttributeFiltersType,
|
48
46
|
fractal_ssh: FractalSSH,
|
49
|
-
worker_init: Optional[
|
50
|
-
|
47
|
+
worker_init: Optional[str] = None,
|
48
|
+
# Not used
|
49
|
+
user_cache_dir: Optional[str] = None,
|
50
|
+
slurm_user: Optional[str] = None,
|
51
|
+
slurm_account: Optional[str] = None,
|
52
|
+
) -> None:
|
51
53
|
"""
|
52
|
-
|
53
|
-
|
54
|
-
This function initialises the a FractalSlurmExecutor, setting logging,
|
55
|
-
workflow working dir and user to impersonate. It then schedules the
|
56
|
-
workflow tasks and returns the new dataset attributes
|
57
|
-
|
58
|
-
Returns:
|
59
|
-
new_dataset_attributes:
|
54
|
+
Process workflow (SLURM backend public interface)
|
60
55
|
"""
|
61
56
|
|
57
|
+
# Set values of first_task_index and last_task_index
|
58
|
+
num_tasks = len(workflow.task_list)
|
59
|
+
first_task_index, last_task_index = set_start_and_last_task_index(
|
60
|
+
num_tasks,
|
61
|
+
first_task_index=first_task_index,
|
62
|
+
last_task_index=last_task_index,
|
63
|
+
)
|
64
|
+
|
62
65
|
if isinstance(worker_init, str):
|
63
66
|
worker_init = worker_init.split("\n")
|
64
67
|
|
@@ -80,57 +83,15 @@ def _process_workflow(
|
|
80
83
|
workflow_dir_remote=workflow_dir_remote,
|
81
84
|
common_script_lines=worker_init,
|
82
85
|
) as executor:
|
83
|
-
|
86
|
+
execute_tasks_v2(
|
84
87
|
wf_task_list=workflow.task_list[
|
85
|
-
first_task_index : (last_task_index + 1)
|
86
|
-
],
|
88
|
+
first_task_index : (last_task_index + 1)
|
89
|
+
],
|
87
90
|
dataset=dataset,
|
88
91
|
executor=executor,
|
89
92
|
workflow_dir_local=workflow_dir_local,
|
90
93
|
workflow_dir_remote=workflow_dir_remote,
|
91
94
|
logger_name=logger_name,
|
92
95
|
submit_setup_call=_slurm_submit_setup,
|
96
|
+
job_attribute_filters=job_attribute_filters,
|
93
97
|
)
|
94
|
-
return new_dataset_attributes
|
95
|
-
|
96
|
-
|
97
|
-
async def process_workflow(
|
98
|
-
*,
|
99
|
-
workflow: WorkflowV2,
|
100
|
-
dataset: DatasetV2,
|
101
|
-
workflow_dir_local: Path,
|
102
|
-
workflow_dir_remote: Optional[Path] = None,
|
103
|
-
first_task_index: Optional[int] = None,
|
104
|
-
last_task_index: Optional[int] = None,
|
105
|
-
logger_name: str,
|
106
|
-
# Not used
|
107
|
-
fractal_ssh: FractalSSH,
|
108
|
-
user_cache_dir: Optional[str] = None,
|
109
|
-
slurm_user: Optional[str] = None,
|
110
|
-
slurm_account: Optional[str] = None,
|
111
|
-
worker_init: Optional[str] = None,
|
112
|
-
) -> dict:
|
113
|
-
"""
|
114
|
-
Process workflow (SLURM backend public interface)
|
115
|
-
"""
|
116
|
-
|
117
|
-
# Set values of first_task_index and last_task_index
|
118
|
-
num_tasks = len(workflow.task_list)
|
119
|
-
first_task_index, last_task_index = set_start_and_last_task_index(
|
120
|
-
num_tasks,
|
121
|
-
first_task_index=first_task_index,
|
122
|
-
last_task_index=last_task_index,
|
123
|
-
)
|
124
|
-
|
125
|
-
new_dataset_attributes = await async_wrap(_process_workflow)(
|
126
|
-
workflow=workflow,
|
127
|
-
dataset=dataset,
|
128
|
-
logger_name=logger_name,
|
129
|
-
workflow_dir_local=workflow_dir_local,
|
130
|
-
workflow_dir_remote=workflow_dir_remote,
|
131
|
-
first_task_index=first_task_index,
|
132
|
-
last_task_index=last_task_index,
|
133
|
-
worker_init=worker_init,
|
134
|
-
fractal_ssh=fractal_ssh,
|
135
|
-
)
|
136
|
-
return new_dataset_attributes
|
@@ -17,44 +17,45 @@ This backend runs fractal workflows in a SLURM cluster using Clusterfutures
|
|
17
17
|
Executor objects.
|
18
18
|
"""
|
19
19
|
from pathlib import Path
|
20
|
-
from typing import Any
|
21
20
|
from typing import Optional
|
22
|
-
from typing import Union
|
23
21
|
|
24
22
|
from ....models.v2 import DatasetV2
|
25
23
|
from ....models.v2 import WorkflowV2
|
26
|
-
from ...async_wrap import async_wrap
|
27
24
|
from ...executors.slurm.sudo.executor import FractalSlurmExecutor
|
28
25
|
from ...set_start_and_last_task_index import set_start_and_last_task_index
|
29
26
|
from ..runner import execute_tasks_v2
|
30
27
|
from ._submit_setup import _slurm_submit_setup
|
28
|
+
from fractal_server.images.models import AttributeFiltersType
|
31
29
|
|
32
30
|
|
33
|
-
def
|
31
|
+
def process_workflow(
|
34
32
|
*,
|
35
33
|
workflow: WorkflowV2,
|
36
34
|
dataset: DatasetV2,
|
37
|
-
logger_name: str,
|
38
35
|
workflow_dir_local: Path,
|
39
|
-
workflow_dir_remote: Path,
|
40
|
-
first_task_index: int,
|
41
|
-
last_task_index: int,
|
36
|
+
workflow_dir_remote: Optional[Path] = None,
|
37
|
+
first_task_index: Optional[int] = None,
|
38
|
+
last_task_index: Optional[int] = None,
|
39
|
+
logger_name: str,
|
40
|
+
job_attribute_filters: AttributeFiltersType,
|
41
|
+
# Slurm-specific
|
42
|
+
user_cache_dir: Optional[str] = None,
|
42
43
|
slurm_user: Optional[str] = None,
|
43
44
|
slurm_account: Optional[str] = None,
|
44
|
-
|
45
|
-
|
46
|
-
) -> dict[str, Any]:
|
45
|
+
worker_init: Optional[str] = None,
|
46
|
+
) -> None:
|
47
47
|
"""
|
48
|
-
|
49
|
-
|
50
|
-
This function initialises the a FractalSlurmExecutor, setting logging,
|
51
|
-
workflow working dir and user to impersonate. It then schedules the
|
52
|
-
workflow tasks and returns the new dataset attributes
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
new_dataset_attributes:
|
48
|
+
Process workflow (SLURM backend public interface).
|
56
49
|
"""
|
57
50
|
|
51
|
+
# Set values of first_task_index and last_task_index
|
52
|
+
num_tasks = len(workflow.task_list)
|
53
|
+
first_task_index, last_task_index = set_start_and_last_task_index(
|
54
|
+
num_tasks,
|
55
|
+
first_task_index=first_task_index,
|
56
|
+
last_task_index=last_task_index,
|
57
|
+
)
|
58
|
+
|
58
59
|
if not slurm_user:
|
59
60
|
raise RuntimeError(
|
60
61
|
"slurm_user argument is required, for slurm backend"
|
@@ -73,58 +74,15 @@ def _process_workflow(
|
|
73
74
|
common_script_lines=worker_init,
|
74
75
|
slurm_account=slurm_account,
|
75
76
|
) as executor:
|
76
|
-
|
77
|
+
execute_tasks_v2(
|
77
78
|
wf_task_list=workflow.task_list[
|
78
|
-
first_task_index : (last_task_index + 1)
|
79
|
-
],
|
79
|
+
first_task_index : (last_task_index + 1)
|
80
|
+
],
|
80
81
|
dataset=dataset,
|
81
82
|
executor=executor,
|
82
83
|
workflow_dir_local=workflow_dir_local,
|
83
84
|
workflow_dir_remote=workflow_dir_remote,
|
84
85
|
logger_name=logger_name,
|
85
86
|
submit_setup_call=_slurm_submit_setup,
|
87
|
+
job_attribute_filters=job_attribute_filters,
|
86
88
|
)
|
87
|
-
return new_dataset_attributes
|
88
|
-
|
89
|
-
|
90
|
-
async def process_workflow(
|
91
|
-
*,
|
92
|
-
workflow: WorkflowV2,
|
93
|
-
dataset: DatasetV2,
|
94
|
-
workflow_dir_local: Path,
|
95
|
-
workflow_dir_remote: Optional[Path] = None,
|
96
|
-
first_task_index: Optional[int] = None,
|
97
|
-
last_task_index: Optional[int] = None,
|
98
|
-
logger_name: str,
|
99
|
-
# Slurm-specific
|
100
|
-
user_cache_dir: Optional[str] = None,
|
101
|
-
slurm_user: Optional[str] = None,
|
102
|
-
slurm_account: Optional[str] = None,
|
103
|
-
worker_init: Optional[str] = None,
|
104
|
-
) -> dict:
|
105
|
-
"""
|
106
|
-
Process workflow (SLURM backend public interface).
|
107
|
-
"""
|
108
|
-
|
109
|
-
# Set values of first_task_index and last_task_index
|
110
|
-
num_tasks = len(workflow.task_list)
|
111
|
-
first_task_index, last_task_index = set_start_and_last_task_index(
|
112
|
-
num_tasks,
|
113
|
-
first_task_index=first_task_index,
|
114
|
-
last_task_index=last_task_index,
|
115
|
-
)
|
116
|
-
|
117
|
-
new_dataset_attributes = await async_wrap(_process_workflow)(
|
118
|
-
workflow=workflow,
|
119
|
-
dataset=dataset,
|
120
|
-
logger_name=logger_name,
|
121
|
-
workflow_dir_local=workflow_dir_local,
|
122
|
-
workflow_dir_remote=workflow_dir_remote,
|
123
|
-
first_task_index=first_task_index,
|
124
|
-
last_task_index=last_task_index,
|
125
|
-
user_cache_dir=user_cache_dir,
|
126
|
-
slurm_user=slurm_user,
|
127
|
-
slurm_account=slurm_account,
|
128
|
-
worker_init=worker_init,
|
129
|
-
)
|
130
|
-
return new_dataset_attributes
|
@@ -12,147 +12,48 @@
|
|
12
12
|
"""
|
13
13
|
Helper functions to handle Dataset history.
|
14
14
|
"""
|
15
|
-
import json
|
16
15
|
import logging
|
17
|
-
|
18
|
-
from
|
19
|
-
from typing import Optional
|
16
|
+
|
17
|
+
from sqlalchemy.orm.attributes import flag_modified
|
20
18
|
|
21
19
|
from ...models.v2 import DatasetV2
|
22
|
-
from ...models.v2 import JobV2
|
23
|
-
from ...models.v2 import WorkflowTaskV2
|
24
|
-
from ...models.v2 import WorkflowV2
|
25
20
|
from ...schemas.v2 import WorkflowTaskStatusTypeV2
|
26
|
-
from
|
27
|
-
from ..filenames import HISTORY_FILENAME
|
28
|
-
from ..filenames import IMAGES_FILENAME
|
21
|
+
from fractal_server.app.db import get_sync_db
|
29
22
|
|
30
23
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
logger_name: Optional[str] = None,
|
36
|
-
failed_wftask: Optional[WorkflowTaskV2] = None,
|
37
|
-
) -> list[dict[str, Any]]:
|
24
|
+
def mark_last_wftask_as_failed(
|
25
|
+
dataset_id: int,
|
26
|
+
logger_name: str,
|
27
|
+
) -> None:
|
38
28
|
"""
|
39
|
-
|
29
|
+
Edit dataset history, by marking last item as failed.
|
40
30
|
|
41
31
|
Args:
|
42
|
-
|
43
|
-
The failed `JobV2` object.
|
44
|
-
dataset:
|
45
|
-
The `DatasetV2` object associated to `job`.
|
46
|
-
workflow:
|
47
|
-
The `WorkflowV2` object associated to `job`.
|
32
|
+
dataset: The `DatasetV2` object
|
48
33
|
logger_name: A logger name.
|
49
|
-
failed_wftask:
|
50
|
-
If set, append it to `history` during step 3; if `None`, infer
|
51
|
-
it by comparing the job task list and the one in
|
52
|
-
`HISTORY_FILENAME`.
|
53
|
-
|
54
|
-
Returns:
|
55
|
-
The new value of `history`, to be merged into
|
56
|
-
`dataset.meta`.
|
57
34
|
"""
|
58
35
|
|
59
36
|
logger = logging.getLogger(logger_name)
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
# Part 2: Extend history based on temporary-file contents
|
69
|
-
tmp_history_file = Path(job.working_dir) / HISTORY_FILENAME
|
70
|
-
try:
|
71
|
-
with tmp_history_file.open("r") as f:
|
72
|
-
tmp_file_history = json.load(f)
|
73
|
-
new_history.extend(tmp_file_history)
|
74
|
-
except FileNotFoundError:
|
75
|
-
tmp_file_history = []
|
76
|
-
|
77
|
-
# Part 3/A: Identify failed task, if needed
|
78
|
-
if failed_wftask is None:
|
79
|
-
job_wftasks = workflow.task_list[
|
80
|
-
job.first_task_index : (job.last_task_index + 1) # noqa
|
81
|
-
]
|
82
|
-
tmp_file_wftasks = [
|
83
|
-
history_item["workflowtask"] for history_item in tmp_file_history
|
84
|
-
]
|
85
|
-
if len(job_wftasks) <= len(tmp_file_wftasks):
|
86
|
-
n_tasks_job = len(job_wftasks)
|
87
|
-
n_tasks_tmp = len(tmp_file_wftasks)
|
88
|
-
logger.error(
|
89
|
-
"Cannot identify the failed task based on job task list "
|
90
|
-
f"(length {n_tasks_job}) and temporary-file task list "
|
91
|
-
f"(length {n_tasks_tmp})."
|
37
|
+
with next(get_sync_db()) as db:
|
38
|
+
db_dataset = db.get(DatasetV2, dataset_id)
|
39
|
+
if len(db_dataset.history) == 0:
|
40
|
+
logger.warning(
|
41
|
+
f"History for {dataset_id=} is empty. Likely reason: the job "
|
42
|
+
"failed before its first task was marked as SUBMITTED. "
|
43
|
+
"Continue."
|
92
44
|
)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
)
|
106
|
-
|
107
|
-
|
108
|
-
return new_history
|
109
|
-
|
110
|
-
|
111
|
-
def assemble_images_failed_job(job: JobV2) -> Optional[dict[str, Any]]:
|
112
|
-
"""
|
113
|
-
Assemble `DatasetV2.images` for a failed workflow-execution.
|
114
|
-
|
115
|
-
Assemble new value of `images` based on the last successful task, i.e.
|
116
|
-
based on the content of the temporary `IMAGES_FILENAME` file. If the file
|
117
|
-
is missing, return `None`.
|
118
|
-
|
119
|
-
Argumentss:
|
120
|
-
job:
|
121
|
-
The failed `JobV2` object.
|
122
|
-
|
123
|
-
Returns:
|
124
|
-
The new value of `dataset.images`, or `None` if `IMAGES_FILENAME`
|
125
|
-
is missing.
|
126
|
-
"""
|
127
|
-
tmp_file = Path(job.working_dir) / IMAGES_FILENAME
|
128
|
-
try:
|
129
|
-
with tmp_file.open("r") as f:
|
130
|
-
new_images = json.load(f)
|
131
|
-
return new_images
|
132
|
-
except FileNotFoundError:
|
133
|
-
return None
|
134
|
-
|
135
|
-
|
136
|
-
def assemble_filters_failed_job(job: JobV2) -> Optional[dict[str, Any]]:
|
137
|
-
"""
|
138
|
-
Assemble `DatasetV2.filters` for a failed workflow-execution.
|
139
|
-
|
140
|
-
Assemble new value of `filters` based on the last successful task, i.e.
|
141
|
-
based on the content of the temporary `FILTERS_FILENAME` file. If the file
|
142
|
-
is missing, return `None`.
|
143
|
-
|
144
|
-
Argumentss:
|
145
|
-
job:
|
146
|
-
The failed `JobV2` object.
|
147
|
-
|
148
|
-
Returns:
|
149
|
-
The new value of `dataset.filters`, or `None` if `FILTERS_FILENAME`
|
150
|
-
is missing.
|
151
|
-
"""
|
152
|
-
tmp_file = Path(job.working_dir) / FILTERS_FILENAME
|
153
|
-
try:
|
154
|
-
with tmp_file.open("r") as f:
|
155
|
-
new_filters = json.load(f)
|
156
|
-
return new_filters
|
157
|
-
except FileNotFoundError:
|
158
|
-
return None
|
45
|
+
return
|
46
|
+
workflowtask_id = db_dataset.history[-1]["workflowtask"]["id"]
|
47
|
+
last_item_status = db_dataset.history[-1]["status"]
|
48
|
+
if last_item_status != WorkflowTaskStatusTypeV2.SUBMITTED:
|
49
|
+
logger.warning(
|
50
|
+
"Unexpected branch: "
|
51
|
+
f"Last history item, for {workflowtask_id=}, "
|
52
|
+
f"has status {last_item_status}. Skip."
|
53
|
+
)
|
54
|
+
return
|
55
|
+
logger.info(f"Setting history item for {workflowtask_id=} to failed.")
|
56
|
+
db_dataset.history[-1]["status"] = WorkflowTaskStatusTypeV2.FAILED
|
57
|
+
flag_modified(db_dataset, "history")
|
58
|
+
db.merge(db_dataset)
|
59
|
+
db.commit()
|
@@ -1,38 +1,27 @@
|
|
1
|
-
from copy import copy
|
2
|
-
|
3
1
|
from fractal_server.app.runner.v2.deduplicate_list import deduplicate_list
|
4
2
|
from fractal_server.app.runner.v2.task_interface import TaskOutput
|
5
3
|
|
6
4
|
|
7
5
|
def merge_outputs(task_outputs: list[TaskOutput]) -> TaskOutput:
|
8
6
|
|
7
|
+
if len(task_outputs) == 0:
|
8
|
+
return TaskOutput()
|
9
|
+
|
9
10
|
final_image_list_updates = []
|
10
11
|
final_image_list_removals = []
|
11
|
-
last_new_filters = None
|
12
12
|
|
13
|
-
for
|
13
|
+
for task_output in task_outputs:
|
14
14
|
|
15
15
|
final_image_list_updates.extend(task_output.image_list_updates)
|
16
16
|
final_image_list_removals.extend(task_output.image_list_removals)
|
17
17
|
|
18
|
-
|
19
|
-
current_new_filters = task_output.filters
|
20
|
-
if ind == 0:
|
21
|
-
last_new_filters = copy(current_new_filters)
|
22
|
-
if current_new_filters != last_new_filters:
|
23
|
-
raise ValueError(f"{current_new_filters=} but {last_new_filters=}")
|
24
|
-
last_new_filters = copy(current_new_filters)
|
25
|
-
|
18
|
+
# Note: the ordering of `image_list_removals` is not guaranteed
|
26
19
|
final_image_list_updates = deduplicate_list(final_image_list_updates)
|
27
|
-
|
28
|
-
additional_args = {}
|
29
|
-
if last_new_filters is not None:
|
30
|
-
additional_args["filters"] = last_new_filters
|
20
|
+
final_image_list_removals = list(set(final_image_list_removals))
|
31
21
|
|
32
22
|
final_output = TaskOutput(
|
33
23
|
image_list_updates=final_image_list_updates,
|
34
24
|
image_list_removals=final_image_list_removals,
|
35
|
-
**additional_args,
|
36
25
|
)
|
37
26
|
|
38
27
|
return final_output
|