fractal-server 2.1.0__py3-none-any.whl → 2.2.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/db/__init__.py +1 -1
- fractal_server/app/routes/admin/v1.py +2 -4
- fractal_server/app/routes/admin/v2.py +2 -4
- fractal_server/app/routes/api/v1/_aux_functions.py +24 -0
- fractal_server/app/routes/api/v1/job.py +3 -4
- fractal_server/app/routes/api/v1/project.py +28 -18
- fractal_server/app/routes/api/v2/_aux_functions.py +35 -12
- fractal_server/app/routes/api/v2/job.py +3 -4
- fractal_server/app/routes/api/v2/project.py +21 -0
- fractal_server/app/routes/api/v2/submit.py +33 -7
- fractal_server/app/routes/aux/_job.py +3 -1
- fractal_server/app/routes/aux/_runner.py +3 -3
- fractal_server/app/runner/executors/slurm/executor.py +157 -68
- fractal_server/app/runner/shutdown.py +88 -0
- fractal_server/app/runner/task_files.py +59 -27
- fractal_server/app/runner/v1/__init__.py +35 -19
- fractal_server/app/runner/v1/_common.py +53 -51
- fractal_server/app/runner/v1/_local/__init__.py +12 -11
- fractal_server/app/runner/v1/_local/_submit_setup.py +4 -4
- fractal_server/app/runner/v1/_slurm/__init__.py +16 -16
- fractal_server/app/runner/v1/_slurm/_submit_setup.py +11 -10
- fractal_server/app/runner/v1/_slurm/get_slurm_config.py +6 -6
- fractal_server/app/runner/v2/__init__.py +65 -17
- fractal_server/app/runner/v2/_local/__init__.py +12 -11
- fractal_server/app/runner/v2/_local/_local_config.py +1 -1
- fractal_server/app/runner/v2/_local/_submit_setup.py +4 -4
- fractal_server/app/runner/v2/_local_experimental/__init__.py +145 -0
- fractal_server/app/runner/v2/_local_experimental/_local_config.py +108 -0
- fractal_server/app/runner/v2/_local_experimental/_submit_setup.py +42 -0
- fractal_server/app/runner/v2/_local_experimental/executor.py +152 -0
- fractal_server/app/runner/v2/_slurm/__init__.py +10 -10
- fractal_server/app/runner/v2/_slurm/_submit_setup.py +11 -10
- fractal_server/app/runner/v2/_slurm/get_slurm_config.py +6 -6
- fractal_server/app/runner/v2/runner.py +17 -15
- fractal_server/app/runner/v2/runner_functions.py +38 -38
- fractal_server/app/runner/v2/runner_functions_low_level.py +12 -6
- fractal_server/config.py +52 -19
- fractal_server/gunicorn_fractal.py +40 -0
- fractal_server/{logger/__init__.py → logger.py} +2 -2
- fractal_server/main.py +24 -1
- fractal_server/migrations/env.py +1 -1
- {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/METADATA +3 -1
- {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/RECORD +47 -42
- fractal_server/logger/gunicorn_logger.py +0 -19
- {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/LICENSE +0 -0
- {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/WHEEL +0 -0
- {fractal_server-2.1.0.dist-info → fractal_server-2.2.0a1.dist-info}/entry_points.txt +0 -0
@@ -50,8 +50,8 @@ def _process_workflow(
|
|
50
50
|
input_metadata: dict[str, Any],
|
51
51
|
input_history: list[dict[str, Any]],
|
52
52
|
logger_name: str,
|
53
|
-
|
54
|
-
|
53
|
+
workflow_dir_local: Path,
|
54
|
+
workflow_dir_remote: Path,
|
55
55
|
first_task_index: int,
|
56
56
|
last_task_index: int,
|
57
57
|
slurm_user: Optional[str] = None,
|
@@ -86,8 +86,8 @@ def _process_workflow(
|
|
86
86
|
keep_logs=True,
|
87
87
|
slurm_user=slurm_user,
|
88
88
|
user_cache_dir=user_cache_dir,
|
89
|
-
|
90
|
-
|
89
|
+
workflow_dir_local=workflow_dir_local,
|
90
|
+
workflow_dir_remote=workflow_dir_remote,
|
91
91
|
common_script_lines=worker_init,
|
92
92
|
slurm_account=slurm_account,
|
93
93
|
) as executor:
|
@@ -102,8 +102,8 @@ def _process_workflow(
|
|
102
102
|
metadata=input_metadata,
|
103
103
|
history=input_history,
|
104
104
|
),
|
105
|
-
|
106
|
-
|
105
|
+
workflow_dir_local=workflow_dir_local,
|
106
|
+
workflow_dir_remote=workflow_dir_remote,
|
107
107
|
submit_setup_call=_slurm_submit_setup,
|
108
108
|
logger_name=logger_name,
|
109
109
|
)
|
@@ -121,8 +121,8 @@ async def process_workflow(
|
|
121
121
|
input_metadata: dict[str, Any],
|
122
122
|
input_history: list[dict[str, Any]],
|
123
123
|
logger_name: str,
|
124
|
-
|
125
|
-
|
124
|
+
workflow_dir_local: Path,
|
125
|
+
workflow_dir_remote: Optional[Path] = None,
|
126
126
|
user_cache_dir: Optional[str] = None,
|
127
127
|
slurm_user: Optional[str] = None,
|
128
128
|
slurm_account: Optional[str] = None,
|
@@ -152,8 +152,8 @@ async def process_workflow(
|
|
152
152
|
input_metadata=input_metadata,
|
153
153
|
input_history=input_history,
|
154
154
|
logger_name=logger_name,
|
155
|
-
|
156
|
-
|
155
|
+
workflow_dir_local=workflow_dir_local,
|
156
|
+
workflow_dir_remote=workflow_dir_remote,
|
157
157
|
slurm_user=slurm_user,
|
158
158
|
slurm_account=slurm_account,
|
159
159
|
user_cache_dir=user_cache_dir,
|
@@ -166,8 +166,8 @@ async def process_workflow(
|
|
166
166
|
|
167
167
|
def get_slurm_config(
|
168
168
|
wftask: WorkflowTask,
|
169
|
-
|
170
|
-
|
169
|
+
workflow_dir_local: Path,
|
170
|
+
workflow_dir_remote: Path,
|
171
171
|
config_path: Optional[Path] = None,
|
172
172
|
) -> SlurmConfig:
|
173
173
|
"""
|
@@ -187,13 +187,13 @@ def get_slurm_config(
|
|
187
187
|
wftask:
|
188
188
|
WorkflowTask for which the SLURM configuration is is to be
|
189
189
|
prepared.
|
190
|
-
|
190
|
+
workflow_dir_local:
|
191
191
|
Server-owned directory to store all task-execution-related relevant
|
192
192
|
files (inputs, outputs, errors, and all meta files related to the
|
193
193
|
job execution). Note: users cannot write directly to this folder.
|
194
|
-
|
195
|
-
User-side directory with the same scope as `
|
196
|
-
where a user can write.
|
194
|
+
workflow_dir_remote:
|
195
|
+
User-side directory with the same scope as `workflow_dir_local`,
|
196
|
+
and where a user can write.
|
197
197
|
config_path:
|
198
198
|
Path of aFractal SLURM configuration file; if `None`, use
|
199
199
|
`FRACTAL_SLURM_CONFIG_FILE` variable from settings.
|
@@ -24,8 +24,8 @@ from fractal_server.app.models.v1 import WorkflowTask
|
|
24
24
|
def _slurm_submit_setup(
|
25
25
|
*,
|
26
26
|
wftask: WorkflowTask,
|
27
|
-
|
28
|
-
|
27
|
+
workflow_dir_local: Path,
|
28
|
+
workflow_dir_remote: Path,
|
29
29
|
) -> dict[str, object]:
|
30
30
|
"""
|
31
31
|
Collect WorfklowTask-specific configuration parameters from different
|
@@ -43,13 +43,13 @@ def _slurm_submit_setup(
|
|
43
43
|
Arguments:
|
44
44
|
wftask:
|
45
45
|
WorkflowTask for which the configuration is to be assembled
|
46
|
-
|
46
|
+
workflow_dir_local:
|
47
47
|
Server-owned directory to store all task-execution-related relevant
|
48
48
|
files (inputs, outputs, errors, and all meta files related to the
|
49
49
|
job execution). Note: users cannot write directly to this folder.
|
50
|
-
|
51
|
-
User-side directory with the same scope as `
|
52
|
-
where a user can write.
|
50
|
+
workflow_dir_remote:
|
51
|
+
User-side directory with the same scope as `workflow_dir_local`,
|
52
|
+
and where a user can write.
|
53
53
|
|
54
54
|
Returns:
|
55
55
|
submit_setup_dict:
|
@@ -61,15 +61,16 @@ def _slurm_submit_setup(
|
|
61
61
|
# Get SlurmConfig object
|
62
62
|
slurm_config = get_slurm_config(
|
63
63
|
wftask=wftask,
|
64
|
-
|
65
|
-
|
64
|
+
workflow_dir_local=workflow_dir_local,
|
65
|
+
workflow_dir_remote=workflow_dir_remote,
|
66
66
|
)
|
67
67
|
|
68
68
|
# Get TaskFiles object
|
69
69
|
task_files = get_task_file_paths(
|
70
|
-
|
71
|
-
|
70
|
+
workflow_dir_local=workflow_dir_local,
|
71
|
+
workflow_dir_remote=workflow_dir_remote,
|
72
72
|
task_order=wftask.order,
|
73
|
+
task_name=wftask.task.name,
|
73
74
|
)
|
74
75
|
|
75
76
|
# Prepare and return output dictionary
|
@@ -17,8 +17,8 @@ from fractal_server.app.runner.executors.slurm._slurm_config import (
|
|
17
17
|
|
18
18
|
def get_slurm_config(
|
19
19
|
wftask: WorkflowTask,
|
20
|
-
|
21
|
-
|
20
|
+
workflow_dir_local: Path,
|
21
|
+
workflow_dir_remote: Path,
|
22
22
|
config_path: Optional[Path] = None,
|
23
23
|
) -> SlurmConfig:
|
24
24
|
"""
|
@@ -38,13 +38,13 @@ def get_slurm_config(
|
|
38
38
|
wftask:
|
39
39
|
WorkflowTask for which the SLURM configuration is is to be
|
40
40
|
prepared.
|
41
|
-
|
41
|
+
workflow_dir_local:
|
42
42
|
Server-owned directory to store all task-execution-related relevant
|
43
43
|
files (inputs, outputs, errors, and all meta files related to the
|
44
44
|
job execution). Note: users cannot write directly to this folder.
|
45
|
-
|
46
|
-
User-side directory with the same scope as `
|
47
|
-
where a user can write.
|
45
|
+
workflow_dir_remote:
|
46
|
+
User-side directory with the same scope as `workflow_dir_local`,
|
47
|
+
and where a user can write.
|
48
48
|
config_path:
|
49
49
|
Path of aFractal SLURM configuration file; if `None`, use
|
50
50
|
`FRACTAL_SLURM_CONFIG_FILE` variable from settings.
|
@@ -25,8 +25,13 @@ from ...models.v2 import WorkflowV2
|
|
25
25
|
from ...schemas.v2 import JobStatusTypeV2
|
26
26
|
from ..exceptions import JobExecutionError
|
27
27
|
from ..exceptions import TaskExecutionError
|
28
|
+
from ..executors.slurm._subprocess_run_as_user import _mkdir_as_user
|
28
29
|
from ..filenames import WORKFLOW_LOG_FILENAME
|
30
|
+
from ..task_files import task_subfolder_name
|
29
31
|
from ._local import process_workflow as local_process_workflow
|
32
|
+
from ._local_experimental import (
|
33
|
+
process_workflow as local_experimental_process_workflow,
|
34
|
+
)
|
30
35
|
from ._slurm import process_workflow as slurm_process_workflow
|
31
36
|
from .handle_failed_job import assemble_filters_failed_job
|
32
37
|
from .handle_failed_job import assemble_history_failed_job
|
@@ -35,6 +40,7 @@ from fractal_server import __VERSION__
|
|
35
40
|
|
36
41
|
_backends = {}
|
37
42
|
_backends["local"] = local_process_workflow
|
43
|
+
_backends["local_experimental"] = local_experimental_process_workflow
|
38
44
|
_backends["slurm"] = slurm_process_workflow
|
39
45
|
|
40
46
|
|
@@ -78,6 +84,8 @@ async def submit_workflow(
|
|
78
84
|
FRACTAL_RUNNER_BACKEND = settings.FRACTAL_RUNNER_BACKEND
|
79
85
|
if FRACTAL_RUNNER_BACKEND == "local":
|
80
86
|
process_workflow = local_process_workflow
|
87
|
+
elif FRACTAL_RUNNER_BACKEND == "local_experimental":
|
88
|
+
process_workflow = local_experimental_process_workflow
|
81
89
|
elif FRACTAL_RUNNER_BACKEND == "slurm":
|
82
90
|
process_workflow = slurm_process_workflow
|
83
91
|
else:
|
@@ -108,29 +116,67 @@ async def submit_workflow(
|
|
108
116
|
return
|
109
117
|
|
110
118
|
# Define and create server-side working folder
|
111
|
-
|
112
|
-
if
|
119
|
+
WORKFLOW_DIR_LOCAL = Path(job.working_dir)
|
120
|
+
if WORKFLOW_DIR_LOCAL.exists():
|
113
121
|
job.status = JobStatusTypeV2.FAILED
|
114
122
|
job.end_timestamp = get_timestamp()
|
115
|
-
job.log = f"Workflow dir {
|
123
|
+
job.log = f"Workflow dir {WORKFLOW_DIR_LOCAL} already exists."
|
116
124
|
db_sync.merge(job)
|
117
125
|
db_sync.commit()
|
118
126
|
db_sync.close()
|
119
127
|
return
|
120
128
|
|
121
|
-
|
122
|
-
original_umask = os.umask(0)
|
123
|
-
WORKFLOW_DIR.mkdir(parents=True, mode=0o755)
|
124
|
-
os.umask(original_umask)
|
129
|
+
try:
|
125
130
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
)
|
131
|
+
# Create WORKFLOW_DIR
|
132
|
+
original_umask = os.umask(0)
|
133
|
+
WORKFLOW_DIR_LOCAL.mkdir(parents=True, mode=0o755)
|
134
|
+
|
135
|
+
os.umask(original_umask)
|
132
136
|
|
133
|
-
|
137
|
+
# Define and create WORKFLOW_DIR_REMOTE
|
138
|
+
if FRACTAL_RUNNER_BACKEND == "local":
|
139
|
+
WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
|
140
|
+
elif FRACTAL_RUNNER_BACKEND == "local_experimental":
|
141
|
+
WORKFLOW_DIR_REMOTE = WORKFLOW_DIR_LOCAL
|
142
|
+
elif FRACTAL_RUNNER_BACKEND == "slurm":
|
143
|
+
WORKFLOW_DIR_REMOTE = (
|
144
|
+
Path(user_cache_dir) / WORKFLOW_DIR_LOCAL.name
|
145
|
+
)
|
146
|
+
_mkdir_as_user(
|
147
|
+
folder=str(WORKFLOW_DIR_REMOTE), user=slurm_user
|
148
|
+
)
|
149
|
+
|
150
|
+
# Create all tasks subfolders
|
151
|
+
for order in range(job.first_task_index, job.last_task_index + 1):
|
152
|
+
this_wftask = workflow.task_list[order]
|
153
|
+
if this_wftask.is_legacy_task:
|
154
|
+
task_name = this_wftask.task_legacy.name
|
155
|
+
else:
|
156
|
+
task_name = this_wftask.task.name
|
157
|
+
subfolder_name = task_subfolder_name(
|
158
|
+
order=order,
|
159
|
+
task_name=task_name,
|
160
|
+
)
|
161
|
+
original_umask = os.umask(0)
|
162
|
+
(WORKFLOW_DIR_LOCAL / subfolder_name).mkdir(mode=0o755)
|
163
|
+
os.umask(original_umask)
|
164
|
+
if FRACTAL_RUNNER_BACKEND == "slurm":
|
165
|
+
_mkdir_as_user(
|
166
|
+
folder=str(WORKFLOW_DIR_REMOTE / subfolder_name),
|
167
|
+
user=slurm_user,
|
168
|
+
)
|
169
|
+
except Exception as e:
|
170
|
+
job.status = JobStatusTypeV2.FAILED
|
171
|
+
job.end_timestamp = get_timestamp()
|
172
|
+
job.log = (
|
173
|
+
"An error occurred while creating job folder and subfolders.\n"
|
174
|
+
f"Original error: {str(e)}"
|
175
|
+
)
|
176
|
+
db_sync.merge(job)
|
177
|
+
db_sync.commit()
|
178
|
+
db_sync.close()
|
179
|
+
return
|
134
180
|
|
135
181
|
# After Session.commit() is called, either explicitly or when using a
|
136
182
|
# context manager, all objects associated with the Session are expired.
|
@@ -145,10 +191,12 @@ async def submit_workflow(
|
|
145
191
|
|
146
192
|
db_sync.refresh(dataset)
|
147
193
|
db_sync.refresh(workflow)
|
194
|
+
for wftask in workflow.task_list:
|
195
|
+
db_sync.refresh(wftask)
|
148
196
|
|
149
197
|
# Write logs
|
150
198
|
logger_name = f"WF{workflow_id}_job{job_id}"
|
151
|
-
log_file_path =
|
199
|
+
log_file_path = WORKFLOW_DIR_LOCAL / WORKFLOW_LOG_FILENAME
|
152
200
|
logger = set_logger(
|
153
201
|
logger_name=logger_name,
|
154
202
|
log_file_path=log_file_path,
|
@@ -189,8 +237,8 @@ async def submit_workflow(
|
|
189
237
|
slurm_user=slurm_user,
|
190
238
|
slurm_account=job.slurm_account,
|
191
239
|
user_cache_dir=user_cache_dir,
|
192
|
-
|
193
|
-
|
240
|
+
workflow_dir_local=WORKFLOW_DIR_LOCAL,
|
241
|
+
workflow_dir_remote=WORKFLOW_DIR_REMOTE,
|
194
242
|
logger_name=logger_name,
|
195
243
|
worker_init=worker_init,
|
196
244
|
first_task_index=job.first_task_index,
|
@@ -36,7 +36,7 @@ def _process_workflow(
|
|
36
36
|
workflow: WorkflowV2,
|
37
37
|
dataset: DatasetV2,
|
38
38
|
logger_name: str,
|
39
|
-
|
39
|
+
workflow_dir_local: Path,
|
40
40
|
first_task_index: int,
|
41
41
|
last_task_index: int,
|
42
42
|
) -> dict:
|
@@ -57,8 +57,8 @@ def _process_workflow(
|
|
57
57
|
], # noqa
|
58
58
|
dataset=dataset,
|
59
59
|
executor=executor,
|
60
|
-
|
61
|
-
|
60
|
+
workflow_dir_local=workflow_dir_local,
|
61
|
+
workflow_dir_remote=workflow_dir_local,
|
62
62
|
logger_name=logger_name,
|
63
63
|
submit_setup_call=_local_submit_setup,
|
64
64
|
)
|
@@ -69,8 +69,8 @@ async def process_workflow(
|
|
69
69
|
*,
|
70
70
|
workflow: WorkflowV2,
|
71
71
|
dataset: DatasetV2,
|
72
|
-
|
73
|
-
|
72
|
+
workflow_dir_local: Path,
|
73
|
+
workflow_dir_remote: Optional[Path] = None,
|
74
74
|
first_task_index: Optional[int] = None,
|
75
75
|
last_task_index: Optional[int] = None,
|
76
76
|
logger_name: str,
|
@@ -94,12 +94,13 @@ async def process_workflow(
|
|
94
94
|
The workflow to be run
|
95
95
|
dataset:
|
96
96
|
Initial dataset.
|
97
|
-
|
97
|
+
workflow_dir_local:
|
98
98
|
Working directory for this run.
|
99
|
-
|
99
|
+
workflow_dir_remote:
|
100
100
|
Working directory for this run, on the user side. This argument is
|
101
101
|
present for compatibility with the standard backend interface, but
|
102
|
-
for the `local` backend it cannot be different from
|
102
|
+
for the `local` backend it cannot be different from
|
103
|
+
`workflow_dir_local`.
|
103
104
|
first_task_index:
|
104
105
|
Positional index of the first task to execute; if `None`, start
|
105
106
|
from `0`.
|
@@ -137,10 +138,10 @@ async def process_workflow(
|
|
137
138
|
of the workflow
|
138
139
|
"""
|
139
140
|
|
140
|
-
if
|
141
|
+
if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
|
141
142
|
raise NotImplementedError(
|
142
143
|
"Local backend does not support different directories "
|
143
|
-
f"{
|
144
|
+
f"{workflow_dir_local=} and {workflow_dir_remote=}"
|
144
145
|
)
|
145
146
|
|
146
147
|
# Set values of first_task_index and last_task_index
|
@@ -155,7 +156,7 @@ async def process_workflow(
|
|
155
156
|
workflow=workflow,
|
156
157
|
dataset=dataset,
|
157
158
|
logger_name=logger_name,
|
158
|
-
|
159
|
+
workflow_dir_local=workflow_dir_local,
|
159
160
|
first_task_index=first_task_index,
|
160
161
|
last_task_index=last_task_index,
|
161
162
|
)
|
@@ -22,8 +22,8 @@ from ._local_config import get_local_backend_config
|
|
22
22
|
def _local_submit_setup(
|
23
23
|
*,
|
24
24
|
wftask: WorkflowTaskV2,
|
25
|
-
|
26
|
-
|
25
|
+
workflow_dir_local: Optional[Path] = None,
|
26
|
+
workflow_dir_remote: Optional[Path] = None,
|
27
27
|
which_type: Literal["non_parallel", "parallel"],
|
28
28
|
) -> dict[str, object]:
|
29
29
|
"""
|
@@ -33,9 +33,9 @@ def _local_submit_setup(
|
|
33
33
|
Arguments:
|
34
34
|
wftask:
|
35
35
|
WorkflowTask for which the configuration is to be assembled
|
36
|
-
|
36
|
+
workflow_dir_local:
|
37
37
|
Not used in this function.
|
38
|
-
|
38
|
+
workflow_dir_remote:
|
39
39
|
Not used in this function.
|
40
40
|
|
41
41
|
Returns:
|
@@ -0,0 +1,145 @@
|
|
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 ...async_wrap import async_wrap
|
7
|
+
from ...filenames import SHUTDOWN_FILENAME
|
8
|
+
from ...set_start_and_last_task_index import set_start_and_last_task_index
|
9
|
+
from ..runner import execute_tasks_v2
|
10
|
+
from ._submit_setup import _local_submit_setup
|
11
|
+
from .executor import FractalProcessPoolExecutor
|
12
|
+
|
13
|
+
|
14
|
+
def _process_workflow(
|
15
|
+
*,
|
16
|
+
workflow: WorkflowV2,
|
17
|
+
dataset: DatasetV2,
|
18
|
+
logger_name: str,
|
19
|
+
workflow_dir_local: Path,
|
20
|
+
first_task_index: int,
|
21
|
+
last_task_index: int,
|
22
|
+
) -> dict:
|
23
|
+
"""
|
24
|
+
Internal processing routine
|
25
|
+
|
26
|
+
Schedules the workflow using a `FractalProcessPoolExecutor`.
|
27
|
+
|
28
|
+
Cf.
|
29
|
+
[process_workflow][fractal_server.app.runner.v2._local_experimental.process_workflow]
|
30
|
+
for the call signature.
|
31
|
+
"""
|
32
|
+
|
33
|
+
with FractalProcessPoolExecutor(
|
34
|
+
shutdown_file=workflow_dir_local / SHUTDOWN_FILENAME
|
35
|
+
) as executor:
|
36
|
+
new_dataset_attributes = execute_tasks_v2(
|
37
|
+
wf_task_list=workflow.task_list[
|
38
|
+
first_task_index : (last_task_index + 1) # noqa
|
39
|
+
], # noqa
|
40
|
+
dataset=dataset,
|
41
|
+
executor=executor,
|
42
|
+
workflow_dir_local=workflow_dir_local,
|
43
|
+
workflow_dir_remote=workflow_dir_local,
|
44
|
+
logger_name=logger_name,
|
45
|
+
submit_setup_call=_local_submit_setup,
|
46
|
+
)
|
47
|
+
return new_dataset_attributes
|
48
|
+
|
49
|
+
|
50
|
+
async def process_workflow(
|
51
|
+
*,
|
52
|
+
workflow: WorkflowV2,
|
53
|
+
dataset: DatasetV2,
|
54
|
+
workflow_dir_local: Path,
|
55
|
+
workflow_dir_remote: Optional[Path] = None,
|
56
|
+
first_task_index: Optional[int] = None,
|
57
|
+
last_task_index: Optional[int] = None,
|
58
|
+
logger_name: str,
|
59
|
+
# Slurm-specific
|
60
|
+
user_cache_dir: Optional[str] = None,
|
61
|
+
slurm_user: Optional[str] = None,
|
62
|
+
slurm_account: Optional[str] = None,
|
63
|
+
worker_init: Optional[str] = None,
|
64
|
+
) -> dict:
|
65
|
+
"""
|
66
|
+
Run a workflow
|
67
|
+
|
68
|
+
This function is responsible for running a workflow on some input data,
|
69
|
+
saving the output and taking care of any exception raised during the run.
|
70
|
+
|
71
|
+
NOTE: This is the `local_experimental` backend's public interface,
|
72
|
+
which also works as a reference implementation for other backends.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
workflow:
|
76
|
+
The workflow to be run
|
77
|
+
dataset:
|
78
|
+
Initial dataset.
|
79
|
+
workflow_dir_local:
|
80
|
+
Working directory for this run.
|
81
|
+
workflow_dir_remote:
|
82
|
+
Working directory for this run, on the user side. This argument is
|
83
|
+
present for compatibility with the standard backend interface, but
|
84
|
+
for the `local` backend it cannot be different from
|
85
|
+
`workflow_dir_local`.
|
86
|
+
first_task_index:
|
87
|
+
Positional index of the first task to execute; if `None`, start
|
88
|
+
from `0`.
|
89
|
+
last_task_index:
|
90
|
+
Positional index of the last task to execute; if `None`, proceed
|
91
|
+
until the last task.
|
92
|
+
logger_name: Logger name
|
93
|
+
slurm_user:
|
94
|
+
Username to impersonate to run the workflow. This argument is
|
95
|
+
present for compatibility with the standard backend interface, but
|
96
|
+
is ignored in the `local` backend.
|
97
|
+
slurm_account:
|
98
|
+
SLURM account to use when running the workflow. This argument is
|
99
|
+
present for compatibility with the standard backend interface, but
|
100
|
+
is ignored in the `local` backend.
|
101
|
+
user_cache_dir:
|
102
|
+
Cache directory of the user who will run the workflow. This
|
103
|
+
argument is present for compatibility with the standard backend
|
104
|
+
interface, but is ignored in the `local` backend.
|
105
|
+
worker_init:
|
106
|
+
Any additional, usually backend specific, information to be passed
|
107
|
+
to the backend executor. This argument is present for compatibility
|
108
|
+
with the standard backend interface, but is ignored in the `local`
|
109
|
+
backend.
|
110
|
+
|
111
|
+
Raises:
|
112
|
+
TaskExecutionError: wrapper for errors raised during tasks' execution
|
113
|
+
(positive exit codes).
|
114
|
+
JobExecutionError: wrapper for errors raised by the tasks' executors
|
115
|
+
(negative exit codes).
|
116
|
+
|
117
|
+
Returns:
|
118
|
+
output_dataset_metadata:
|
119
|
+
The updated metadata for the dataset, as returned by the last task
|
120
|
+
of the workflow
|
121
|
+
"""
|
122
|
+
|
123
|
+
if workflow_dir_remote and (workflow_dir_remote != workflow_dir_local):
|
124
|
+
raise NotImplementedError(
|
125
|
+
"LocalExperimental backend does not support different directories "
|
126
|
+
f"{workflow_dir_local=} and {workflow_dir_remote=}"
|
127
|
+
)
|
128
|
+
|
129
|
+
# Set values of first_task_index and last_task_index
|
130
|
+
num_tasks = len(workflow.task_list)
|
131
|
+
first_task_index, last_task_index = set_start_and_last_task_index(
|
132
|
+
num_tasks,
|
133
|
+
first_task_index=first_task_index,
|
134
|
+
last_task_index=last_task_index,
|
135
|
+
)
|
136
|
+
|
137
|
+
new_dataset_attributes = await async_wrap(_process_workflow)(
|
138
|
+
workflow=workflow,
|
139
|
+
dataset=dataset,
|
140
|
+
logger_name=logger_name,
|
141
|
+
workflow_dir_local=workflow_dir_local,
|
142
|
+
first_task_index=first_task_index,
|
143
|
+
last_task_index=last_task_index,
|
144
|
+
)
|
145
|
+
return new_dataset_attributes
|
@@ -0,0 +1,108 @@
|
|
1
|
+
"""
|
2
|
+
Submodule to handle the local-backend configuration for a WorkflowTask
|
3
|
+
"""
|
4
|
+
import json
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Literal
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
from pydantic import BaseModel
|
10
|
+
from pydantic import Extra
|
11
|
+
from pydantic.error_wrappers import ValidationError
|
12
|
+
|
13
|
+
from .....config import get_settings
|
14
|
+
from .....syringe import Inject
|
15
|
+
from ....models.v2 import WorkflowTaskV2
|
16
|
+
|
17
|
+
|
18
|
+
class LocalBackendConfigError(ValueError):
|
19
|
+
"""
|
20
|
+
Local-backend configuration error
|
21
|
+
"""
|
22
|
+
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
class LocalBackendConfig(BaseModel, extra=Extra.forbid):
|
27
|
+
"""
|
28
|
+
Specifications of the local-backend configuration
|
29
|
+
|
30
|
+
Attributes:
|
31
|
+
parallel_tasks_per_job:
|
32
|
+
Maximum number of tasks to be run in parallel as part of a call to
|
33
|
+
`FractalProcessPoolExecutor.map`; if `None`, then all tasks will
|
34
|
+
start at the same time.
|
35
|
+
"""
|
36
|
+
|
37
|
+
parallel_tasks_per_job: Optional[int]
|
38
|
+
|
39
|
+
|
40
|
+
def get_default_local_backend_config():
|
41
|
+
"""
|
42
|
+
Return a default `LocalBackendConfig` configuration object
|
43
|
+
"""
|
44
|
+
return LocalBackendConfig(parallel_tasks_per_job=None)
|
45
|
+
|
46
|
+
|
47
|
+
def get_local_backend_config(
|
48
|
+
wftask: WorkflowTaskV2,
|
49
|
+
which_type: Literal["non_parallel", "parallel"],
|
50
|
+
config_path: Optional[Path] = None,
|
51
|
+
) -> LocalBackendConfig:
|
52
|
+
"""
|
53
|
+
Prepare a `LocalBackendConfig` configuration object
|
54
|
+
|
55
|
+
The sources for `parallel_tasks_per_job` attributes, starting from the
|
56
|
+
highest-priority one, are
|
57
|
+
|
58
|
+
1. Properties in `wftask.meta_parallel` or `wftask.meta_non_parallel`
|
59
|
+
(depending on `which_type`);
|
60
|
+
2. The general content of the local-backend configuration file;
|
61
|
+
3. The default value (`None`).
|
62
|
+
|
63
|
+
Arguments:
|
64
|
+
wftask:
|
65
|
+
WorkflowTaskV2 for which the backend configuration should
|
66
|
+
be prepared.
|
67
|
+
config_path:
|
68
|
+
Path of local-backend configuration file; if `None`, use
|
69
|
+
`FRACTAL_LOCAL_CONFIG_FILE` variable from settings.
|
70
|
+
|
71
|
+
Returns:
|
72
|
+
A local-backend configuration object
|
73
|
+
"""
|
74
|
+
|
75
|
+
key = "parallel_tasks_per_job"
|
76
|
+
default_value = None
|
77
|
+
|
78
|
+
if which_type == "non_parallel":
|
79
|
+
wftask_meta = wftask.meta_non_parallel
|
80
|
+
elif which_type == "parallel":
|
81
|
+
wftask_meta = wftask.meta_parallel
|
82
|
+
else:
|
83
|
+
raise ValueError(
|
84
|
+
"`get_local_backend_config` received an invalid argument"
|
85
|
+
f" {which_type=}."
|
86
|
+
)
|
87
|
+
|
88
|
+
if wftask_meta and key in wftask_meta:
|
89
|
+
parallel_tasks_per_job = wftask_meta[key]
|
90
|
+
else:
|
91
|
+
if not config_path:
|
92
|
+
settings = Inject(get_settings)
|
93
|
+
config_path = settings.FRACTAL_LOCAL_CONFIG_FILE
|
94
|
+
if config_path is None:
|
95
|
+
parallel_tasks_per_job = default_value
|
96
|
+
else:
|
97
|
+
with config_path.open("r") as f:
|
98
|
+
env = json.load(f)
|
99
|
+
try:
|
100
|
+
_ = LocalBackendConfig(**env)
|
101
|
+
except ValidationError as e:
|
102
|
+
raise LocalBackendConfigError(
|
103
|
+
f"Error while loading {config_path=}. "
|
104
|
+
f"Original error:\n{str(e)}"
|
105
|
+
)
|
106
|
+
|
107
|
+
parallel_tasks_per_job = env.get(key, default_value)
|
108
|
+
return LocalBackendConfig(parallel_tasks_per_job=parallel_tasks_per_job)
|