executorlib 1.10.1__tar.gz → 1.10.3__tar.gz
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.
- {executorlib-1.10.1 → executorlib-1.10.3}/PKG-INFO +10 -10
- {executorlib-1.10.1 → executorlib-1.10.3}/pyproject.toml +16 -12
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/__init__.py +29 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/_version.py +2 -2
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/single.py +2 -2
- executorlib-1.10.3/src/executorlib/standalone/command_pysqa.py +148 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/communication.py +9 -9
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/serialize.py +16 -11
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/shared.py +101 -23
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/spawner_pysqa.py +59 -3
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/spawner_subprocess.py +2 -2
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/task_scheduler.py +13 -8
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/blockallocation.py +36 -27
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/shared.py +29 -19
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/spawner_pysqa.py +2 -2
- executorlib-1.10.1/src/executorlib/standalone/scheduler.py +0 -65
- {executorlib-1.10.1 → executorlib-1.10.3}/.gitignore +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/LICENSE +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/README.md +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/api.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/cache_parallel.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/cache_serial.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/interactive_parallel.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/interactive_serial.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/base.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/flux.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/slurm.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/batched.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/command.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/error.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/hdf.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/inputcheck.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/arguments.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/backend.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/spawner.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/queue.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/select.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/validate.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/base.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/backend.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/__init__.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/dependency.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/dependency_plot.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/onetoone.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/spawner_flux.py +0 -0
- {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/spawner_slurm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: executorlib
|
|
3
|
-
Version: 1.10.
|
|
3
|
+
Version: 1.10.3
|
|
4
4
|
Summary: Up-scale python functions for high performance computing (HPC) with executorlib.
|
|
5
5
|
Project-URL: Homepage, https://github.com/pyiron/executorlib
|
|
6
6
|
Project-URL: Documentation, https://executorlib.readthedocs.io
|
|
@@ -49,31 +49,31 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
49
49
|
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
50
50
|
Requires-Python: <3.15,>3.9
|
|
51
51
|
Requires-Dist: cloudpickle<=3.1.2,>=2.0.0
|
|
52
|
-
Requires-Dist: pyzmq<=27.
|
|
52
|
+
Requires-Dist: pyzmq<=27.2.0,>=25.0.0
|
|
53
53
|
Provides-Extra: all
|
|
54
54
|
Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'all'
|
|
55
55
|
Requires-Dist: ipython<=9.9.0,>=7.33.0; extra == 'all'
|
|
56
56
|
Requires-Dist: mpi4py<=4.1.2,>=3.1.4; extra == 'all'
|
|
57
57
|
Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'all'
|
|
58
|
-
Requires-Dist: pydantic<=2.13.
|
|
59
|
-
Requires-Dist: pygraphviz<=1
|
|
60
|
-
Requires-Dist: pysqa==0.
|
|
58
|
+
Requires-Dist: pydantic<=2.13.5,>=2.5.3; extra == 'all'
|
|
59
|
+
Requires-Dist: pygraphviz<=2.0.1,>=1.10; extra == 'all'
|
|
60
|
+
Requires-Dist: pysqa==1.0.0; extra == 'all'
|
|
61
61
|
Provides-Extra: cache
|
|
62
62
|
Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'cache'
|
|
63
63
|
Provides-Extra: cluster
|
|
64
64
|
Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'cluster'
|
|
65
|
-
Requires-Dist: pysqa==0.
|
|
65
|
+
Requires-Dist: pysqa==1.0.0; extra == 'cluster'
|
|
66
66
|
Provides-Extra: graph
|
|
67
67
|
Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'graph'
|
|
68
|
-
Requires-Dist: pygraphviz<=1
|
|
68
|
+
Requires-Dist: pygraphviz<=2.0.1,>=1.10; extra == 'graph'
|
|
69
69
|
Provides-Extra: graphnotebook
|
|
70
70
|
Requires-Dist: ipython<=9.9.0,>=7.33.0; extra == 'graphnotebook'
|
|
71
71
|
Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'graphnotebook'
|
|
72
|
-
Requires-Dist: pygraphviz<=1
|
|
72
|
+
Requires-Dist: pygraphviz<=2.0.1,>=1.10; extra == 'graphnotebook'
|
|
73
73
|
Provides-Extra: mpi
|
|
74
74
|
Requires-Dist: mpi4py<=4.1.2,>=3.1.4; extra == 'mpi'
|
|
75
75
|
Provides-Extra: validation
|
|
76
|
-
Requires-Dist: pydantic<=2.13.
|
|
76
|
+
Requires-Dist: pydantic<=2.13.5,>=2.5.3; extra == 'validation'
|
|
77
77
|
Description-Content-Type: text/markdown
|
|
78
78
|
|
|
79
79
|
# executorlib
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
[build-system]
|
|
2
2
|
requires = [
|
|
3
|
-
"hatchling>=1.27.0,<=1.
|
|
3
|
+
"hatchling>=1.27.0,<=1.32.0",
|
|
4
4
|
"hatch-vcs==0.5.0",
|
|
5
5
|
"cloudpickle>=2.0.0,<=3.1.2",
|
|
6
|
-
"pyzmq>=25.0.0,<=27.
|
|
6
|
+
"pyzmq>=25.0.0,<=27.2.0",
|
|
7
7
|
]
|
|
8
8
|
build-backend = "hatchling.build"
|
|
9
9
|
|
|
@@ -31,7 +31,7 @@ classifiers = [
|
|
|
31
31
|
]
|
|
32
32
|
dependencies = [
|
|
33
33
|
"cloudpickle>=2.0.0,<=3.1.2",
|
|
34
|
-
"pyzmq>=25.0.0,<=27.
|
|
34
|
+
"pyzmq>=25.0.0,<=27.2.0",
|
|
35
35
|
]
|
|
36
36
|
dynamic = ["version"]
|
|
37
37
|
|
|
@@ -43,28 +43,28 @@ Repository = "https://github.com/pyiron/executorlib"
|
|
|
43
43
|
[project.optional-dependencies]
|
|
44
44
|
cache = ["h5py>=3.6.0,<=3.16.0"]
|
|
45
45
|
graph = [
|
|
46
|
-
"pygraphviz>=1.10,<=1
|
|
46
|
+
"pygraphviz>=1.10,<=2.0.1",
|
|
47
47
|
"networkx>=2.8.8,<=3.6.1",
|
|
48
48
|
]
|
|
49
49
|
graphnotebook = [
|
|
50
|
-
"pygraphviz>=1.10,<=1
|
|
50
|
+
"pygraphviz>=1.10,<=2.0.1",
|
|
51
51
|
"networkx>=2.8.8,<=3.6.1",
|
|
52
52
|
"ipython>=7.33.0,<=9.9.0",
|
|
53
53
|
]
|
|
54
54
|
mpi = ["mpi4py>=3.1.4,<=4.1.2"]
|
|
55
55
|
cluster = [
|
|
56
|
-
"pysqa==0.
|
|
56
|
+
"pysqa==1.0.0",
|
|
57
57
|
"h5py>=3.6.0,<=3.16.0",
|
|
58
58
|
]
|
|
59
59
|
validation = [
|
|
60
|
-
"pydantic>=2.5.3,<=2.13.
|
|
60
|
+
"pydantic>=2.5.3,<=2.13.5",
|
|
61
61
|
]
|
|
62
62
|
all = [
|
|
63
63
|
"mpi4py>=3.1.4,<=4.1.2",
|
|
64
|
-
"pysqa==0.
|
|
64
|
+
"pysqa==1.0.0",
|
|
65
65
|
"h5py>=3.6.0,<=3.16.0",
|
|
66
|
-
"pydantic>=2.5.3,<=2.13.
|
|
67
|
-
"pygraphviz>=1.10,<=1
|
|
66
|
+
"pydantic>=2.5.3,<=2.13.5",
|
|
67
|
+
"pygraphviz>=1.10,<=2.0.1",
|
|
68
68
|
"networkx>=2.8.8,<=3.6.1",
|
|
69
69
|
"ipython>=7.33.0,<=9.9.0",
|
|
70
70
|
]
|
|
@@ -98,12 +98,16 @@ ignore = [
|
|
|
98
98
|
"E501",
|
|
99
99
|
# ignore imports which are not at the top of a module
|
|
100
100
|
"PLC0415",
|
|
101
|
+
# Too many branches
|
|
102
|
+
"PLR0912",
|
|
101
103
|
# Too many arguments in function definition
|
|
102
104
|
"PLR0913",
|
|
105
|
+
# Too many statements
|
|
106
|
+
"PLR0915",
|
|
107
|
+
# Too many not-keyword-only arguments,
|
|
108
|
+
"PLR0917",
|
|
103
109
|
# Magic value used in comparison
|
|
104
110
|
"PLR2004",
|
|
105
|
-
# Too many branches
|
|
106
|
-
"PLR0912",
|
|
107
111
|
]
|
|
108
112
|
|
|
109
113
|
[tool.hatch.build]
|
|
@@ -44,6 +44,35 @@ def get_cache_data(cache_directory: str) -> list[dict]:
|
|
|
44
44
|
return get_cache_data(cache_directory=cache_directory)
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
def get_cache_data_queue(
|
|
48
|
+
cache_directory: str,
|
|
49
|
+
queue_type: Optional[str] = None,
|
|
50
|
+
config_directory: Optional[str] = None,
|
|
51
|
+
) -> list[dict]:
|
|
52
|
+
"""
|
|
53
|
+
Collect all HDF5 files in the cache directory and check their status in the queue system
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
cache_directory (str): The directory to store cache files.
|
|
57
|
+
queue_type (str, optional): The type of the queue system ["slurm", "flux"].
|
|
58
|
+
config_directory (str, optional): The directory containing the configuration for the queue system.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
list[dict]: List of dictionaries each representing on of the HDF5 files in the
|
|
62
|
+
cache directory with their status in the queue system.
|
|
63
|
+
"""
|
|
64
|
+
from executorlib.standalone.hdf import get_cache_data
|
|
65
|
+
from executorlib.task_scheduler.file.spawner_pysqa import (
|
|
66
|
+
get_queue_system_cache_data,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return get_queue_system_cache_data(
|
|
70
|
+
cache_dict=get_cache_data(cache_directory=cache_directory),
|
|
71
|
+
queue_type=queue_type,
|
|
72
|
+
config_directory=config_directory,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
47
76
|
def get_future_from_cache(
|
|
48
77
|
cache_directory: str,
|
|
49
78
|
cache_key: str,
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.10.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 10,
|
|
21
|
+
__version__ = version = '1.10.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 10, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -393,7 +393,7 @@ class TestClusterExecutor(BaseExecutor):
|
|
|
393
393
|
)
|
|
394
394
|
if not plot_dependency_graph:
|
|
395
395
|
from executorlib.task_scheduler.file.spawner_subprocess import (
|
|
396
|
-
|
|
396
|
+
subprocess_execute,
|
|
397
397
|
)
|
|
398
398
|
from executorlib.task_scheduler.file.task_scheduler import (
|
|
399
399
|
create_file_executor,
|
|
@@ -415,7 +415,7 @@ class TestClusterExecutor(BaseExecutor):
|
|
|
415
415
|
block_allocation=block_allocation,
|
|
416
416
|
init_function=init_function,
|
|
417
417
|
disable_dependencies=disable_dependencies,
|
|
418
|
-
execute_function=
|
|
418
|
+
execute_function=subprocess_execute,
|
|
419
419
|
wait=wait,
|
|
420
420
|
refresh_rate=refresh_rate,
|
|
421
421
|
validator=validate_resource_dict_with_optional_keys,
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from time import monotonic
|
|
5
|
+
from typing import Optional, Union
|
|
6
|
+
|
|
7
|
+
from pysqa import QueueAdapter
|
|
8
|
+
|
|
9
|
+
# Minimum time between two queries of the queuing system for the status of a task whose output
|
|
10
|
+
# file has not appeared yet. Detecting a dead job (timeout, OOM, node failure, scancel, ...) relies
|
|
11
|
+
# on this status query, but it must not be issued on every poll of the (much faster) refresh_rate
|
|
12
|
+
# loop, as that would flood the queuing system commands (e.g. squeue/sacct) with requests.
|
|
13
|
+
_JOB_STATUS_CHECK_INTERVAL = 30.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pysqa_job_output_validation(
|
|
17
|
+
task_key: str,
|
|
18
|
+
file_name: str,
|
|
19
|
+
queue_id: int,
|
|
20
|
+
status_check_dict: Optional[dict],
|
|
21
|
+
pysqa_config_directory: Optional[str] = None,
|
|
22
|
+
backend: Optional[str] = None,
|
|
23
|
+
job_status_check_interval: float = _JOB_STATUS_CHECK_INTERVAL,
|
|
24
|
+
) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Check whether the queuing system job backing a task has died without ever writing its output
|
|
27
|
+
file. Only applies to queuing system backends (pysqa) and is throttled to at most once every
|
|
28
|
+
``job_status_check_interval`` seconds per task, to avoid flooding the queuing system with
|
|
29
|
+
status queries on every poll of the (much faster) refresh_rate loop.
|
|
30
|
+
|
|
31
|
+
A dead job is recognized in two ways, since queuing systems differ in whether they drop
|
|
32
|
+
terminated jobs from their listing: slurm's squeue removes a job as soon as it is gone
|
|
33
|
+
(status None), while flux's "flux jobs -a" keeps listing inactive jobs and instead reports
|
|
34
|
+
pysqa's terminal-failure status "error" (the same status pysqa_terminate already treats as
|
|
35
|
+
not alive).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
task_key (str): The key of the task.
|
|
39
|
+
file_name (str): Path of the expected output HDF5 file.
|
|
40
|
+
queue_id (int, optional): The queuing system ID of the task.
|
|
41
|
+
pysqa_config_directory (str, optional): path to the pysqa config directory.
|
|
42
|
+
backend (str, optional): name of the backend used to spawn tasks ["slurm", "flux"].
|
|
43
|
+
status_check_dict (dict): Dictionary tracking when each task's job status was last queried.
|
|
44
|
+
job_status_check_interval (float): Minimum time interval between job status checks for the same task.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
bool: True if the job is no longer known to the queuing system, or is reported as having
|
|
48
|
+
errored out, and still has no output.
|
|
49
|
+
"""
|
|
50
|
+
now = monotonic()
|
|
51
|
+
last_checked = (
|
|
52
|
+
status_check_dict.get(task_key, 0.0) if status_check_dict is not None else 0.0
|
|
53
|
+
)
|
|
54
|
+
if now - last_checked < job_status_check_interval:
|
|
55
|
+
return False
|
|
56
|
+
if status_check_dict is not None:
|
|
57
|
+
status_check_dict[task_key] = now
|
|
58
|
+
status = pysqa_get_status_of_job(
|
|
59
|
+
queue_id=queue_id,
|
|
60
|
+
config_directory=pysqa_config_directory,
|
|
61
|
+
backend=backend,
|
|
62
|
+
)
|
|
63
|
+
return (status is None or status == "error") and not os.path.exists(file_name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def pysqa_terminate(
|
|
67
|
+
queue_id: int,
|
|
68
|
+
config_directory: Optional[str] = None,
|
|
69
|
+
backend: Optional[str] = None,
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Delete job from queuing system
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
queue_id (int): Queuing system ID of the job to delete.
|
|
76
|
+
config_directory (str, optional): path to the config directory.
|
|
77
|
+
backend (str, optional): name of the backend used to spawn tasks ["slurm", "flux"].
|
|
78
|
+
"""
|
|
79
|
+
qa = QueueAdapter(
|
|
80
|
+
directory=config_directory,
|
|
81
|
+
queue_type=backend,
|
|
82
|
+
execute_command=pysqa_execute_command,
|
|
83
|
+
)
|
|
84
|
+
status = qa.get_status_of_job(process_id=queue_id)
|
|
85
|
+
if status is not None and status not in ["finished", "error"]:
|
|
86
|
+
with contextlib.suppress(subprocess.CalledProcessError):
|
|
87
|
+
qa.delete_job(process_id=queue_id)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def pysqa_get_status_of_job(
|
|
91
|
+
queue_id: int,
|
|
92
|
+
config_directory: Optional[str] = None,
|
|
93
|
+
backend: Optional[str] = None,
|
|
94
|
+
) -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Query the status of a job from the queuing system.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
queue_id (int): Queuing system ID of the job to check.
|
|
100
|
+
config_directory (str, optional): path to the config directory.
|
|
101
|
+
backend (str, optional): name of the backend used to spawn tasks ["slurm", "flux"].
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
str: status of the job as reported by the queuing system, None if the queuing system no
|
|
105
|
+
longer knows about the job (e.g. it timed out, was killed or already finished).
|
|
106
|
+
"""
|
|
107
|
+
qa = QueueAdapter(
|
|
108
|
+
directory=config_directory,
|
|
109
|
+
queue_type=backend,
|
|
110
|
+
execute_command=pysqa_execute_command,
|
|
111
|
+
)
|
|
112
|
+
return qa.get_status_of_job(process_id=queue_id)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def pysqa_execute_command(
|
|
116
|
+
commands: str,
|
|
117
|
+
working_directory: Optional[str] = None,
|
|
118
|
+
split_output: bool = True,
|
|
119
|
+
shell: bool = False,
|
|
120
|
+
error_filename: str = "pysqa.err",
|
|
121
|
+
) -> Union[str, list[str]]:
|
|
122
|
+
"""
|
|
123
|
+
A wrapper around the subprocess.check_output function. Modified from pysqa to raise an exception if the subprocess
|
|
124
|
+
fails to submit the job to the queue.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
commands (str): The command(s) to be executed on the command line
|
|
128
|
+
working_directory (str, optional): The directory where the command is executed. Defaults to None.
|
|
129
|
+
split_output (bool, optional): Boolean flag to split newlines in the output. Defaults to True.
|
|
130
|
+
shell (bool, optional): Additional switch to convert commands to a single string. Defaults to False.
|
|
131
|
+
error_filename (str, optional): In case the execution fails, the output is written to this file. Defaults to "pysqa.err".
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Union[str, List[str]]: Output of the shell command either as a string or as a list of strings
|
|
135
|
+
"""
|
|
136
|
+
if shell and isinstance(commands, list):
|
|
137
|
+
commands = " ".join(commands)
|
|
138
|
+
out = subprocess.check_output(
|
|
139
|
+
commands,
|
|
140
|
+
cwd=working_directory,
|
|
141
|
+
stderr=subprocess.STDOUT,
|
|
142
|
+
universal_newlines=True,
|
|
143
|
+
shell=not isinstance(commands, list),
|
|
144
|
+
)
|
|
145
|
+
if out is not None and split_output:
|
|
146
|
+
return out.split("\n")
|
|
147
|
+
else:
|
|
148
|
+
return out
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/communication.py
RENAMED
|
@@ -78,19 +78,17 @@ class SocketInterface:
|
|
|
78
78
|
while len(response_lst) == 0:
|
|
79
79
|
response_lst = self._poller.poll(self._time_out_ms)
|
|
80
80
|
if not self._spawner.poll():
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
81
|
+
return {
|
|
82
|
+
"error": ExecutorlibSocketError(
|
|
83
|
+
"SocketInterface crashed during execution."
|
|
84
|
+
),
|
|
85
|
+
}
|
|
84
86
|
data = self._socket.recv(zmq.NOBLOCK)
|
|
85
87
|
if self._logger is not None:
|
|
86
88
|
self._logger.warning(
|
|
87
89
|
"Received dictionary of size: " + str(sys.getsizeof(data))
|
|
88
90
|
)
|
|
89
|
-
|
|
90
|
-
if "result" in output:
|
|
91
|
-
return output["result"]
|
|
92
|
-
else:
|
|
93
|
-
raise output["error"]
|
|
91
|
+
return cloudpickle.loads(data)
|
|
94
92
|
|
|
95
93
|
def send_and_receive_dict(self, input_dict: dict) -> dict:
|
|
96
94
|
"""
|
|
@@ -152,9 +150,11 @@ class SocketInterface:
|
|
|
152
150
|
"""
|
|
153
151
|
result = None
|
|
154
152
|
if self._spawner.poll():
|
|
155
|
-
|
|
153
|
+
output = self.send_and_receive_dict(
|
|
156
154
|
input_dict={"shutdown": True, "wait": wait}
|
|
157
155
|
)
|
|
156
|
+
if "result" in output:
|
|
157
|
+
result = output["result"]
|
|
158
158
|
self._spawner.shutdown(wait=wait)
|
|
159
159
|
self._reset_socket()
|
|
160
160
|
return result
|
|
@@ -34,7 +34,7 @@ def serialize_funct(
|
|
|
34
34
|
fn_kwargs: Optional[dict] = None,
|
|
35
35
|
resource_dict: Optional[dict] = None,
|
|
36
36
|
cache_key: Optional[str] = None,
|
|
37
|
-
) -> tuple[str, dict]:
|
|
37
|
+
) -> tuple[str, dict, Optional[Exception]]:
|
|
38
38
|
"""
|
|
39
39
|
Serialize a function and its arguments and keyword arguments into an HDF5 file.
|
|
40
40
|
|
|
@@ -56,7 +56,8 @@ def serialize_funct(
|
|
|
56
56
|
overwritten by setting the cache_key.
|
|
57
57
|
|
|
58
58
|
Returns:
|
|
59
|
-
Tuple[str, dict]: A tuple containing the task key
|
|
59
|
+
Tuple[str, dict, Optional[Exception]]: A tuple containing the task key, the serialized data and an
|
|
60
|
+
exception if the serialization failed.
|
|
60
61
|
|
|
61
62
|
"""
|
|
62
63
|
if fn_args is None:
|
|
@@ -68,21 +69,25 @@ def serialize_funct(
|
|
|
68
69
|
if cache_key is not None:
|
|
69
70
|
task_key = cache_key
|
|
70
71
|
else:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
try:
|
|
73
|
+
binary_all = cloudpickle.dumps(
|
|
74
|
+
{
|
|
75
|
+
"fn": fn,
|
|
76
|
+
"args": fn_args,
|
|
77
|
+
"kwargs": fn_kwargs,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
return "", {}, e
|
|
82
|
+
else:
|
|
83
|
+
task_key = _get_function_name(fn=fn) + _get_hash(binary=binary_all)
|
|
79
84
|
data = {
|
|
80
85
|
"fn": fn,
|
|
81
86
|
"args": fn_args,
|
|
82
87
|
"kwargs": fn_kwargs,
|
|
83
88
|
"resource_dict": resource_dict,
|
|
84
89
|
}
|
|
85
|
-
return task_key, data
|
|
90
|
+
return task_key, data, None
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
def _get_hash(binary: bytes) -> str:
|
|
@@ -8,7 +8,7 @@ from typing import Any, Callable, Optional
|
|
|
8
8
|
from executorlib.standalone.command import get_cache_execute_command
|
|
9
9
|
from executorlib.standalone.hdf import get_cache_files, get_output, get_queue_id
|
|
10
10
|
from executorlib.standalone.serialize import serialize_funct
|
|
11
|
-
from executorlib.task_scheduler.file.spawner_subprocess import
|
|
11
|
+
from executorlib.task_scheduler.file.spawner_subprocess import subprocess_terminate
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class FutureItem:
|
|
@@ -59,6 +59,7 @@ def execute_tasks_h5(
|
|
|
59
59
|
execute_function: Callable,
|
|
60
60
|
executor_kwargs: dict,
|
|
61
61
|
terminate_function: Optional[Callable] = None,
|
|
62
|
+
validate_function: Optional[Callable] = None,
|
|
62
63
|
pysqa_config_directory: Optional[str] = None,
|
|
63
64
|
backend: Optional[str] = None,
|
|
64
65
|
disable_dependencies: bool = False,
|
|
@@ -76,6 +77,7 @@ def execute_tasks_h5(
|
|
|
76
77
|
- cwd (str/None): current working directory where the parallel python task is executed
|
|
77
78
|
execute_function (Callable): The function to execute the tasks.
|
|
78
79
|
terminate_function (Callable): The function to terminate the tasks.
|
|
80
|
+
validate_function (Callable): The function to validate the tasks.
|
|
79
81
|
pysqa_config_directory (str, optional): path to the pysqa config directory (only for pysqa based backend).
|
|
80
82
|
backend (str, optional): name of the backend used to spawn tasks.
|
|
81
83
|
disable_dependencies (boolean): Disable resolving future objects during the submission.
|
|
@@ -92,6 +94,7 @@ def execute_tasks_h5(
|
|
|
92
94
|
cache_dir_dict: dict = {}
|
|
93
95
|
file_name_dict: dict = {}
|
|
94
96
|
duplicate_dict: dict = {}
|
|
97
|
+
status_check_dict: dict = {}
|
|
95
98
|
while True:
|
|
96
99
|
task_dict = None
|
|
97
100
|
with contextlib.suppress(queue.Empty):
|
|
@@ -104,7 +107,9 @@ def execute_tasks_h5(
|
|
|
104
107
|
process_dict=process_dict,
|
|
105
108
|
duplicate_dict=duplicate_dict,
|
|
106
109
|
cache_dir_dict=cache_dir_dict,
|
|
110
|
+
status_check_dict=status_check_dict,
|
|
107
111
|
terminate_function=terminate_function,
|
|
112
|
+
validate_function=validate_function,
|
|
108
113
|
pysqa_config_directory=pysqa_config_directory,
|
|
109
114
|
backend=backend,
|
|
110
115
|
refresh_rate=refresh_rate,
|
|
@@ -124,7 +129,7 @@ def execute_tasks_h5(
|
|
|
124
129
|
executor_kwargs=executor_kwargs,
|
|
125
130
|
)
|
|
126
131
|
)
|
|
127
|
-
task_key, data_dict = serialize_funct(
|
|
132
|
+
task_key, data_dict, serialize_exception = serialize_funct(
|
|
128
133
|
fn=task_dict["fn"],
|
|
129
134
|
fn_args=task_args,
|
|
130
135
|
fn_kwargs=task_kwargs,
|
|
@@ -132,7 +137,9 @@ def execute_tasks_h5(
|
|
|
132
137
|
cache_key=cache_key,
|
|
133
138
|
)
|
|
134
139
|
data_dict["error_log_file"] = error_log_file
|
|
135
|
-
if
|
|
140
|
+
if serialize_exception is not None:
|
|
141
|
+
task_dict["future"].set_exception(serialize_exception)
|
|
142
|
+
elif task_key not in memory_dict:
|
|
136
143
|
if os.path.join(
|
|
137
144
|
cache_directory, task_key + "_o.h5"
|
|
138
145
|
) not in get_cache_files(cache_directory=cache_directory):
|
|
@@ -151,25 +158,31 @@ def execute_tasks_h5(
|
|
|
151
158
|
)
|
|
152
159
|
)
|
|
153
160
|
task_dependent_lst = []
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
try:
|
|
162
|
+
process_dict[task_key] = execute_function(
|
|
163
|
+
command=get_cache_execute_command(
|
|
164
|
+
file_name=file_name,
|
|
165
|
+
cores=task_resource_dict["cores"],
|
|
166
|
+
backend=backend,
|
|
167
|
+
exclusive=task_resource_dict.get("exclusive", False),
|
|
168
|
+
openmpi_oversubscribe=task_resource_dict.get(
|
|
169
|
+
"openmpi_oversubscribe", False
|
|
170
|
+
),
|
|
171
|
+
pmi_mode=pmi_mode,
|
|
172
|
+
),
|
|
156
173
|
file_name=file_name,
|
|
157
|
-
|
|
174
|
+
data_dict=data_dict,
|
|
175
|
+
task_dependent_lst=task_dependent_lst,
|
|
176
|
+
resource_dict=task_resource_dict,
|
|
177
|
+
config_directory=pysqa_config_directory,
|
|
158
178
|
backend=backend,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
data_dict=data_dict,
|
|
167
|
-
task_dependent_lst=task_dependent_lst,
|
|
168
|
-
resource_dict=task_resource_dict,
|
|
169
|
-
config_directory=pysqa_config_directory,
|
|
170
|
-
backend=backend,
|
|
171
|
-
cache_directory=cache_directory,
|
|
172
|
-
)
|
|
179
|
+
cache_directory=cache_directory,
|
|
180
|
+
)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
task_dict["future"].set_exception(e)
|
|
183
|
+
memory_dict[task_key] = task_dict["future"]
|
|
184
|
+
future_queue.task_done()
|
|
185
|
+
continue
|
|
173
186
|
file_name = os.path.join(cache_directory, task_key + "_o.h5")
|
|
174
187
|
file_name_dict[task_key] = file_name
|
|
175
188
|
queue_id = get_queue_id(file_name=file_name)
|
|
@@ -188,7 +201,9 @@ def execute_tasks_h5(
|
|
|
188
201
|
cache_dir_dict=cache_dir_dict,
|
|
189
202
|
process_dict=process_dict,
|
|
190
203
|
duplicate_dict=duplicate_dict,
|
|
204
|
+
status_check_dict=status_check_dict,
|
|
191
205
|
terminate_function=terminate_function,
|
|
206
|
+
validate_function=validate_function,
|
|
192
207
|
pysqa_config_directory=pysqa_config_directory,
|
|
193
208
|
backend=backend,
|
|
194
209
|
refresh_rate=refresh_rate,
|
|
@@ -199,15 +214,30 @@ def _check_task_output(
|
|
|
199
214
|
task_key: str,
|
|
200
215
|
future_obj: Future,
|
|
201
216
|
cache_directory: str,
|
|
217
|
+
queue_id: Optional[int] = None,
|
|
218
|
+
pysqa_config_directory: Optional[str] = None,
|
|
219
|
+
backend: Optional[str] = None,
|
|
220
|
+
validate_function: Optional[Callable] = None,
|
|
221
|
+
status_check_dict: Optional[dict] = None,
|
|
202
222
|
duplicate_dict: Optional[dict] = None,
|
|
203
223
|
) -> Future:
|
|
204
224
|
"""
|
|
205
225
|
Check the output of a task and set the result of the future object if available.
|
|
206
226
|
|
|
227
|
+
If the output file is missing and the task is running on a queuing system backend, this also
|
|
228
|
+
detects jobs which died without producing output (e.g. walltime TIMEOUT, OOM, NODE_FAIL or an
|
|
229
|
+
external scancel) by periodically querying the job status via pysqa and fails the future
|
|
230
|
+
instead of leaving it pending forever.
|
|
231
|
+
|
|
207
232
|
Args:
|
|
208
233
|
task_key (str): The key of the task.
|
|
209
234
|
future_obj (Future): The future object associated with the task.
|
|
210
235
|
cache_directory (str): The directory where the HDF5 files are stored.
|
|
236
|
+
queue_id (int, optional): The queuing system ID of the task, if submitted via pysqa.
|
|
237
|
+
pysqa_config_directory (str, optional): path to the pysqa config directory.
|
|
238
|
+
backend (str, optional): name of the backend used to spawn tasks ["slurm", "flux"].
|
|
239
|
+
status_check_dict (dict): Dictionary tracking when each task's job status was last queried,
|
|
240
|
+
used to throttle calls to the queuing system.
|
|
211
241
|
duplicate_dict (dict): The dictionary mapping task keys to their associated duplicate future objects.
|
|
212
242
|
Returns:
|
|
213
243
|
Future: The updated future object.
|
|
@@ -215,8 +245,32 @@ def _check_task_output(
|
|
|
215
245
|
"""
|
|
216
246
|
file_name = os.path.join(cache_directory, task_key + "_o.h5")
|
|
217
247
|
if not os.path.exists(file_name):
|
|
218
|
-
|
|
219
|
-
|
|
248
|
+
if (
|
|
249
|
+
backend is None
|
|
250
|
+
or queue_id is None
|
|
251
|
+
or validate_function is None
|
|
252
|
+
or not validate_function(
|
|
253
|
+
task_key=task_key,
|
|
254
|
+
file_name=file_name,
|
|
255
|
+
queue_id=queue_id,
|
|
256
|
+
pysqa_config_directory=pysqa_config_directory,
|
|
257
|
+
backend=backend,
|
|
258
|
+
status_check_dict=status_check_dict,
|
|
259
|
+
)
|
|
260
|
+
):
|
|
261
|
+
return future_obj
|
|
262
|
+
exec_flag, no_error_flag, result = (
|
|
263
|
+
True,
|
|
264
|
+
False,
|
|
265
|
+
RuntimeError(
|
|
266
|
+
f"executorlib: queue job {queue_id} for task {task_key} terminated without "
|
|
267
|
+
"producing output (timeout, out-of-memory, node failure or cancellation)."
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
else:
|
|
271
|
+
exec_flag, no_error_flag, result = get_output(file_name=file_name)
|
|
272
|
+
if status_check_dict is not None:
|
|
273
|
+
status_check_dict.pop(task_key, None)
|
|
220
274
|
_update_future(
|
|
221
275
|
future_obj=future_obj,
|
|
222
276
|
exec_flag=exec_flag,
|
|
@@ -321,7 +375,9 @@ def _refresh_memory_dict(
|
|
|
321
375
|
cache_dir_dict: dict,
|
|
322
376
|
process_dict: dict,
|
|
323
377
|
duplicate_dict: Optional[dict] = None,
|
|
378
|
+
status_check_dict: Optional[dict] = None,
|
|
324
379
|
terminate_function: Optional[Callable] = None,
|
|
380
|
+
validate_function: Optional[Callable] = None,
|
|
325
381
|
pysqa_config_directory: Optional[str] = None,
|
|
326
382
|
backend: Optional[str] = None,
|
|
327
383
|
refresh_rate: float = 0.01,
|
|
@@ -334,7 +390,10 @@ def _refresh_memory_dict(
|
|
|
334
390
|
cache_dir_dict (dict): dictionary with task keys and cache directories
|
|
335
391
|
process_dict (dict): dictionary with task keys and process reference.
|
|
336
392
|
duplicate_dict (dict): dictionary with task keys and duplicate future objects.
|
|
393
|
+
status_check_dict (dict): dictionary with task keys and the last time their queuing system
|
|
394
|
+
job status was queried, used to throttle detection of jobs that died without output.
|
|
337
395
|
terminate_function (callable): The function to terminate the tasks.
|
|
396
|
+
validate_function (callable): The function to validate the tasks.
|
|
338
397
|
pysqa_config_directory (str): path to the pysqa config directory (only for pysqa based backend).
|
|
339
398
|
backend (str): name of the backend used to spawn tasks.
|
|
340
399
|
refresh_rate (float): The rate at which to refresh the result. Defaults to 0.01.
|
|
@@ -351,11 +410,19 @@ def _refresh_memory_dict(
|
|
|
351
410
|
pysqa_config_directory=pysqa_config_directory,
|
|
352
411
|
backend=backend,
|
|
353
412
|
)
|
|
413
|
+
if status_check_dict is not None:
|
|
414
|
+
for key in cancelled_lst:
|
|
415
|
+
status_check_dict.pop(key, None)
|
|
354
416
|
memory_updated_dict = {
|
|
355
417
|
key: _check_task_output(
|
|
356
418
|
task_key=key,
|
|
357
419
|
future_obj=value,
|
|
358
420
|
cache_directory=cache_dir_dict[key],
|
|
421
|
+
queue_id=process_dict.get(key),
|
|
422
|
+
pysqa_config_directory=pysqa_config_directory,
|
|
423
|
+
backend=backend,
|
|
424
|
+
validate_function=validate_function,
|
|
425
|
+
status_check_dict=status_check_dict,
|
|
359
426
|
duplicate_dict=duplicate_dict,
|
|
360
427
|
)
|
|
361
428
|
for key, value in memory_dict.items()
|
|
@@ -381,7 +448,7 @@ def _cancel_processes(
|
|
|
381
448
|
pysqa_config_directory (str): path to the pysqa config directory (only for pysqa based backend).
|
|
382
449
|
backend (str): name of the backend used to spawn tasks.
|
|
383
450
|
"""
|
|
384
|
-
if terminate_function is not None and terminate_function ==
|
|
451
|
+
if terminate_function is not None and terminate_function == subprocess_terminate:
|
|
385
452
|
for task in process_dict.values():
|
|
386
453
|
terminate_function(task=task)
|
|
387
454
|
elif terminate_function is not None and backend is not None:
|
|
@@ -443,7 +510,9 @@ def _shutdown_executor(
|
|
|
443
510
|
process_dict: dict,
|
|
444
511
|
cache_dir_dict: dict,
|
|
445
512
|
duplicate_dict: Optional[dict] = None,
|
|
513
|
+
status_check_dict: Optional[dict] = None,
|
|
446
514
|
terminate_function: Optional[Callable] = None,
|
|
515
|
+
validate_function: Optional[Callable] = None,
|
|
447
516
|
pysqa_config_directory: Optional[str] = None,
|
|
448
517
|
backend: Optional[str] = None,
|
|
449
518
|
refresh_rate: float = 0.01,
|
|
@@ -466,6 +535,9 @@ def _shutdown_executor(
|
|
|
466
535
|
process_dict (dict): Mapping of task keys to process handles or queue IDs.
|
|
467
536
|
duplicate_dict (dict): Mapping of task keys to lists of duplicate Future objects.
|
|
468
537
|
cache_dir_dict (dict): Mapping of task keys to the cache directory for each task.
|
|
538
|
+
status_check_dict (dict): Mapping of task keys to the last time their queuing system job
|
|
539
|
+
status was queried, used to throttle detection of jobs that died without output.
|
|
540
|
+
validate_function (Callable, optional): Function used to validate the tasks.
|
|
469
541
|
terminate_function (Callable, optional): Function used to terminate running processes.
|
|
470
542
|
pysqa_config_directory (str, optional): Path to the pysqa config directory.
|
|
471
543
|
backend (str, optional): Name of the backend ("slurm", "flux", or None for subprocess).
|
|
@@ -478,7 +550,9 @@ def _shutdown_executor(
|
|
|
478
550
|
cache_dir_dict=cache_dir_dict,
|
|
479
551
|
process_dict=process_dict,
|
|
480
552
|
duplicate_dict=duplicate_dict,
|
|
553
|
+
status_check_dict=status_check_dict,
|
|
481
554
|
terminate_function=terminate_function,
|
|
555
|
+
validate_function=validate_function,
|
|
482
556
|
pysqa_config_directory=pysqa_config_directory,
|
|
483
557
|
backend=backend,
|
|
484
558
|
refresh_rate=refresh_rate,
|
|
@@ -493,7 +567,9 @@ def _shutdown_executor(
|
|
|
493
567
|
cache_dir_dict=cache_dir_dict,
|
|
494
568
|
process_dict=process_dict,
|
|
495
569
|
duplicate_dict=duplicate_dict,
|
|
570
|
+
status_check_dict=status_check_dict,
|
|
496
571
|
terminate_function=terminate_function,
|
|
572
|
+
validate_function=validate_function,
|
|
497
573
|
pysqa_config_directory=pysqa_config_directory,
|
|
498
574
|
backend=backend,
|
|
499
575
|
refresh_rate=refresh_rate,
|
|
@@ -512,7 +588,9 @@ def _shutdown_executor(
|
|
|
512
588
|
cache_dir_dict=cache_dir_dict,
|
|
513
589
|
process_dict=process_dict,
|
|
514
590
|
duplicate_dict=duplicate_dict,
|
|
591
|
+
status_check_dict=status_check_dict,
|
|
515
592
|
terminate_function=terminate_function,
|
|
593
|
+
validate_function=validate_function,
|
|
516
594
|
pysqa_config_directory=pysqa_config_directory,
|
|
517
595
|
backend=backend,
|
|
518
596
|
refresh_rate=refresh_rate,
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/spawner_pysqa.py
RENAMED
|
@@ -2,14 +2,15 @@ import os
|
|
|
2
2
|
from subprocess import CalledProcessError
|
|
3
3
|
from typing import Optional
|
|
4
4
|
|
|
5
|
+
from pandas import DataFrame
|
|
5
6
|
from pysqa import QueueAdapter
|
|
6
7
|
|
|
8
|
+
from executorlib.standalone.command_pysqa import pysqa_execute_command, pysqa_terminate
|
|
7
9
|
from executorlib.standalone.hdf import dump, get_queue_id
|
|
8
10
|
from executorlib.standalone.inputcheck import check_file_exists
|
|
9
11
|
from executorlib.standalone.interactive.spawner import (
|
|
10
12
|
set_current_directory_in_environment,
|
|
11
13
|
)
|
|
12
|
-
from executorlib.standalone.scheduler import pysqa_execute_command, terminate_with_pysqa
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def execute_with_pysqa(
|
|
@@ -122,7 +123,7 @@ def terminate_tasks_in_cache(
|
|
|
122
123
|
for f in hdf5_file_lst:
|
|
123
124
|
queue_id = get_queue_id(f)
|
|
124
125
|
if queue_id is not None:
|
|
125
|
-
|
|
126
|
+
pysqa_terminate(
|
|
126
127
|
queue_id=queue_id,
|
|
127
128
|
config_directory=pysqa_config_directory,
|
|
128
129
|
backend=backend,
|
|
@@ -148,9 +149,64 @@ def terminate_task_in_cache(
|
|
|
148
149
|
file_name = os.path.join(cache_directory, cache_key + "_i.h5")
|
|
149
150
|
queue_id = get_queue_id(file_name=file_name)
|
|
150
151
|
if queue_id is not None:
|
|
151
|
-
|
|
152
|
+
pysqa_terminate(
|
|
152
153
|
queue_id=queue_id,
|
|
153
154
|
config_directory=pysqa_config_directory,
|
|
154
155
|
backend=backend,
|
|
155
156
|
)
|
|
156
157
|
os.remove(file_name)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_queue_system_cache_data(
|
|
161
|
+
cache_dict: list[dict],
|
|
162
|
+
queue_type: Optional[str] = None,
|
|
163
|
+
config_directory: Optional[str] = None,
|
|
164
|
+
) -> list[dict]:
|
|
165
|
+
"""
|
|
166
|
+
Get the status of the queue system.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
cache_dict (list[dict]): List of dictionaries each representing on of the HDF5 files in the cache directory.
|
|
170
|
+
queue_type (str, optional): The type of the queue system ["slurm", "flux"].
|
|
171
|
+
config_directory (str, optional): The directory containing the configuration for the queue system.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
cache_dict (list[dict]): List of dictionaries each representing on of the HDF5 files in the cache directory.
|
|
175
|
+
"""
|
|
176
|
+
return _merge_cache_with_queue_status(
|
|
177
|
+
cache_dict=cache_dict,
|
|
178
|
+
dataframe=QueueAdapter(
|
|
179
|
+
directory=config_directory,
|
|
180
|
+
queue_type=queue_type,
|
|
181
|
+
).get_queue_status(),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _merge_cache_with_queue_status(
|
|
186
|
+
cache_dict: list[dict], dataframe: DataFrame
|
|
187
|
+
) -> list[dict]:
|
|
188
|
+
"""
|
|
189
|
+
Merge the cache data with the queue status.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
cache_dict (list[dict]): List of dictionaries each representing on of the HDF5 files in the cache directory.
|
|
193
|
+
dataframe (DataFrame): DataFrame containing the status of the queue system.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
cache_dict (list[dict]): List of dictionaries each representing on of the HDF5 files in the cache directory.
|
|
197
|
+
"""
|
|
198
|
+
cache_updated_dict = []
|
|
199
|
+
for row in cache_dict:
|
|
200
|
+
if "output" in row and row["output"]:
|
|
201
|
+
row["status"] = "finished"
|
|
202
|
+
elif "queue_id" in row and row["queue_id"] in dataframe["jobid"].values:
|
|
203
|
+
row["status"] = dataframe[dataframe["jobid"] == row["queue_id"]][
|
|
204
|
+
"status"
|
|
205
|
+
].values[-1]
|
|
206
|
+
elif "queue_id" not in row:
|
|
207
|
+
row["status"] = "running"
|
|
208
|
+
else:
|
|
209
|
+
row["status"] = "aborted"
|
|
210
|
+
cache_updated_dict.append(row)
|
|
211
|
+
|
|
212
|
+
return cache_updated_dict
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/spawner_subprocess.py
RENAMED
|
@@ -10,7 +10,7 @@ from executorlib.standalone.interactive.spawner import (
|
|
|
10
10
|
)
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
def
|
|
13
|
+
def subprocess_execute(
|
|
14
14
|
command: list,
|
|
15
15
|
file_name: str,
|
|
16
16
|
data_dict: dict,
|
|
@@ -65,7 +65,7 @@ def execute_in_subprocess(
|
|
|
65
65
|
return subprocess.Popen(command, universal_newlines=True, cwd=cwd)
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def
|
|
68
|
+
def subprocess_terminate(task):
|
|
69
69
|
"""
|
|
70
70
|
Terminate a subprocess and wait for it to complete.
|
|
71
71
|
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/task_scheduler.py
RENAMED
|
@@ -12,17 +12,21 @@ from executorlib.standalone.inputcheck import (
|
|
|
12
12
|
from executorlib.task_scheduler.base import TaskSchedulerBase, validate_resource_dict
|
|
13
13
|
from executorlib.task_scheduler.file.shared import execute_tasks_h5
|
|
14
14
|
from executorlib.task_scheduler.file.spawner_subprocess import (
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
subprocess_execute,
|
|
16
|
+
subprocess_terminate,
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
try:
|
|
20
|
-
from executorlib.standalone.
|
|
20
|
+
from executorlib.standalone.command_pysqa import (
|
|
21
|
+
pysqa_job_output_validation,
|
|
22
|
+
pysqa_terminate,
|
|
23
|
+
)
|
|
21
24
|
from executorlib.task_scheduler.file.spawner_pysqa import execute_with_pysqa
|
|
22
25
|
except ImportError:
|
|
23
26
|
# If pysqa is not available fall back to executing tasks in a subprocess
|
|
24
|
-
execute_with_pysqa =
|
|
25
|
-
|
|
27
|
+
execute_with_pysqa = subprocess_execute # type: ignore
|
|
28
|
+
pysqa_terminate = None # type: ignore
|
|
29
|
+
pysqa_job_output_validation = None # type: ignore
|
|
26
30
|
|
|
27
31
|
|
|
28
32
|
class FileTaskScheduler(TaskSchedulerBase):
|
|
@@ -74,6 +78,7 @@ class FileTaskScheduler(TaskSchedulerBase):
|
|
|
74
78
|
"future_queue": self._future_queue,
|
|
75
79
|
"execute_function": execute_function,
|
|
76
80
|
"terminate_function": terminate_function,
|
|
81
|
+
"validate_function": pysqa_job_output_validation,
|
|
77
82
|
"pysqa_config_directory": pysqa_config_directory,
|
|
78
83
|
"backend": backend,
|
|
79
84
|
"disable_dependencies": disable_dependencies,
|
|
@@ -126,10 +131,10 @@ def create_file_executor(
|
|
|
126
131
|
check_executor(executor=flux_executor)
|
|
127
132
|
check_nested_flux_executor(nested_flux_executor=flux_executor_nesting)
|
|
128
133
|
check_flux_log_files(flux_log_files=flux_log_files)
|
|
129
|
-
if execute_function !=
|
|
130
|
-
terminate_function =
|
|
134
|
+
if execute_function != subprocess_execute:
|
|
135
|
+
terminate_function = pysqa_terminate # type: ignore
|
|
131
136
|
else:
|
|
132
|
-
terminate_function =
|
|
137
|
+
terminate_function = subprocess_terminate # type: ignore
|
|
133
138
|
return FileTaskScheduler(
|
|
134
139
|
executor_kwargs=executor_kwargs,
|
|
135
140
|
pysqa_config_directory=pysqa_config_directory,
|
|
@@ -80,35 +80,37 @@ class BlockAllocationTaskScheduler(TaskSchedulerBase):
|
|
|
80
80
|
executor_kwargs["restart_limit"] = restart_limit
|
|
81
81
|
self._process_kwargs = executor_kwargs
|
|
82
82
|
self._max_workers = max_workers
|
|
83
|
-
|
|
84
|
-
self._self_id = self_id
|
|
83
|
+
self._self_id = random.getrandbits(128)
|
|
85
84
|
_interrupt_bootup_dict[self._self_id] = False
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
self._alive_workers = [max_workers]
|
|
86
|
+
self._alive_workers_lock = Lock()
|
|
87
|
+
self._bootup_events = [Event() for _ in range(self._max_workers)]
|
|
88
|
+
self._bootup_events[0].set()
|
|
90
89
|
self._set_process(
|
|
91
90
|
process=[
|
|
92
91
|
Thread(
|
|
93
92
|
target=_execute_multiple_tasks,
|
|
94
|
-
kwargs=
|
|
95
|
-
| {
|
|
96
|
-
"worker_id": worker_id,
|
|
97
|
-
"stop_function": lambda: _interrupt_bootup_dict[self_id],
|
|
98
|
-
"bootup_event": bootup_events[worker_id],
|
|
99
|
-
"next_bootup_event": (
|
|
100
|
-
bootup_events[worker_id + 1]
|
|
101
|
-
if worker_id + 1 < self._max_workers
|
|
102
|
-
else None
|
|
103
|
-
),
|
|
104
|
-
"alive_workers": alive_workers,
|
|
105
|
-
"alive_workers_lock": alive_workers_lock,
|
|
106
|
-
},
|
|
93
|
+
kwargs=self._worker_kwargs(worker_id),
|
|
107
94
|
)
|
|
108
95
|
for worker_id in range(self._max_workers)
|
|
109
96
|
],
|
|
110
97
|
)
|
|
111
98
|
|
|
99
|
+
def _worker_kwargs(self, worker_id: int) -> dict:
|
|
100
|
+
self_id = self._self_id
|
|
101
|
+
return self._process_kwargs | {
|
|
102
|
+
"worker_id": worker_id,
|
|
103
|
+
"stop_function": lambda: _interrupt_bootup_dict[self_id],
|
|
104
|
+
"bootup_event": self._bootup_events[worker_id],
|
|
105
|
+
"next_bootup_event": (
|
|
106
|
+
self._bootup_events[worker_id + 1]
|
|
107
|
+
if worker_id + 1 < len(self._bootup_events)
|
|
108
|
+
else None
|
|
109
|
+
),
|
|
110
|
+
"alive_workers": self._alive_workers,
|
|
111
|
+
"alive_workers_lock": self._alive_workers_lock,
|
|
112
|
+
}
|
|
113
|
+
|
|
112
114
|
@property
|
|
113
115
|
def max_workers(self) -> int:
|
|
114
116
|
return self._max_workers
|
|
@@ -126,12 +128,20 @@ class BlockAllocationTaskScheduler(TaskSchedulerBase):
|
|
|
126
128
|
process for process in self._process if process.is_alive()
|
|
127
129
|
]
|
|
128
130
|
elif self._max_workers < max_workers:
|
|
131
|
+
old_max_workers = self._max_workers
|
|
132
|
+
self._bootup_events.extend(
|
|
133
|
+
Event() for _ in range(max_workers - old_max_workers)
|
|
134
|
+
)
|
|
135
|
+
for idx in range(old_max_workers, max_workers):
|
|
136
|
+
self._bootup_events[idx].set()
|
|
137
|
+
with self._alive_workers_lock:
|
|
138
|
+
self._alive_workers[0] += max_workers - old_max_workers
|
|
129
139
|
new_process_lst = [
|
|
130
140
|
Thread(
|
|
131
141
|
target=_execute_multiple_tasks,
|
|
132
|
-
kwargs=self.
|
|
142
|
+
kwargs=self._worker_kwargs(worker_id),
|
|
133
143
|
)
|
|
134
|
-
for
|
|
144
|
+
for worker_id in range(old_max_workers, max_workers)
|
|
135
145
|
]
|
|
136
146
|
for process_instance in new_process_lst:
|
|
137
147
|
process_instance.start()
|
|
@@ -379,10 +389,9 @@ def _set_init_function(
|
|
|
379
389
|
) -> Optional[Exception]:
|
|
380
390
|
interface_initialization_exception = None
|
|
381
391
|
if init_function is not None and interface.status:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
interface_initialization_exception = init_exception
|
|
392
|
+
output = interface.send_and_receive_dict(
|
|
393
|
+
input_dict={"init": True, "fn": init_function, "args": (), "kwargs": {}}
|
|
394
|
+
)
|
|
395
|
+
if "error" in output:
|
|
396
|
+
interface_initialization_exception = output["error"]
|
|
388
397
|
return interface_initialization_exception
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/shared.py
RENAMED
|
@@ -102,12 +102,16 @@ def _execute_task_without_cache(
|
|
|
102
102
|
bool: True if the task was submitted successfully, False otherwise.
|
|
103
103
|
"""
|
|
104
104
|
try:
|
|
105
|
-
|
|
106
|
-
except Exception as
|
|
107
|
-
|
|
105
|
+
output = interface.send_and_receive_dict(input_dict=task_dict)
|
|
106
|
+
except Exception as e:
|
|
107
|
+
future_obj.set_exception(exception=e)
|
|
108
|
+
else:
|
|
109
|
+
if "result" in output:
|
|
110
|
+
future_obj.set_result(output["result"])
|
|
111
|
+
elif isinstance(output["error"], ExecutorlibSocketError):
|
|
108
112
|
return False
|
|
109
113
|
else:
|
|
110
|
-
future_obj.set_exception(exception=
|
|
114
|
+
future_obj.set_exception(exception=output["error"])
|
|
111
115
|
return True
|
|
112
116
|
|
|
113
117
|
|
|
@@ -132,28 +136,34 @@ def _execute_task_with_cache(
|
|
|
132
136
|
"""
|
|
133
137
|
from executorlib.standalone.hdf import dump, get_cache_files, get_output
|
|
134
138
|
|
|
135
|
-
task_key, data_dict = serialize_funct(
|
|
139
|
+
task_key, data_dict, serialize_exception = serialize_funct(
|
|
136
140
|
fn=task_dict["fn"],
|
|
137
141
|
fn_args=task_dict["args"],
|
|
138
142
|
fn_kwargs=task_dict["kwargs"],
|
|
139
143
|
resource_dict=task_dict.get("resource_dict", {}),
|
|
140
144
|
cache_key=cache_key,
|
|
141
145
|
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
146
|
+
if serialize_exception is not None:
|
|
147
|
+
future_obj.set_exception(exception=serialize_exception)
|
|
148
|
+
else:
|
|
149
|
+
file_name = os.path.abspath(os.path.join(cache_directory, task_key + "_o.h5"))
|
|
150
|
+
if file_name not in get_cache_files(cache_directory=cache_directory):
|
|
145
151
|
time_start = time.time()
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
try:
|
|
153
|
+
output = interface.send_and_receive_dict(input_dict=task_dict)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
future_obj.set_exception(exception=e)
|
|
156
|
+
return True
|
|
157
|
+
if "result" in output:
|
|
158
|
+
data_dict["output"] = output["result"]
|
|
159
|
+
data_dict["runtime"] = time.time() - time_start
|
|
160
|
+
dump(file_name=file_name, data_dict=data_dict)
|
|
161
|
+
future_obj.set_result(output["result"])
|
|
162
|
+
elif isinstance(output["error"], ExecutorlibSocketError):
|
|
153
163
|
return False
|
|
154
164
|
else:
|
|
155
|
-
future_obj.set_exception(exception=
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
future_obj.set_exception(exception=output["error"])
|
|
166
|
+
else:
|
|
167
|
+
_, _, result = get_output(file_name=file_name)
|
|
168
|
+
future_obj.set_result(result)
|
|
159
169
|
return True
|
|
@@ -5,12 +5,12 @@ from typing import Callable, Optional
|
|
|
5
5
|
|
|
6
6
|
from pysqa import QueueAdapter
|
|
7
7
|
|
|
8
|
+
from executorlib.standalone.command_pysqa import pysqa_execute_command, pysqa_terminate
|
|
8
9
|
from executorlib.standalone.inputcheck import validate_number_of_cores
|
|
9
10
|
from executorlib.standalone.interactive.spawner import (
|
|
10
11
|
BaseSpawner,
|
|
11
12
|
set_current_directory_in_environment,
|
|
12
13
|
)
|
|
13
|
-
from executorlib.standalone.scheduler import pysqa_execute_command, terminate_with_pysqa
|
|
14
14
|
from executorlib.task_scheduler.base import validate_resource_dict
|
|
15
15
|
from executorlib.task_scheduler.interactive.blockallocation import (
|
|
16
16
|
BlockAllocationTaskScheduler,
|
|
@@ -162,7 +162,7 @@ class PysqaSpawner(BaseSpawner):
|
|
|
162
162
|
wait (bool, optional): Whether to wait for the interface to shutdown. Defaults to True.
|
|
163
163
|
"""
|
|
164
164
|
if self._process is not None:
|
|
165
|
-
|
|
165
|
+
pysqa_terminate(
|
|
166
166
|
queue_id=self._process,
|
|
167
167
|
config_directory=self._config_directory,
|
|
168
168
|
backend=self._backend,
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import contextlib
|
|
2
|
-
import subprocess
|
|
3
|
-
from typing import Optional, Union
|
|
4
|
-
|
|
5
|
-
from pysqa import QueueAdapter
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def terminate_with_pysqa(
|
|
9
|
-
queue_id: int,
|
|
10
|
-
config_directory: Optional[str] = None,
|
|
11
|
-
backend: Optional[str] = None,
|
|
12
|
-
):
|
|
13
|
-
"""
|
|
14
|
-
Delete job from queuing system
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
queue_id (int): Queuing system ID of the job to delete.
|
|
18
|
-
config_directory (str, optional): path to the config directory.
|
|
19
|
-
backend (str, optional): name of the backend used to spawn tasks ["slurm", "flux"].
|
|
20
|
-
"""
|
|
21
|
-
qa = QueueAdapter(
|
|
22
|
-
directory=config_directory,
|
|
23
|
-
queue_type=backend,
|
|
24
|
-
execute_command=pysqa_execute_command,
|
|
25
|
-
)
|
|
26
|
-
status = qa.get_status_of_job(process_id=queue_id)
|
|
27
|
-
if status is not None and status not in ["finished", "error"]:
|
|
28
|
-
with contextlib.suppress(subprocess.CalledProcessError):
|
|
29
|
-
qa.delete_job(process_id=queue_id)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def pysqa_execute_command(
|
|
33
|
-
commands: str,
|
|
34
|
-
working_directory: Optional[str] = None,
|
|
35
|
-
split_output: bool = True,
|
|
36
|
-
shell: bool = False,
|
|
37
|
-
error_filename: str = "pysqa.err",
|
|
38
|
-
) -> Union[str, list[str]]:
|
|
39
|
-
"""
|
|
40
|
-
A wrapper around the subprocess.check_output function. Modified from pysqa to raise an exception if the subprocess
|
|
41
|
-
fails to submit the job to the queue.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
commands (str): The command(s) to be executed on the command line
|
|
45
|
-
working_directory (str, optional): The directory where the command is executed. Defaults to None.
|
|
46
|
-
split_output (bool, optional): Boolean flag to split newlines in the output. Defaults to True.
|
|
47
|
-
shell (bool, optional): Additional switch to convert commands to a single string. Defaults to False.
|
|
48
|
-
error_filename (str, optional): In case the execution fails, the output is written to this file. Defaults to "pysqa.err".
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Union[str, List[str]]: Output of the shell command either as a string or as a list of strings
|
|
52
|
-
"""
|
|
53
|
-
if shell and isinstance(commands, list):
|
|
54
|
-
commands = " ".join(commands)
|
|
55
|
-
out = subprocess.check_output(
|
|
56
|
-
commands,
|
|
57
|
-
cwd=working_directory,
|
|
58
|
-
stderr=subprocess.STDOUT,
|
|
59
|
-
universal_newlines=True,
|
|
60
|
-
shell=not isinstance(commands, list),
|
|
61
|
-
)
|
|
62
|
-
if out is not None and split_output:
|
|
63
|
-
return out.split("\n")
|
|
64
|
-
else:
|
|
65
|
-
return out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/__init__.py
RENAMED
|
File without changes
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/arguments.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/__init__.py
RENAMED
|
File without changes
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/dependency.py
RENAMED
|
File without changes
|
|
File without changes
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/onetoone.py
RENAMED
|
File without changes
|
{executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/spawner_flux.py
RENAMED
|
File without changes
|
|
File without changes
|