fractal-server 2.10.5__py3-none-any.whl → 2.11.0a2__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/images.py +29 -6
- fractal_server/app/routes/api/v2/status.py +20 -20
- fractal_server/app/routes/api/v2/submit.py +5 -1
- fractal_server/app/routes/api/v2/workflowtask.py +3 -3
- fractal_server/app/runner/filenames.py +2 -4
- fractal_server/app/runner/v1/_common.py +4 -4
- fractal_server/app/runner/v1/handle_failed_job.py +4 -4
- fractal_server/app/runner/v2/__init__.py +11 -65
- fractal_server/app/runner/v2/_local/__init__.py +12 -17
- fractal_server/app/runner/v2/_local_experimental/__init__.py +11 -20
- fractal_server/app/runner/v2/_slurm_ssh/__init__.py +14 -16
- fractal_server/app/runner/v2/_slurm_sudo/__init__.py +12 -14
- fractal_server/app/runner/v2/handle_failed_job.py +31 -130
- fractal_server/app/runner/v2/merge_outputs.py +13 -16
- fractal_server/app/runner/v2/runner.py +63 -72
- fractal_server/app/runner/v2/task_interface.py +41 -2
- fractal_server/app/schemas/_filter_validators.py +47 -0
- fractal_server/app/schemas/_validators.py +13 -2
- fractal_server/app/schemas/v2/dataset.py +58 -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 +42 -19
- fractal_server/data_migrations/2_11_0.py +67 -0
- fractal_server/images/__init__.py +0 -1
- fractal_server/images/models.py +12 -35
- fractal_server/images/tools.py +29 -13
- fractal_server/migrations/versions/db09233ad13a_split_filters_and_keep_old_columns.py +96 -0
- {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/METADATA +1 -1
- {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/RECORD +40 -37
- {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/LICENSE +0 -0
- {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/WHEEL +0 -0
- {fractal_server-2.10.5.dist-info → fractal_server-2.11.0a2.dist-info}/entry_points.txt +0 -0
fractal_server/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__VERSION__ = "2.
|
1
|
+
__VERSION__ = "2.11.0a2"
|
@@ -11,6 +11,7 @@ from sqlmodel import Relationship
|
|
11
11
|
from sqlmodel import SQLModel
|
12
12
|
|
13
13
|
from ....utils import get_timestamp
|
14
|
+
from fractal_server.images.models import AttributeFiltersType
|
14
15
|
|
15
16
|
|
16
17
|
class DatasetV2(SQLModel, table=True):
|
@@ -41,12 +42,14 @@ class DatasetV2(SQLModel, table=True):
|
|
41
42
|
sa_column=Column(JSON, server_default="[]", nullable=False)
|
42
43
|
)
|
43
44
|
|
44
|
-
filters:
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
45
|
+
filters: Optional[
|
46
|
+
dict[Literal["attributes", "types"], dict[str, Any]]
|
47
|
+
] = Field(sa_column=Column(JSON, nullable=True, server_default="null"))
|
48
|
+
type_filters: dict[str, bool] = Field(
|
49
|
+
sa_column=Column(JSON, nullable=False, server_default="{}")
|
50
|
+
)
|
51
|
+
attribute_filters: AttributeFiltersType = Field(
|
52
|
+
sa_column=Column(JSON, nullable=False, server_default="{}")
|
50
53
|
)
|
51
54
|
|
52
55
|
@property
|
@@ -10,6 +10,7 @@ from sqlmodel import SQLModel
|
|
10
10
|
|
11
11
|
from ....utils import get_timestamp
|
12
12
|
from ...schemas.v2 import JobStatusTypeV2
|
13
|
+
from fractal_server.images.models import AttributeFiltersType
|
13
14
|
|
14
15
|
|
15
16
|
class JobV2(SQLModel, table=True):
|
@@ -49,3 +50,7 @@ class JobV2(SQLModel, table=True):
|
|
49
50
|
)
|
50
51
|
status: str = JobStatusTypeV2.SUBMITTED
|
51
52
|
log: Optional[str] = None
|
53
|
+
|
54
|
+
attribute_filters: AttributeFiltersType = Field(
|
55
|
+
sa_column=Column(JSON, nullable=False, server_default="{}")
|
56
|
+
)
|
@@ -25,14 +25,11 @@ class WorkflowTaskV2(SQLModel, table=True):
|
|
25
25
|
args_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
|
26
26
|
args_non_parallel: Optional[dict[str, Any]] = Field(sa_column=Column(JSON))
|
27
27
|
|
28
|
-
input_filters:
|
29
|
-
Literal["attributes", "types"], dict[str, Any]
|
30
|
-
] = Field(
|
31
|
-
|
32
|
-
|
33
|
-
nullable=False,
|
34
|
-
server_default='{"attributes": {}, "types": {}}',
|
35
|
-
)
|
28
|
+
input_filters: Optional[
|
29
|
+
dict[Literal["attributes", "types"], dict[str, Any]]
|
30
|
+
] = Field(sa_column=Column(JSON, nullable=True, server_default="null"))
|
31
|
+
type_filters: dict[str, bool] = Field(
|
32
|
+
sa_column=Column(JSON, nullable=False, server_default="{}")
|
36
33
|
)
|
37
34
|
|
38
35
|
# Task
|
@@ -17,7 +17,7 @@ from ....models.v1 import ApplyWorkflow
|
|
17
17
|
from ....models.v1 import Dataset
|
18
18
|
from ....models.v1 import Project
|
19
19
|
from ....models.v1 import Resource
|
20
|
-
from ....runner.filenames import
|
20
|
+
from ....runner.filenames import HISTORY_FILENAME_V1
|
21
21
|
from ....schemas.v1 import DatasetCreateV1
|
22
22
|
from ....schemas.v1 import DatasetReadV1
|
23
23
|
from ....schemas.v1 import DatasetStatusReadV1
|
@@ -511,7 +511,7 @@ async def get_workflowtask_status(
|
|
511
511
|
# Highest priority: Read status updates coming from the running-job
|
512
512
|
# temporary file. Note: this file only contains information on
|
513
513
|
# WorkflowTask's that ran through successfully
|
514
|
-
tmp_file = Path(running_job.working_dir) /
|
514
|
+
tmp_file = Path(running_job.working_dir) / HISTORY_FILENAME_V1
|
515
515
|
try:
|
516
516
|
with tmp_file.open("r") as f:
|
517
517
|
history = json.load(f)
|
@@ -21,7 +21,6 @@ from ....models.v2 import TaskV2
|
|
21
21
|
from ....models.v2 import WorkflowTaskV2
|
22
22
|
from ....models.v2 import WorkflowV2
|
23
23
|
from ....schemas.v2 import JobStatusTypeV2
|
24
|
-
from fractal_server.images import Filters
|
25
24
|
|
26
25
|
|
27
26
|
async def _get_project_check_owner(
|
@@ -336,7 +335,7 @@ async def _workflow_insert_task(
|
|
336
335
|
meta_non_parallel: Optional[dict[str, Any]] = None,
|
337
336
|
args_non_parallel: Optional[dict[str, Any]] = None,
|
338
337
|
args_parallel: Optional[dict[str, Any]] = None,
|
339
|
-
|
338
|
+
type_filters: Optional[dict[str, bool]] = None,
|
340
339
|
db: AsyncSession,
|
341
340
|
) -> WorkflowTaskV2:
|
342
341
|
"""
|
@@ -350,7 +349,7 @@ async def _workflow_insert_task(
|
|
350
349
|
meta_non_parallel:
|
351
350
|
args_non_parallel:
|
352
351
|
args_parallel:
|
353
|
-
|
352
|
+
type_filters:
|
354
353
|
db:
|
355
354
|
"""
|
356
355
|
db_workflow = await db.get(WorkflowV2, workflow_id)
|
@@ -376,12 +375,6 @@ async def _workflow_insert_task(
|
|
376
375
|
if final_meta_non_parallel == {}:
|
377
376
|
final_meta_non_parallel = None
|
378
377
|
|
379
|
-
# Prepare input_filters attribute
|
380
|
-
if input_filters is None:
|
381
|
-
input_filters_kwarg = {}
|
382
|
-
else:
|
383
|
-
input_filters_kwarg = dict(input_filters=input_filters)
|
384
|
-
|
385
378
|
# Create DB entry
|
386
379
|
wf_task = WorkflowTaskV2(
|
387
380
|
task_type=task_type,
|
@@ -390,7 +383,7 @@ async def _workflow_insert_task(
|
|
390
383
|
args_parallel=args_parallel,
|
391
384
|
meta_parallel=final_meta_parallel,
|
392
385
|
meta_non_parallel=final_meta_non_parallel,
|
393
|
-
|
386
|
+
type_filters=(type_filters or dict()),
|
394
387
|
)
|
395
388
|
db_workflow.task_list.append(wf_task)
|
396
389
|
flag_modified(db_workflow, "task_list")
|
@@ -8,6 +8,8 @@ from fastapi import Response
|
|
8
8
|
from fastapi import status
|
9
9
|
from pydantic import BaseModel
|
10
10
|
from pydantic import Field
|
11
|
+
from pydantic import root_validator
|
12
|
+
from pydantic import validator
|
11
13
|
from sqlalchemy.orm.attributes import flag_modified
|
12
14
|
|
13
15
|
from ._aux_functions import _get_dataset_check_owner
|
@@ -15,9 +17,14 @@ from fractal_server.app.db import AsyncSession
|
|
15
17
|
from fractal_server.app.db import get_async_db
|
16
18
|
from fractal_server.app.models import UserOAuth
|
17
19
|
from fractal_server.app.routes.auth import current_active_user
|
18
|
-
from fractal_server.
|
20
|
+
from fractal_server.app.schemas._filter_validators import (
|
21
|
+
validate_attribute_filters,
|
22
|
+
)
|
23
|
+
from fractal_server.app.schemas._filter_validators import validate_type_filters
|
24
|
+
from fractal_server.app.schemas._validators import root_validate_dict_keys
|
19
25
|
from fractal_server.images import SingleImage
|
20
26
|
from fractal_server.images import SingleImageUpdate
|
27
|
+
from fractal_server.images.models import AttributeFiltersType
|
21
28
|
from fractal_server.images.tools import find_image_by_zarr_url
|
22
29
|
from fractal_server.images.tools import match_filter
|
23
30
|
|
@@ -38,7 +45,18 @@ class ImagePage(BaseModel):
|
|
38
45
|
|
39
46
|
class ImageQuery(BaseModel):
|
40
47
|
zarr_url: Optional[str]
|
41
|
-
|
48
|
+
type_filters: dict[str, bool] = Field(default_factory=dict)
|
49
|
+
attribute_filters: AttributeFiltersType = Field(default_factory=dict)
|
50
|
+
|
51
|
+
_dict_keys = root_validator(pre=True, allow_reuse=True)(
|
52
|
+
root_validate_dict_keys
|
53
|
+
)
|
54
|
+
_type_filters = validator("type_filters", allow_reuse=True)(
|
55
|
+
validate_type_filters
|
56
|
+
)
|
57
|
+
_attribute_filters = validator("attribute_filters", allow_reuse=True)(
|
58
|
+
validate_attribute_filters
|
59
|
+
)
|
42
60
|
|
43
61
|
|
44
62
|
@router.post(
|
@@ -124,7 +142,11 @@ async def query_dataset_images(
|
|
124
142
|
images = [
|
125
143
|
image
|
126
144
|
for image in images
|
127
|
-
if match_filter(
|
145
|
+
if match_filter(
|
146
|
+
image=image,
|
147
|
+
type_filters=dataset.type_filters,
|
148
|
+
attribute_filters=dataset.attribute_filters,
|
149
|
+
)
|
128
150
|
]
|
129
151
|
|
130
152
|
attributes = {}
|
@@ -154,13 +176,14 @@ async def query_dataset_images(
|
|
154
176
|
else:
|
155
177
|
images = [image]
|
156
178
|
|
157
|
-
if query.
|
179
|
+
if query.attribute_filters or query.type_filters:
|
158
180
|
images = [
|
159
181
|
image
|
160
182
|
for image in images
|
161
183
|
if match_filter(
|
162
|
-
image,
|
163
|
-
|
184
|
+
image=image,
|
185
|
+
type_filters=query.type_filters,
|
186
|
+
attribute_filters=query.attribute_filters,
|
164
187
|
)
|
165
188
|
]
|
166
189
|
|
@@ -1,5 +1,3 @@
|
|
1
|
-
import json
|
2
|
-
from pathlib import Path
|
3
1
|
from typing import Optional
|
4
2
|
|
5
3
|
from fastapi import APIRouter
|
@@ -18,7 +16,6 @@ from ._aux_functions import _get_submitted_jobs_statement
|
|
18
16
|
from ._aux_functions import _get_workflow_check_owner
|
19
17
|
from fractal_server.app.models import UserOAuth
|
20
18
|
from fractal_server.app.routes.auth import current_active_user
|
21
|
-
from fractal_server.app.runner.filenames import HISTORY_FILENAME
|
22
19
|
|
23
20
|
router = APIRouter()
|
24
21
|
|
@@ -98,8 +95,8 @@ async def get_workflowtask_status(
|
|
98
95
|
if running_job is None:
|
99
96
|
# If no job is running, the chronological-last history item is also the
|
100
97
|
# positional-last workflow task to be included in the response.
|
101
|
-
if len(
|
102
|
-
last_valid_wftask_id =
|
98
|
+
if len(history) > 0:
|
99
|
+
last_valid_wftask_id = history[-1]["workflowtask"]["id"]
|
103
100
|
else:
|
104
101
|
last_valid_wftask_id = None
|
105
102
|
else:
|
@@ -109,7 +106,24 @@ async def get_workflowtask_status(
|
|
109
106
|
# as "submitted"
|
110
107
|
start = running_job.first_task_index
|
111
108
|
end = running_job.last_task_index + 1
|
112
|
-
|
109
|
+
|
110
|
+
running_job_wftasks = workflow.task_list[start:end]
|
111
|
+
running_job_statuses = [
|
112
|
+
workflow_tasks_status_dict.get(wft.id, None)
|
113
|
+
for wft in running_job_wftasks
|
114
|
+
]
|
115
|
+
try:
|
116
|
+
first_submitted_index = running_job_statuses.index(
|
117
|
+
WorkflowTaskStatusTypeV2.SUBMITTED
|
118
|
+
)
|
119
|
+
except ValueError:
|
120
|
+
logger.warning(
|
121
|
+
f"Job {running_job.id} is submitted but its task list does "
|
122
|
+
f"not contain a {WorkflowTaskStatusTypeV2.SUBMITTED} task."
|
123
|
+
)
|
124
|
+
first_submitted_index = 0
|
125
|
+
|
126
|
+
for wftask in running_job_wftasks[first_submitted_index:]:
|
113
127
|
workflow_tasks_status_dict[
|
114
128
|
wftask.id
|
115
129
|
] = WorkflowTaskStatusTypeV2.SUBMITTED
|
@@ -133,20 +147,6 @@ async def get_workflowtask_status(
|
|
133
147
|
last_valid_wftask_id = None
|
134
148
|
logger.warning(f"Now setting {last_valid_wftask_id=}.")
|
135
149
|
|
136
|
-
# Highest priority: Read status updates coming from the running-job
|
137
|
-
# temporary file. Note: this file only contains information on
|
138
|
-
# WorkflowTask's that ran through successfully.
|
139
|
-
tmp_file = Path(running_job.working_dir) / HISTORY_FILENAME
|
140
|
-
try:
|
141
|
-
with tmp_file.open("r") as f:
|
142
|
-
history = json.load(f)
|
143
|
-
except FileNotFoundError:
|
144
|
-
history = []
|
145
|
-
for history_item in history:
|
146
|
-
wftask_id = history_item["workflowtask"]["id"]
|
147
|
-
wftask_status = history_item["status"]
|
148
|
-
workflow_tasks_status_dict[wftask_id] = wftask_status
|
149
|
-
|
150
150
|
# Based on previously-gathered information, clean up the response body
|
151
151
|
clean_workflow_tasks_status_dict = {}
|
152
152
|
for wf_task in workflow.task_list:
|
@@ -159,7 +159,11 @@ async def apply_workflow(
|
|
159
159
|
dataset_id=dataset_id,
|
160
160
|
workflow_id=workflow_id,
|
161
161
|
user_email=user.email,
|
162
|
-
|
162
|
+
# The 'filters' field is not supported any more but still exists as a
|
163
|
+
# database column, therefore we manually exclude it from dumps.
|
164
|
+
dataset_dump=json.loads(
|
165
|
+
dataset.json(exclude={"images", "history", "filters"})
|
166
|
+
),
|
163
167
|
workflow_dump=json.loads(workflow.json(exclude={"task_list"})),
|
164
168
|
project_dump=json.loads(project.json(exclude={"user_list"})),
|
165
169
|
**job_create.dict(),
|
@@ -109,7 +109,7 @@ async def replace_workflowtask(
|
|
109
109
|
task_type=task.type,
|
110
110
|
task=task,
|
111
111
|
# old-task values
|
112
|
-
|
112
|
+
type_filters=old_workflow_task.type_filters,
|
113
113
|
# possibly new values
|
114
114
|
args_non_parallel=_args_non_parallel,
|
115
115
|
args_parallel=_args_parallel,
|
@@ -183,7 +183,7 @@ async def create_workflowtask(
|
|
183
183
|
meta_parallel=new_task.meta_parallel,
|
184
184
|
args_non_parallel=new_task.args_non_parallel,
|
185
185
|
args_parallel=new_task.args_parallel,
|
186
|
-
|
186
|
+
type_filters=new_task.type_filters,
|
187
187
|
db=db,
|
188
188
|
)
|
189
189
|
|
@@ -274,7 +274,7 @@ async def update_workflowtask(
|
|
274
274
|
if not actual_args:
|
275
275
|
actual_args = None
|
276
276
|
setattr(db_wf_task, key, actual_args)
|
277
|
-
elif key in ["meta_parallel", "meta_non_parallel", "
|
277
|
+
elif key in ["meta_parallel", "meta_non_parallel", "type_filters"]:
|
278
278
|
setattr(db_wf_task, key, value)
|
279
279
|
else:
|
280
280
|
raise HTTPException(
|
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
IMAGES_FILENAME = "images.json"
|
4
|
-
METADATA_FILENAME = "metadata.json"
|
1
|
+
HISTORY_FILENAME_V1 = "history.json"
|
2
|
+
METADATA_FILENAME_V1 = "metadata.json"
|
5
3
|
SHUTDOWN_FILENAME = "shutdown"
|
6
4
|
WORKFLOW_LOG_FILENAME = "workflow.log"
|
@@ -28,8 +28,8 @@ from ..exceptions import JobExecutionError
|
|
28
28
|
from ..exceptions import TaskExecutionError
|
29
29
|
from .common import TaskParameters
|
30
30
|
from .common import write_args_file
|
31
|
-
from fractal_server.app.runner.filenames import
|
32
|
-
from fractal_server.app.runner.filenames import
|
31
|
+
from fractal_server.app.runner.filenames import HISTORY_FILENAME_V1
|
32
|
+
from fractal_server.app.runner.filenames import METADATA_FILENAME_V1
|
33
33
|
from fractal_server.app.runner.task_files import get_task_file_paths
|
34
34
|
from fractal_server.string_tools import validate_cmd
|
35
35
|
|
@@ -610,11 +610,11 @@ def execute_tasks(
|
|
610
610
|
)
|
611
611
|
|
612
612
|
# Write most recent metadata to METADATA_FILENAME
|
613
|
-
with open(workflow_dir_local /
|
613
|
+
with open(workflow_dir_local / METADATA_FILENAME_V1, "w") as f:
|
614
614
|
json.dump(current_task_pars.metadata, f, indent=2)
|
615
615
|
|
616
616
|
# Write most recent metadata to HISTORY_FILENAME
|
617
|
-
with open(workflow_dir_local /
|
617
|
+
with open(workflow_dir_local / HISTORY_FILENAME_V1, "w") as f:
|
618
618
|
json.dump(current_task_pars.history, f, indent=2)
|
619
619
|
|
620
620
|
return current_task_pars
|
@@ -24,8 +24,8 @@ from ...models.v1 import Dataset
|
|
24
24
|
from ...models.v1 import Workflow
|
25
25
|
from ...models.v1 import WorkflowTask
|
26
26
|
from ...schemas.v1 import WorkflowTaskStatusTypeV1
|
27
|
-
from ..filenames import
|
28
|
-
from ..filenames import
|
27
|
+
from ..filenames import HISTORY_FILENAME_V1
|
28
|
+
from ..filenames import METADATA_FILENAME_V1
|
29
29
|
|
30
30
|
|
31
31
|
def assemble_history_failed_job(
|
@@ -64,7 +64,7 @@ def assemble_history_failed_job(
|
|
64
64
|
new_history = output_dataset.history
|
65
65
|
|
66
66
|
# Part 2: Extend history based on tmp_metadata_file
|
67
|
-
tmp_history_file = Path(job.working_dir) /
|
67
|
+
tmp_history_file = Path(job.working_dir) / HISTORY_FILENAME_V1
|
68
68
|
try:
|
69
69
|
with tmp_history_file.open("r") as f:
|
70
70
|
tmp_file_history = json.load(f)
|
@@ -129,7 +129,7 @@ def assemble_meta_failed_job(
|
|
129
129
|
"""
|
130
130
|
|
131
131
|
new_meta = deepcopy(output_dataset.meta)
|
132
|
-
metadata_file = Path(job.working_dir) /
|
132
|
+
metadata_file = Path(job.working_dir) / METADATA_FILENAME_V1
|
133
133
|
try:
|
134
134
|
with metadata_file.open("r") as f:
|
135
135
|
metadata_update = json.load(f)
|
@@ -11,7 +11,6 @@ from pathlib import Path
|
|
11
11
|
from typing import Optional
|
12
12
|
|
13
13
|
from sqlalchemy.orm import Session as DBSyncSession
|
14
|
-
from sqlalchemy.orm.attributes import flag_modified
|
15
14
|
|
16
15
|
from ....config import get_settings
|
17
16
|
from ....logger import get_logger
|
@@ -24,7 +23,6 @@ from ....zip_tools import _zip_folder_to_file_and_remove
|
|
24
23
|
from ...db import DB
|
25
24
|
from ...models.v2 import DatasetV2
|
26
25
|
from ...models.v2 import JobV2
|
27
|
-
from ...models.v2 import WorkflowTaskV2
|
28
26
|
from ...models.v2 import WorkflowV2
|
29
27
|
from ...schemas.v2 import JobStatusTypeV2
|
30
28
|
from ..exceptions import JobExecutionError
|
@@ -38,12 +36,11 @@ from ._local_experimental import (
|
|
38
36
|
)
|
39
37
|
from ._slurm_ssh import process_workflow as slurm_ssh_process_workflow
|
40
38
|
from ._slurm_sudo import process_workflow as slurm_sudo_process_workflow
|
41
|
-
from .handle_failed_job import
|
42
|
-
from .handle_failed_job import assemble_history_failed_job
|
43
|
-
from .handle_failed_job import assemble_images_failed_job
|
39
|
+
from .handle_failed_job import mark_last_wftask_as_failed
|
44
40
|
from fractal_server import __VERSION__
|
45
41
|
from fractal_server.app.models import UserSettings
|
46
42
|
|
43
|
+
|
47
44
|
_backends = {}
|
48
45
|
_backends["local"] = local_process_workflow
|
49
46
|
_backends["slurm"] = slurm_sudo_process_workflow
|
@@ -115,7 +112,6 @@ async def submit_workflow(
|
|
115
112
|
logger = set_logger(logger_name=logger_name)
|
116
113
|
|
117
114
|
with next(DB.get_sync_db()) as db_sync:
|
118
|
-
|
119
115
|
try:
|
120
116
|
job: Optional[JobV2] = db_sync.get(JobV2, job_id)
|
121
117
|
dataset: Optional[DatasetV2] = db_sync.get(DatasetV2, dataset_id)
|
@@ -322,7 +318,7 @@ async def submit_workflow(
|
|
322
318
|
db_sync = next(DB.get_sync_db())
|
323
319
|
db_sync.close()
|
324
320
|
|
325
|
-
|
321
|
+
await process_workflow(
|
326
322
|
workflow=workflow,
|
327
323
|
dataset=dataset,
|
328
324
|
workflow_dir_local=WORKFLOW_DIR_LOCAL,
|
@@ -331,6 +327,7 @@ async def submit_workflow(
|
|
331
327
|
worker_init=worker_init,
|
332
328
|
first_task_index=job.first_task_index,
|
333
329
|
last_task_index=job.last_task_index,
|
330
|
+
job_attribute_filters=job.attribute_filters,
|
334
331
|
**backend_specific_kwargs,
|
335
332
|
)
|
336
333
|
|
@@ -340,14 +337,6 @@ async def submit_workflow(
|
|
340
337
|
)
|
341
338
|
logger.debug(f'END workflow "{workflow.name}"')
|
342
339
|
|
343
|
-
# Update dataset attributes, in case of successful execution
|
344
|
-
dataset.history.extend(new_dataset_attributes["history"])
|
345
|
-
dataset.filters = new_dataset_attributes["filters"]
|
346
|
-
dataset.images = new_dataset_attributes["images"]
|
347
|
-
for attribute_name in ["filters", "history", "images"]:
|
348
|
-
flag_modified(dataset, attribute_name)
|
349
|
-
db_sync.merge(dataset)
|
350
|
-
|
351
340
|
# Update job DB entry
|
352
341
|
job.status = JobStatusTypeV2.DONE
|
353
342
|
job.end_timestamp = get_timestamp()
|
@@ -358,28 +347,13 @@ async def submit_workflow(
|
|
358
347
|
db_sync.commit()
|
359
348
|
|
360
349
|
except TaskExecutionError as e:
|
361
|
-
|
362
350
|
logger.debug(f'FAILED workflow "{workflow.name}", TaskExecutionError.')
|
363
351
|
logger.info(f'Workflow "{workflow.name}" failed (TaskExecutionError).')
|
364
352
|
|
365
|
-
|
366
|
-
|
367
|
-
failed_wftask = db_sync.get(WorkflowTaskV2, e.workflow_task_id)
|
368
|
-
dataset.history = assemble_history_failed_job(
|
369
|
-
job,
|
370
|
-
dataset,
|
371
|
-
workflow,
|
353
|
+
mark_last_wftask_as_failed(
|
354
|
+
dataset_id=dataset_id,
|
372
355
|
logger_name=logger_name,
|
373
|
-
failed_wftask=failed_wftask,
|
374
356
|
)
|
375
|
-
latest_filters = assemble_filters_failed_job(job)
|
376
|
-
if latest_filters is not None:
|
377
|
-
dataset.filters = latest_filters
|
378
|
-
latest_images = assemble_images_failed_job(job)
|
379
|
-
if latest_images is not None:
|
380
|
-
dataset.images = latest_images
|
381
|
-
db_sync.merge(dataset)
|
382
|
-
|
383
357
|
exception_args_string = "\n".join(e.args)
|
384
358
|
log_msg = (
|
385
359
|
f"TASK ERROR: "
|
@@ -390,26 +364,12 @@ async def submit_workflow(
|
|
390
364
|
fail_job(db=db_sync, job=job, log_msg=log_msg, logger_name=logger_name)
|
391
365
|
|
392
366
|
except JobExecutionError as e:
|
393
|
-
|
394
367
|
logger.debug(f'FAILED workflow "{workflow.name}", JobExecutionError.')
|
395
368
|
logger.info(f'Workflow "{workflow.name}" failed (JobExecutionError).')
|
396
|
-
|
397
|
-
|
398
|
-
# update the DB dataset accordingly
|
399
|
-
dataset.history = assemble_history_failed_job(
|
400
|
-
job,
|
401
|
-
dataset,
|
402
|
-
workflow,
|
369
|
+
mark_last_wftask_as_failed(
|
370
|
+
dataset_id=dataset_id,
|
403
371
|
logger_name=logger_name,
|
404
372
|
)
|
405
|
-
latest_filters = assemble_filters_failed_job(job)
|
406
|
-
if latest_filters is not None:
|
407
|
-
dataset.filters = latest_filters
|
408
|
-
latest_images = assemble_images_failed_job(job)
|
409
|
-
if latest_images is not None:
|
410
|
-
dataset.images = latest_images
|
411
|
-
db_sync.merge(dataset)
|
412
|
-
|
413
373
|
fail_job(
|
414
374
|
db=db_sync,
|
415
375
|
job=job,
|
@@ -421,27 +381,13 @@ async def submit_workflow(
|
|
421
381
|
)
|
422
382
|
|
423
383
|
except Exception:
|
424
|
-
|
425
384
|
logger.debug(f'FAILED workflow "{workflow.name}", unknown error.')
|
426
385
|
logger.info(f'Workflow "{workflow.name}" failed (unkwnon error).')
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
# Read dataset attributes produced by the last successful task, and
|
431
|
-
# update the DB dataset accordingly
|
432
|
-
dataset.history = assemble_history_failed_job(
|
433
|
-
job,
|
434
|
-
dataset,
|
435
|
-
workflow,
|
386
|
+
mark_last_wftask_as_failed(
|
387
|
+
dataset_id=dataset_id,
|
436
388
|
logger_name=logger_name,
|
437
389
|
)
|
438
|
-
|
439
|
-
if latest_filters is not None:
|
440
|
-
dataset.filters = latest_filters
|
441
|
-
latest_images = assemble_images_failed_job(job)
|
442
|
-
if latest_images is not None:
|
443
|
-
dataset.images = latest_images
|
444
|
-
db_sync.merge(dataset)
|
390
|
+
current_traceback = traceback.format_exc()
|
445
391
|
fail_job(
|
446
392
|
db=db_sync,
|
447
393
|
job=job,
|
@@ -29,6 +29,7 @@ from ...set_start_and_last_task_index import set_start_and_last_task_index
|
|
29
29
|
from ..runner import execute_tasks_v2
|
30
30
|
from ._submit_setup import _local_submit_setup
|
31
31
|
from .executor import FractalThreadPoolExecutor
|
32
|
+
from fractal_server.images.models import AttributeFiltersType
|
32
33
|
|
33
34
|
|
34
35
|
def _process_workflow(
|
@@ -39,26 +40,24 @@ def _process_workflow(
|
|
39
40
|
workflow_dir_local: Path,
|
40
41
|
first_task_index: int,
|
41
42
|
last_task_index: int,
|
42
|
-
|
43
|
+
job_attribute_filters: AttributeFiltersType,
|
44
|
+
) -> None:
|
43
45
|
"""
|
44
|
-
|
45
|
-
|
46
|
-
Schedules the workflow using a `FractalThreadPoolExecutor`.
|
46
|
+
Run the workflow using a `FractalThreadPoolExecutor`.
|
47
47
|
"""
|
48
|
-
|
49
48
|
with FractalThreadPoolExecutor() as executor:
|
50
|
-
|
49
|
+
execute_tasks_v2(
|
51
50
|
wf_task_list=workflow.task_list[
|
52
|
-
first_task_index : (last_task_index + 1)
|
53
|
-
],
|
51
|
+
first_task_index : (last_task_index + 1)
|
52
|
+
],
|
54
53
|
dataset=dataset,
|
55
54
|
executor=executor,
|
56
55
|
workflow_dir_local=workflow_dir_local,
|
57
56
|
workflow_dir_remote=workflow_dir_local,
|
58
57
|
logger_name=logger_name,
|
59
58
|
submit_setup_call=_local_submit_setup,
|
59
|
+
job_attribute_filters=job_attribute_filters,
|
60
60
|
)
|
61
|
-
return new_dataset_attributes
|
62
61
|
|
63
62
|
|
64
63
|
async def process_workflow(
|
@@ -70,12 +69,13 @@ async def process_workflow(
|
|
70
69
|
first_task_index: Optional[int] = None,
|
71
70
|
last_task_index: Optional[int] = None,
|
72
71
|
logger_name: str,
|
72
|
+
job_attribute_filters: AttributeFiltersType,
|
73
73
|
# Slurm-specific
|
74
74
|
user_cache_dir: Optional[str] = None,
|
75
75
|
slurm_user: Optional[str] = None,
|
76
76
|
slurm_account: Optional[str] = None,
|
77
77
|
worker_init: Optional[str] = None,
|
78
|
-
) ->
|
78
|
+
) -> None:
|
79
79
|
"""
|
80
80
|
Run a workflow
|
81
81
|
|
@@ -127,11 +127,6 @@ async def process_workflow(
|
|
127
127
|
(positive exit codes).
|
128
128
|
JobExecutionError: wrapper for errors raised by the tasks' executors
|
129
129
|
(negative exit codes).
|
130
|
-
|
131
|
-
Returns:
|
132
|
-
output_dataset_metadata:
|
133
|
-
The updated metadata for the dataset, as returned by the last task
|
134
|
-
of the workflow
|
135
130
|
"""
|
136
131
|
|
137
132
|
if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
|
@@ -148,12 +143,12 @@ async def process_workflow(
|
|
148
143
|
last_task_index=last_task_index,
|
149
144
|
)
|
150
145
|
|
151
|
-
|
146
|
+
await async_wrap(_process_workflow)(
|
152
147
|
workflow=workflow,
|
153
148
|
dataset=dataset,
|
154
149
|
logger_name=logger_name,
|
155
150
|
workflow_dir_local=workflow_dir_local,
|
156
151
|
first_task_index=first_task_index,
|
157
152
|
last_task_index=last_task_index,
|
153
|
+
job_attribute_filters=job_attribute_filters,
|
158
154
|
)
|
159
|
-
return new_dataset_attributes
|