fractal-server 2.14.0a2__py3-none-any.whl → 2.14.0a4__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/__main__.py +3 -1
- fractal_server/app/history/__init__.py +4 -4
- fractal_server/app/history/image_updates.py +124 -142
- fractal_server/app/history/status_enum.py +2 -2
- fractal_server/app/models/v2/__init__.py +6 -4
- fractal_server/app/models/v2/history.py +44 -20
- fractal_server/app/routes/admin/v2/task.py +1 -1
- fractal_server/app/routes/api/__init__.py +1 -1
- fractal_server/app/routes/api/v2/__init__.py +4 -0
- fractal_server/app/routes/api/v2/_aux_functions_history.py +49 -0
- fractal_server/app/routes/api/v2/dataset.py +0 -12
- fractal_server/app/routes/api/v2/history.py +302 -176
- fractal_server/app/routes/api/v2/project.py +1 -26
- fractal_server/app/routes/api/v2/status_legacy.py +168 -0
- fractal_server/app/routes/api/v2/workflow.py +2 -17
- fractal_server/app/routes/api/v2/workflowtask.py +41 -71
- fractal_server/app/routes/auth/oauth.py +5 -3
- fractal_server/app/runner/executors/base_runner.py +2 -1
- fractal_server/app/runner/executors/local/_submit_setup.py +5 -13
- fractal_server/app/runner/executors/local/runner.py +10 -55
- fractal_server/app/runner/executors/slurm_common/_slurm_config.py +1 -1
- fractal_server/app/runner/executors/slurm_common/get_slurm_config.py +1 -1
- fractal_server/app/runner/executors/slurm_common/remote.py +1 -1
- fractal_server/app/runner/executors/slurm_sudo/runner.py +171 -108
- fractal_server/app/runner/v2/__init__.py +2 -22
- fractal_server/app/runner/v2/_slurm_ssh.py +1 -1
- fractal_server/app/runner/v2/_slurm_sudo.py +1 -1
- fractal_server/app/runner/v2/runner.py +47 -59
- fractal_server/app/runner/v2/runner_functions.py +185 -69
- fractal_server/app/schemas/_validators.py +13 -24
- fractal_server/app/schemas/user.py +10 -7
- fractal_server/app/schemas/user_settings.py +9 -21
- fractal_server/app/schemas/v2/dataset.py +8 -6
- fractal_server/app/schemas/v2/job.py +9 -5
- fractal_server/app/schemas/v2/manifest.py +3 -7
- fractal_server/app/schemas/v2/project.py +9 -7
- fractal_server/app/schemas/v2/task.py +41 -77
- fractal_server/app/schemas/v2/task_collection.py +14 -32
- fractal_server/app/schemas/v2/task_group.py +10 -9
- fractal_server/app/schemas/v2/workflow.py +10 -11
- fractal_server/app/security/__init__.py +3 -3
- fractal_server/app/security/signup_email.py +2 -2
- fractal_server/config.py +33 -34
- fractal_server/migrations/versions/fbce16ff4e47_new_history_items.py +120 -0
- fractal_server/tasks/v2/templates/2_pip_install.sh +1 -1
- fractal_server/tasks/v2/templates/4_pip_show.sh +1 -1
- fractal_server/tasks/v2/utils_templates.py +6 -0
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/METADATA +1 -1
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/RECORD +53 -54
- fractal_server/app/runner/executors/slurm_sudo/_executor_wait_thread.py +0 -130
- fractal_server/app/schemas/v2/history.py +0 -23
- fractal_server/migrations/versions/87cd72a537a2_add_historyitem_table.py +0 -68
- fractal_server/migrations/versions/954ddc64425a_image_status.py +0 -63
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/LICENSE +0 -0
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/WHEEL +0 -0
- {fractal_server-2.14.0a2.dist-info → fractal_server-2.14.0a4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,168 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from fastapi import APIRouter
|
4
|
+
from fastapi import Depends
|
5
|
+
from fastapi import HTTPException
|
6
|
+
from fastapi import status
|
7
|
+
|
8
|
+
from .....logger import set_logger
|
9
|
+
from ....db import AsyncSession
|
10
|
+
from ....db import get_async_db
|
11
|
+
from ....models.v2 import JobV2
|
12
|
+
from ....schemas.v2.status import StatusReadV2
|
13
|
+
from ....schemas.v2.workflowtask import WorkflowTaskStatusTypeV2
|
14
|
+
from ._aux_functions import _get_dataset_check_owner
|
15
|
+
from ._aux_functions import _get_submitted_jobs_statement
|
16
|
+
from ._aux_functions import _get_workflow_check_owner
|
17
|
+
from fractal_server.app.models import UserOAuth
|
18
|
+
from fractal_server.app.routes.auth import current_active_user
|
19
|
+
|
20
|
+
router = APIRouter()
|
21
|
+
|
22
|
+
logger = set_logger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
@router.get(
|
26
|
+
"/project/{project_id}/status-legacy/",
|
27
|
+
response_model=StatusReadV2,
|
28
|
+
)
|
29
|
+
async def get_workflowtask_status(
|
30
|
+
project_id: int,
|
31
|
+
dataset_id: int,
|
32
|
+
workflow_id: int,
|
33
|
+
user: UserOAuth = Depends(current_active_user),
|
34
|
+
db: AsyncSession = Depends(get_async_db),
|
35
|
+
) -> Optional[StatusReadV2]:
|
36
|
+
"""
|
37
|
+
Extract the status of all `WorkflowTaskV2` of a given `WorkflowV2` that ran
|
38
|
+
on a given `DatasetV2`.
|
39
|
+
|
40
|
+
*NOTE*: the current endpoint is not guaranteed to provide consistent
|
41
|
+
results if the workflow task list is modified in a non-trivial way
|
42
|
+
(that is, by adding intermediate tasks, removing tasks, or changing their
|
43
|
+
order). See fractal-server GitHub issues: 793, 1083.
|
44
|
+
"""
|
45
|
+
# Get the dataset DB entry
|
46
|
+
output = await _get_dataset_check_owner(
|
47
|
+
project_id=project_id,
|
48
|
+
dataset_id=dataset_id,
|
49
|
+
user_id=user.id,
|
50
|
+
db=db,
|
51
|
+
)
|
52
|
+
dataset = output["dataset"]
|
53
|
+
|
54
|
+
# Get the workflow DB entry
|
55
|
+
workflow = await _get_workflow_check_owner(
|
56
|
+
project_id=project_id,
|
57
|
+
workflow_id=workflow_id,
|
58
|
+
user_id=user.id,
|
59
|
+
db=db,
|
60
|
+
)
|
61
|
+
|
62
|
+
# Check whether there exists a submitted job associated to this
|
63
|
+
# workflow/dataset pair. If it does exist, it will be used later.
|
64
|
+
# If there are multiple jobs, raise an error.
|
65
|
+
stm = _get_submitted_jobs_statement()
|
66
|
+
stm = stm.where(JobV2.dataset_id == dataset_id)
|
67
|
+
stm = stm.where(JobV2.workflow_id == workflow_id)
|
68
|
+
res = await db.execute(stm)
|
69
|
+
running_jobs = res.scalars().all()
|
70
|
+
if len(running_jobs) == 0:
|
71
|
+
running_job = None
|
72
|
+
elif len(running_jobs) == 1:
|
73
|
+
running_job = running_jobs[0]
|
74
|
+
else:
|
75
|
+
string_ids = str([job.id for job in running_jobs])[1:-1]
|
76
|
+
raise HTTPException(
|
77
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
78
|
+
detail=(
|
79
|
+
f"Cannot get WorkflowTaskV2 statuses as DatasetV2 {dataset.id}"
|
80
|
+
f" is linked to multiple active jobs: {string_ids}."
|
81
|
+
),
|
82
|
+
)
|
83
|
+
|
84
|
+
# Initialize empty dictionary for WorkflowTaskV2 status
|
85
|
+
workflow_tasks_status_dict: dict = {}
|
86
|
+
|
87
|
+
# Lowest priority: read status from DB, which corresponds to jobs that are
|
88
|
+
# not running
|
89
|
+
history = dataset.history
|
90
|
+
for history_item in history:
|
91
|
+
wftask_id = history_item["workflowtask"]["id"]
|
92
|
+
wftask_status = history_item["status"]
|
93
|
+
workflow_tasks_status_dict[wftask_id] = wftask_status
|
94
|
+
|
95
|
+
if running_job is None:
|
96
|
+
# If no job is running, the chronological-last history item is also the
|
97
|
+
# positional-last workflow task to be included in the response.
|
98
|
+
if len(history) > 0:
|
99
|
+
last_valid_wftask_id = history[-1]["workflowtask"]["id"]
|
100
|
+
else:
|
101
|
+
last_valid_wftask_id = None
|
102
|
+
else:
|
103
|
+
# If a job is running, then gather more up-to-date information
|
104
|
+
|
105
|
+
# Mid priority: Set all WorkflowTask's that are part of the running job
|
106
|
+
# as "submitted"
|
107
|
+
start = running_job.first_task_index
|
108
|
+
end = running_job.last_task_index + 1
|
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:]:
|
127
|
+
workflow_tasks_status_dict[
|
128
|
+
wftask.id
|
129
|
+
] = WorkflowTaskStatusTypeV2.SUBMITTED
|
130
|
+
|
131
|
+
# The last workflow task that is included in the submitted job is also
|
132
|
+
# the positional-last workflow task to be included in the response.
|
133
|
+
try:
|
134
|
+
last_valid_wftask_id = workflow.task_list[end - 1].id
|
135
|
+
except IndexError as e:
|
136
|
+
logger.warning(
|
137
|
+
f"Handled IndexError in `get_workflowtask_status` ({str(e)})."
|
138
|
+
)
|
139
|
+
logger.warning(
|
140
|
+
"Additional information: "
|
141
|
+
f"{running_job.first_task_index=}; "
|
142
|
+
f"{running_job.last_task_index=}; "
|
143
|
+
f"{len(workflow.task_list)=}; "
|
144
|
+
f"{dataset_id=}; "
|
145
|
+
f"{workflow_id=}."
|
146
|
+
)
|
147
|
+
last_valid_wftask_id = None
|
148
|
+
logger.warning(f"Now setting {last_valid_wftask_id=}.")
|
149
|
+
|
150
|
+
# Based on previously-gathered information, clean up the response body
|
151
|
+
clean_workflow_tasks_status_dict = {}
|
152
|
+
for wf_task in workflow.task_list:
|
153
|
+
wf_task_status = workflow_tasks_status_dict.get(wf_task.id)
|
154
|
+
if wf_task_status is None:
|
155
|
+
# If a wftask ID was not found, ignore it and continue
|
156
|
+
continue
|
157
|
+
clean_workflow_tasks_status_dict[str(wf_task.id)] = wf_task_status
|
158
|
+
if wf_task_status == WorkflowTaskStatusTypeV2.FAILED:
|
159
|
+
# Starting from the beginning of `workflow.task_list`, stop the
|
160
|
+
# first time that you hit a failed job
|
161
|
+
break
|
162
|
+
if wf_task.id == last_valid_wftask_id:
|
163
|
+
# Starting from the beginning of `workflow.task_list`, stop the
|
164
|
+
# first time that you hit `last_valid_wftask_id``
|
165
|
+
break
|
166
|
+
|
167
|
+
response_body = StatusReadV2(status=clean_workflow_tasks_status_dict)
|
168
|
+
return response_body
|
@@ -7,13 +7,10 @@ from fastapi import HTTPException
|
|
7
7
|
from fastapi import Response
|
8
8
|
from fastapi import status
|
9
9
|
from pydantic import BaseModel
|
10
|
-
from sqlmodel import delete
|
11
10
|
from sqlmodel import select
|
12
11
|
|
13
12
|
from ....db import AsyncSession
|
14
13
|
from ....db import get_async_db
|
15
|
-
from ....models.v2 import HistoryItemV2
|
16
|
-
from ....models.v2 import ImageStatus
|
17
14
|
from ....models.v2 import JobV2
|
18
15
|
from ....models.v2 import ProjectV2
|
19
16
|
from ....models.v2 import WorkflowV2
|
@@ -228,26 +225,14 @@ async def delete_workflow(
|
|
228
225
|
),
|
229
226
|
)
|
230
227
|
|
231
|
-
# Cascade operation: set foreign-keys to null for jobs
|
232
|
-
#
|
228
|
+
# Cascade operation: set foreign-keys to null for jobs which are in
|
229
|
+
# relationship with the current workflow.
|
233
230
|
stm = select(JobV2).where(JobV2.workflow_id == workflow_id)
|
234
231
|
res = await db.execute(stm)
|
235
232
|
jobs = res.scalars().all()
|
236
233
|
for job in jobs:
|
237
234
|
job.workflow_id = None
|
238
235
|
|
239
|
-
wft_ids = [wft.id for wft in workflow.task_list]
|
240
|
-
stm = select(HistoryItemV2).where(
|
241
|
-
HistoryItemV2.workflowtask_id.in_(wft_ids)
|
242
|
-
)
|
243
|
-
res = await db.execute(stm)
|
244
|
-
history_items = res.scalars().all()
|
245
|
-
for history_item in history_items:
|
246
|
-
history_item.workflowtask_id = None
|
247
|
-
|
248
|
-
stm = delete(ImageStatus).where(ImageStatus.workflowtask_id.in_(wft_ids))
|
249
|
-
await db.execute(stm)
|
250
|
-
|
251
236
|
# Delete workflow
|
252
237
|
await db.delete(workflow)
|
253
238
|
await db.commit()
|
@@ -6,8 +6,6 @@ from fastapi import Depends
|
|
6
6
|
from fastapi import HTTPException
|
7
7
|
from fastapi import Response
|
8
8
|
from fastapi import status
|
9
|
-
from sqlmodel import delete
|
10
|
-
from sqlmodel import select
|
11
9
|
|
12
10
|
from ....db import AsyncSession
|
13
11
|
from ....db import get_async_db
|
@@ -17,8 +15,6 @@ from ._aux_functions import _workflow_insert_task
|
|
17
15
|
from ._aux_functions_tasks import _check_type_filters_compatibility
|
18
16
|
from ._aux_functions_tasks import _get_task_read_access
|
19
17
|
from fractal_server.app.models import UserOAuth
|
20
|
-
from fractal_server.app.models.v2 import HistoryItemV2
|
21
|
-
from fractal_server.app.models.v2 import ImageStatus
|
22
18
|
from fractal_server.app.models.v2 import WorkflowTaskV2
|
23
19
|
from fractal_server.app.routes.auth import current_active_user
|
24
20
|
from fractal_server.app.schemas.v2 import WorkflowTaskCreateV2
|
@@ -39,80 +35,73 @@ async def replace_workflowtask(
|
|
39
35
|
workflow_id: int,
|
40
36
|
workflow_task_id: int,
|
41
37
|
task_id: int,
|
42
|
-
replace:
|
38
|
+
replace: WorkflowTaskReplaceV2,
|
43
39
|
user: UserOAuth = Depends(current_active_user),
|
44
40
|
db: AsyncSession = Depends(get_async_db),
|
45
41
|
) -> WorkflowTaskReadV2:
|
46
42
|
|
47
|
-
|
43
|
+
# Get objects from database
|
44
|
+
old_wftask, workflow = await _get_workflow_task_check_owner(
|
48
45
|
project_id=project_id,
|
49
46
|
workflow_id=workflow_id,
|
50
47
|
workflow_task_id=workflow_task_id,
|
51
48
|
user_id=user.id,
|
52
49
|
db=db,
|
53
50
|
)
|
54
|
-
|
55
51
|
new_task = await _get_task_read_access(
|
56
|
-
task_id=task_id,
|
52
|
+
task_id=task_id,
|
53
|
+
user_id=user.id,
|
54
|
+
db=db,
|
55
|
+
require_active=True,
|
57
56
|
)
|
58
57
|
|
59
|
-
|
58
|
+
# Preliminary checks
|
59
|
+
if old_wftask.task_type != new_task.type:
|
60
60
|
raise HTTPException(
|
61
61
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
62
62
|
detail=(
|
63
|
-
|
64
|
-
f"
|
63
|
+
"Cannot change task type from "
|
64
|
+
f"{old_wftask.task_type} to {new_task.type}."
|
65
65
|
),
|
66
66
|
)
|
67
|
-
|
67
|
+
if replace.args_non_parallel is not None and new_task.type == "parallel":
|
68
|
+
raise HTTPException(
|
69
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
70
|
+
detail="Cannot set 'args_non_parallel' for parallel task.",
|
71
|
+
)
|
72
|
+
if replace.args_parallel is not None and new_task.type == "non_parallel":
|
73
|
+
raise HTTPException(
|
74
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
75
|
+
detail="Cannot set 'args_parallel' for non-parallel task.",
|
76
|
+
)
|
68
77
|
_check_type_filters_compatibility(
|
69
78
|
task_input_types=new_task.input_types,
|
70
|
-
wftask_type_filters=
|
79
|
+
wftask_type_filters=old_wftask.type_filters,
|
71
80
|
)
|
72
81
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
"when Task is 'parallel'."
|
83
|
-
),
|
84
|
-
)
|
85
|
-
else:
|
86
|
-
_args_non_parallel = replace.args_non_parallel
|
87
|
-
|
88
|
-
if replace.args_parallel is not None:
|
89
|
-
if new_task.type == "non_parallel":
|
90
|
-
raise HTTPException(
|
91
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
92
|
-
detail=(
|
93
|
-
"Cannot set 'args_parallel' "
|
94
|
-
"when Task is 'non_parallel'."
|
95
|
-
),
|
96
|
-
)
|
97
|
-
else:
|
98
|
-
_args_parallel = replace.args_parallel
|
82
|
+
# Task arguments
|
83
|
+
if replace.args_non_parallel is None:
|
84
|
+
_args_non_parallel = old_wftask.args_non_parallel
|
85
|
+
else:
|
86
|
+
_args_non_parallel = replace.args_non_parallel
|
87
|
+
if replace.args_parallel is None:
|
88
|
+
_args_parallel = old_wftask.args_parallel
|
89
|
+
else:
|
90
|
+
_args_parallel = replace.args_parallel
|
99
91
|
|
100
92
|
# If user's changes to `meta_non_parallel` are compatible with new task,
|
101
93
|
# keep them; else, get `meta_non_parallel` from new task
|
102
94
|
if (
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
old_workflow_task.task.meta_non_parallel == new_task.meta_non_parallel
|
107
|
-
):
|
108
|
-
_meta_non_parallel = old_workflow_task.meta_non_parallel
|
95
|
+
old_wftask.meta_non_parallel != old_wftask.task.meta_non_parallel
|
96
|
+
) and (old_wftask.task.meta_non_parallel == new_task.meta_non_parallel):
|
97
|
+
_meta_non_parallel = old_wftask.meta_non_parallel
|
109
98
|
else:
|
110
99
|
_meta_non_parallel = new_task.meta_non_parallel
|
111
100
|
# Same for `meta_parallel`
|
112
|
-
if (
|
113
|
-
|
114
|
-
)
|
115
|
-
_meta_parallel =
|
101
|
+
if (old_wftask.meta_parallel != old_wftask.task.meta_parallel) and (
|
102
|
+
old_wftask.task.meta_parallel == new_task.meta_parallel
|
103
|
+
):
|
104
|
+
_meta_parallel = old_wftask.meta_parallel
|
116
105
|
else:
|
117
106
|
_meta_parallel = new_task.meta_parallel
|
118
107
|
|
@@ -121,7 +110,7 @@ async def replace_workflowtask(
|
|
121
110
|
task_type=new_task.type,
|
122
111
|
task=new_task,
|
123
112
|
# old-task values
|
124
|
-
type_filters=
|
113
|
+
type_filters=old_wftask.type_filters,
|
125
114
|
# possibly new values
|
126
115
|
args_non_parallel=_args_non_parallel,
|
127
116
|
args_parallel=_args_parallel,
|
@@ -129,8 +118,8 @@ async def replace_workflowtask(
|
|
129
118
|
meta_parallel=_meta_parallel,
|
130
119
|
)
|
131
120
|
|
132
|
-
workflow_task_order =
|
133
|
-
workflow.task_list.remove(
|
121
|
+
workflow_task_order = old_wftask.order
|
122
|
+
workflow.task_list.remove(old_wftask)
|
134
123
|
workflow.task_list.insert(workflow_task_order, new_workflow_task)
|
135
124
|
await db.commit()
|
136
125
|
await db.refresh(new_workflow_task)
|
@@ -205,8 +194,6 @@ async def create_workflowtask(
|
|
205
194
|
db=db,
|
206
195
|
)
|
207
196
|
|
208
|
-
await db.close()
|
209
|
-
|
210
197
|
return wftask_db
|
211
198
|
|
212
199
|
|
@@ -337,23 +324,6 @@ async def delete_workflowtask(
|
|
337
324
|
db=db,
|
338
325
|
)
|
339
326
|
|
340
|
-
# Cascade operations:
|
341
|
-
# * set foreign-keys to null for history items which are in relationship
|
342
|
-
# with the current workflowtask;
|
343
|
-
# * delete ImageStatus in relationship with the current workflowtask.
|
344
|
-
stm = select(HistoryItemV2).where(
|
345
|
-
HistoryItemV2.workflowtask_id == db_workflow_task.id
|
346
|
-
)
|
347
|
-
res = await db.execute(stm)
|
348
|
-
history_items = res.scalars().all()
|
349
|
-
for history_item in history_items:
|
350
|
-
history_item.workflowtask_id = None
|
351
|
-
|
352
|
-
stm = delete(ImageStatus).where(
|
353
|
-
ImageStatus.workflowtask_id == db_workflow_task.id
|
354
|
-
)
|
355
|
-
await db.execute(stm)
|
356
|
-
|
357
327
|
# Delete WorkflowTask
|
358
328
|
await db.delete(db_workflow_task)
|
359
329
|
await db.commit()
|
@@ -27,20 +27,22 @@ for client_config in settings.OAUTH_CLIENTS_CONFIG:
|
|
27
27
|
from httpx_oauth.clients.google import GoogleOAuth2
|
28
28
|
|
29
29
|
client = GoogleOAuth2(
|
30
|
-
client_config.CLIENT_ID,
|
30
|
+
client_config.CLIENT_ID,
|
31
|
+
client_config.CLIENT_SECRET.get_secret_value(),
|
31
32
|
)
|
32
33
|
elif client_name == "github":
|
33
34
|
from httpx_oauth.clients.github import GitHubOAuth2
|
34
35
|
|
35
36
|
client = GitHubOAuth2(
|
36
|
-
client_config.CLIENT_ID,
|
37
|
+
client_config.CLIENT_ID,
|
38
|
+
client_config.CLIENT_SECRET.get_secret_value(),
|
37
39
|
)
|
38
40
|
else:
|
39
41
|
from httpx_oauth.clients.openid import OpenID
|
40
42
|
|
41
43
|
client = OpenID(
|
42
44
|
client_config.CLIENT_ID,
|
43
|
-
client_config.CLIENT_SECRET,
|
45
|
+
client_config.CLIENT_SECRET.get_secret_value(),
|
44
46
|
client_config.OIDC_CONFIGURATION_ENDPOINT,
|
45
47
|
)
|
46
48
|
|
@@ -106,7 +106,8 @@ class BaseRunner(object):
|
|
106
106
|
)
|
107
107
|
if _COMPONENT_KEY_ not in single_kwargs.keys():
|
108
108
|
raise ValueError(
|
109
|
-
f"No '{_COMPONENT_KEY_}' key
|
109
|
+
f"No '{_COMPONENT_KEY_}' key "
|
110
|
+
f"in {list(single_kwargs.keys())}"
|
110
111
|
)
|
111
112
|
if not in_compound_task:
|
112
113
|
zarr_urls = [kwargs["zarr_url"] for kwargs in list_parameters]
|
@@ -21,18 +21,10 @@ def _local_submit_setup(
|
|
21
21
|
FIXME
|
22
22
|
|
23
23
|
Arguments:
|
24
|
-
wftask:
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
workflow_dir_remote:
|
29
|
-
Not used in this function.
|
30
|
-
|
31
|
-
Returns:
|
32
|
-
submit_setup_dict:
|
33
|
-
A dictionary that will be passed on to
|
34
|
-
`FractalThreadPoolExecutor.submit` and
|
35
|
-
`FractalThreadPoolExecutor.map`, so as to set extra options.
|
24
|
+
wftask: WorkflowTask for which the configuration is to be assembled
|
25
|
+
root_dir_local:
|
26
|
+
root_dir_rempote: Not used in this function.
|
27
|
+
which_type: Whether it is a parallel or non-parallel task.
|
36
28
|
"""
|
37
29
|
|
38
30
|
local_backend_config = get_local_backend_config(
|
@@ -43,7 +35,7 @@ def _local_submit_setup(
|
|
43
35
|
# Get TaskFiles object
|
44
36
|
task_files = TaskFiles(
|
45
37
|
root_dir_local=root_dir_local,
|
46
|
-
root_dir_remote=
|
38
|
+
root_dir_remote=root_dir_local,
|
47
39
|
task_order=wftask.order,
|
48
40
|
task_name=wftask.task.name,
|
49
41
|
)
|
@@ -1,3 +1,4 @@
|
|
1
|
+
from concurrent.futures import Future
|
1
2
|
from concurrent.futures import ThreadPoolExecutor
|
2
3
|
from pathlib import Path
|
3
4
|
from typing import Any
|
@@ -5,15 +6,12 @@ from typing import Optional
|
|
5
6
|
|
6
7
|
from ._local_config import get_default_local_backend_config
|
7
8
|
from ._local_config import LocalBackendConfig
|
8
|
-
from fractal_server.app.history import HistoryItemImageStatus
|
9
|
-
from fractal_server.app.history import update_all_images
|
10
|
-
from fractal_server.app.history import update_single_image
|
11
|
-
from fractal_server.app.history import update_single_image_logfile
|
12
9
|
from fractal_server.app.runner.components import _COMPONENT_KEY_
|
13
10
|
from fractal_server.app.runner.executors.base_runner import BaseRunner
|
14
11
|
from fractal_server.app.runner.task_files import TaskFiles
|
15
12
|
from fractal_server.logger import set_logger
|
16
13
|
|
14
|
+
|
17
15
|
logger = set_logger(__name__)
|
18
16
|
|
19
17
|
|
@@ -51,10 +49,8 @@ class LocalRunner(BaseRunner):
|
|
51
49
|
self,
|
52
50
|
func: callable,
|
53
51
|
parameters: dict[str, Any],
|
54
|
-
history_item_id: int,
|
55
52
|
task_files: TaskFiles,
|
56
|
-
|
57
|
-
**kwargs,
|
53
|
+
local_backend_config: Optional[LocalBackendConfig] = None,
|
58
54
|
) -> tuple[Any, Exception]:
|
59
55
|
logger.debug("[submit] START")
|
60
56
|
|
@@ -68,27 +64,17 @@ class LocalRunner(BaseRunner):
|
|
68
64
|
self.validate_submit_parameters(parameters)
|
69
65
|
workdir_local = current_task_files.wftask_subfolder_local
|
70
66
|
workdir_local.mkdir()
|
67
|
+
|
71
68
|
# SUBMISSION PHASE
|
72
69
|
future = self.executor.submit(func, parameters=parameters)
|
73
70
|
|
74
71
|
# RETRIEVAL PHASE
|
75
72
|
try:
|
76
73
|
result = future.result()
|
77
|
-
if not in_compound_task:
|
78
|
-
update_all_images(
|
79
|
-
history_item_id=history_item_id,
|
80
|
-
status=HistoryItemImageStatus.DONE,
|
81
|
-
logfile=current_task_files.log_file_local,
|
82
|
-
)
|
83
74
|
logger.debug(f"[submit] END {result=}")
|
84
75
|
return result, None
|
85
76
|
except Exception as e:
|
86
77
|
exception = e
|
87
|
-
update_all_images(
|
88
|
-
history_item_id=history_item_id,
|
89
|
-
status=HistoryItemImageStatus.FAILED,
|
90
|
-
logfile=current_task_files.log_file_local,
|
91
|
-
)
|
92
78
|
logger.debug(f"[submit] END {exception=}")
|
93
79
|
return None, exception
|
94
80
|
|
@@ -96,11 +82,9 @@ class LocalRunner(BaseRunner):
|
|
96
82
|
self,
|
97
83
|
func: callable,
|
98
84
|
list_parameters: list[dict],
|
99
|
-
history_item_id: int,
|
100
85
|
task_files: TaskFiles,
|
101
86
|
in_compound_task: bool = False,
|
102
87
|
local_backend_config: Optional[LocalBackendConfig] = None,
|
103
|
-
**kwargs,
|
104
88
|
):
|
105
89
|
logger.debug(f"[multisubmit] START, {len(list_parameters)=}")
|
106
90
|
|
@@ -126,13 +110,12 @@ class LocalRunner(BaseRunner):
|
|
126
110
|
original_task_files = task_files
|
127
111
|
|
128
112
|
# Execute tasks, in chunks of size `parallel_tasks_per_job`
|
129
|
-
results = {}
|
130
|
-
exceptions = {}
|
113
|
+
results: dict[int, Any] = {}
|
114
|
+
exceptions: dict[int, BaseException] = {}
|
131
115
|
for ind_chunk in range(0, n_elements, parallel_tasks_per_job):
|
132
116
|
list_parameters_chunk = list_parameters[
|
133
117
|
ind_chunk : ind_chunk + parallel_tasks_per_job
|
134
118
|
]
|
135
|
-
from concurrent.futures import Future
|
136
119
|
|
137
120
|
active_futures: dict[int, Future] = {}
|
138
121
|
active_task_files: dict[int, TaskFiles] = {}
|
@@ -156,45 +139,17 @@ class LocalRunner(BaseRunner):
|
|
156
139
|
]
|
157
140
|
for positional_index, fut in finished_futures:
|
158
141
|
active_futures.pop(positional_index)
|
159
|
-
current_task_files = active_task_files.pop(
|
160
|
-
|
161
|
-
)
|
142
|
+
# current_task_files = active_task_files.pop(
|
143
|
+
# positional_index
|
144
|
+
# )
|
162
145
|
zarr_url = list_parameters[positional_index]["zarr_url"]
|
163
|
-
if not in_compound_task:
|
164
|
-
update_single_image_logfile(
|
165
|
-
history_item_id=history_item_id,
|
166
|
-
zarr_url=zarr_url,
|
167
|
-
logfile=current_task_files.log_file_local,
|
168
|
-
)
|
169
146
|
try:
|
170
147
|
results[positional_index] = fut.result()
|
171
148
|
print(f"Mark {zarr_url=} as done, {kwargs}")
|
172
|
-
if not in_compound_task:
|
173
|
-
update_single_image(
|
174
|
-
history_item_id=history_item_id,
|
175
|
-
zarr_url=zarr_url,
|
176
|
-
status=HistoryItemImageStatus.DONE,
|
177
|
-
)
|
178
149
|
except Exception as e:
|
179
150
|
print(f"Mark {zarr_url=} as failed, {kwargs} - {e}")
|
180
151
|
exceptions[positional_index] = e
|
181
|
-
|
182
|
-
update_single_image(
|
183
|
-
history_item_id=history_item_id,
|
184
|
-
zarr_url=zarr_url,
|
185
|
-
status=HistoryItemImageStatus.FAILED,
|
186
|
-
)
|
187
|
-
if in_compound_task:
|
188
|
-
if exceptions == {}:
|
189
|
-
update_all_images(
|
190
|
-
history_item_id=history_item_id,
|
191
|
-
status=HistoryItemImageStatus.DONE,
|
192
|
-
)
|
193
|
-
else:
|
194
|
-
update_all_images(
|
195
|
-
history_item_id=history_item_id,
|
196
|
-
status=HistoryItemImageStatus.FAILED,
|
197
|
-
)
|
152
|
+
|
198
153
|
logger.debug(f"[multisubmit] END, {results=}, {exceptions=}")
|
199
154
|
|
200
155
|
return results, exceptions
|
@@ -213,7 +213,7 @@ class SlurmConfig(BaseModel):
|
|
213
213
|
expected file content are defined in
|
214
214
|
[`SlurmConfigFile`](./#fractal_server.app.runner._slurm._slurm_config.SlurmConfigFile)).
|
215
215
|
|
216
|
-
Part of the attributes map directly to some of the SLURM
|
216
|
+
Part of the attributes map directly to some of the SLURM attributes (see
|
217
217
|
https://slurm.schedmd.com/sbatch.html), e.g. `partition`. Other attributes
|
218
218
|
are metaparameters which are needed in fractal-server to combine multiple
|
219
219
|
tasks in the same SLURM job (e.g. `parallel_tasks_per_job` or
|
@@ -19,7 +19,7 @@ def get_slurm_config(
|
|
19
19
|
Prepare a `SlurmConfig` configuration object
|
20
20
|
|
21
21
|
The argument `which_type` determines whether we use `wftask.meta_parallel`
|
22
|
-
or `wftask.meta_non_parallel`. In the following
|
22
|
+
or `wftask.meta_non_parallel`. In the following description, let us assume
|
23
23
|
that `which_type="parallel"`.
|
24
24
|
|
25
25
|
The sources for `SlurmConfig` attributes, in increasing priority order, are
|
@@ -134,7 +134,7 @@ def worker(
|
|
134
134
|
_extra_import_paths = extra_import_paths.split(":")
|
135
135
|
sys.path[:0] = _extra_import_paths
|
136
136
|
|
137
|
-
# Execute the job and
|
137
|
+
# Execute the job and capture exceptions
|
138
138
|
try:
|
139
139
|
with open(in_fname, "rb") as f:
|
140
140
|
indata = f.read()
|