executorlib 1.10.0__tar.gz → 1.10.2__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.
Files changed (52) hide show
  1. {executorlib-1.10.0 → executorlib-1.10.2}/PKG-INFO +5 -4
  2. {executorlib-1.10.0 → executorlib-1.10.2}/pyproject.toml +9 -6
  3. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/_version.py +2 -2
  4. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/backend/interactive_serial.py +2 -2
  5. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/executor/single.py +2 -2
  6. executorlib-1.10.2/src/executorlib/standalone/command_pysqa.py +148 -0
  7. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/interactive/communication.py +7 -9
  8. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/file/shared.py +74 -4
  9. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/file/spawner_pysqa.py +3 -3
  10. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/file/spawner_subprocess.py +2 -2
  11. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/file/task_scheduler.py +13 -8
  12. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/blockallocation.py +5 -6
  13. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/shared.py +16 -17
  14. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/spawner_pysqa.py +3 -5
  15. executorlib-1.10.0/src/executorlib/standalone/scheduler.py +0 -65
  16. {executorlib-1.10.0 → executorlib-1.10.2}/.gitignore +0 -0
  17. {executorlib-1.10.0 → executorlib-1.10.2}/LICENSE +0 -0
  18. {executorlib-1.10.0 → executorlib-1.10.2}/README.md +0 -0
  19. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/__init__.py +0 -0
  20. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/api.py +0 -0
  21. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/backend/__init__.py +0 -0
  22. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/backend/cache_parallel.py +0 -0
  23. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/backend/cache_serial.py +0 -0
  24. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/backend/interactive_parallel.py +0 -0
  25. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/executor/__init__.py +0 -0
  26. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/executor/base.py +0 -0
  27. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/executor/flux.py +0 -0
  28. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/executor/slurm.py +0 -0
  29. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/__init__.py +0 -0
  30. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/batched.py +0 -0
  31. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/command.py +0 -0
  32. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/error.py +0 -0
  33. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/hdf.py +0 -0
  34. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/inputcheck.py +0 -0
  35. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/interactive/__init__.py +0 -0
  36. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/interactive/arguments.py +0 -0
  37. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/interactive/backend.py +0 -0
  38. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/interactive/spawner.py +0 -0
  39. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/queue.py +0 -0
  40. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/select.py +0 -0
  41. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/serialize.py +0 -0
  42. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/standalone/validate.py +0 -0
  43. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/__init__.py +0 -0
  44. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/base.py +0 -0
  45. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/file/__init__.py +0 -0
  46. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/file/backend.py +0 -0
  47. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/__init__.py +0 -0
  48. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/dependency.py +0 -0
  49. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/dependency_plot.py +0 -0
  50. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/onetoone.py +0 -0
  51. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/spawner_flux.py +0 -0
  52. {executorlib-1.10.0 → executorlib-1.10.2}/src/executorlib/task_scheduler/interactive/spawner_slurm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: executorlib
3
- Version: 1.10.0
3
+ Version: 1.10.2
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
@@ -45,8 +45,9 @@ Classifier: Programming Language :: Python :: 3.10
45
45
  Classifier: Programming Language :: Python :: 3.11
46
46
  Classifier: Programming Language :: Python :: 3.12
47
47
  Classifier: Programming Language :: Python :: 3.13
48
+ Classifier: Programming Language :: Python :: 3.14
48
49
  Classifier: Topic :: Scientific/Engineering :: Physics
49
- Requires-Python: <3.14,>3.9
50
+ Requires-Python: <3.15,>3.9
50
51
  Requires-Dist: cloudpickle<=3.1.2,>=2.0.0
51
52
  Requires-Dist: pyzmq<=27.1.0,>=25.0.0
52
53
  Provides-Extra: all
@@ -56,12 +57,12 @@ Requires-Dist: mpi4py<=4.1.2,>=3.1.4; extra == 'all'
56
57
  Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'all'
57
58
  Requires-Dist: pydantic<=2.13.4,>=2.5.3; extra == 'all'
58
59
  Requires-Dist: pygraphviz<=1.14,>=1.10; extra == 'all'
59
- Requires-Dist: pysqa==0.4.2; extra == 'all'
60
+ Requires-Dist: pysqa==0.4.4; extra == 'all'
60
61
  Provides-Extra: cache
61
62
  Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'cache'
62
63
  Provides-Extra: cluster
63
64
  Requires-Dist: h5py<=3.16.0,>=3.6.0; extra == 'cluster'
64
- Requires-Dist: pysqa==0.4.2; extra == 'cluster'
65
+ Requires-Dist: pysqa==0.4.4; extra == 'cluster'
65
66
  Provides-Extra: graph
66
67
  Requires-Dist: networkx<=3.6.1,>=2.8.8; extra == 'graph'
67
68
  Requires-Dist: pygraphviz<=1.14,>=1.10; extra == 'graph'
@@ -1,6 +1,6 @@
1
1
  [build-system]
2
2
  requires = [
3
- "hatchling>=1.27.0,<=1.30.1",
3
+ "hatchling>=1.27.0,<=1.31.0",
4
4
  "hatch-vcs==0.5.0",
5
5
  "cloudpickle>=2.0.0,<=3.1.2",
6
6
  "pyzmq>=25.0.0,<=27.1.0",
@@ -16,7 +16,7 @@ authors = [
16
16
  readme = "README.md"
17
17
  license = { file = "LICENSE" }
18
18
  keywords = ["high performance computing", "hpc", "task scheduler", "slurm", "flux-framework", "executor", "python", "parallel computing", "distributed computing", "scientific computing", "concurrent.futures", "mpi4py", "gpu"]
19
- requires-python = ">3.9, <3.14"
19
+ requires-python = ">3.9, <3.15"
20
20
  classifiers = [
21
21
  "Development Status :: 5 - Production/Stable",
22
22
  "Topic :: Scientific/Engineering :: Physics",
@@ -27,6 +27,7 @@ classifiers = [
27
27
  "Programming Language :: Python :: 3.11",
28
28
  "Programming Language :: Python :: 3.12",
29
29
  "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
30
31
  ]
31
32
  dependencies = [
32
33
  "cloudpickle>=2.0.0,<=3.1.2",
@@ -52,7 +53,7 @@ graphnotebook = [
52
53
  ]
53
54
  mpi = ["mpi4py>=3.1.4,<=4.1.2"]
54
55
  cluster = [
55
- "pysqa==0.4.2",
56
+ "pysqa==0.4.4",
56
57
  "h5py>=3.6.0,<=3.16.0",
57
58
  ]
58
59
  validation = [
@@ -60,7 +61,7 @@ validation = [
60
61
  ]
61
62
  all = [
62
63
  "mpi4py>=3.1.4,<=4.1.2",
63
- "pysqa==0.4.2",
64
+ "pysqa==0.4.4",
64
65
  "h5py>=3.6.0,<=3.16.0",
65
66
  "pydantic>=2.5.3,<=2.13.4",
66
67
  "pygraphviz>=1.10,<=1.14",
@@ -97,12 +98,14 @@ ignore = [
97
98
  "E501",
98
99
  # ignore imports which are not at the top of a module
99
100
  "PLC0415",
101
+ # Too many branches
102
+ "PLR0912",
100
103
  # Too many arguments in function definition
101
104
  "PLR0913",
105
+ # Too many not-keyword-only arguments,
106
+ "PLR0917",
102
107
  # Magic value used in comparison
103
108
  "PLR2004",
104
- # Too many branches
105
- "PLR0912",
106
109
  ]
107
110
 
108
111
  [tool.hatch.build]
@@ -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.0'
22
- __version_tuple__ = version_tuple = (1, 10, 0)
21
+ __version__ = version = '1.10.2'
22
+ __version_tuple__ = version_tuple = (1, 10, 2)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -12,7 +12,7 @@ from executorlib.standalone.interactive.communication import (
12
12
  )
13
13
 
14
14
 
15
- def main(argument_lst: Optional[list[str]] = None):
15
+ def evaluate_cmd(argument_lst: Optional[list[str]] = None):
16
16
  """
17
17
  The main function of the program.
18
18
 
@@ -90,4 +90,4 @@ def main(argument_lst: Optional[list[str]] = None):
90
90
 
91
91
 
92
92
  if __name__ == "__main__":
93
- main(argument_lst=sys.argv)
93
+ evaluate_cmd(argument_lst=sys.argv)
@@ -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
- execute_in_subprocess,
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=execute_in_subprocess,
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
@@ -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
- raise ExecutorlibSocketError(
82
- "SocketInterface crashed during execution."
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
- output = cloudpickle.loads(data)
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
  """
@@ -154,7 +152,7 @@ class SocketInterface:
154
152
  if self._spawner.poll():
155
153
  result = self.send_and_receive_dict(
156
154
  input_dict={"shutdown": True, "wait": wait}
157
- )
155
+ )["result"]
158
156
  self._spawner.shutdown(wait=wait)
159
157
  self._reset_socket()
160
158
  return result
@@ -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 terminate_subprocess
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,
@@ -188,7 +193,9 @@ def execute_tasks_h5(
188
193
  cache_dir_dict=cache_dir_dict,
189
194
  process_dict=process_dict,
190
195
  duplicate_dict=duplicate_dict,
196
+ status_check_dict=status_check_dict,
191
197
  terminate_function=terminate_function,
198
+ validate_function=validate_function,
192
199
  pysqa_config_directory=pysqa_config_directory,
193
200
  backend=backend,
194
201
  refresh_rate=refresh_rate,
@@ -199,15 +206,30 @@ def _check_task_output(
199
206
  task_key: str,
200
207
  future_obj: Future,
201
208
  cache_directory: str,
209
+ queue_id: Optional[int] = None,
210
+ pysqa_config_directory: Optional[str] = None,
211
+ backend: Optional[str] = None,
212
+ validate_function: Optional[Callable] = None,
213
+ status_check_dict: Optional[dict] = None,
202
214
  duplicate_dict: Optional[dict] = None,
203
215
  ) -> Future:
204
216
  """
205
217
  Check the output of a task and set the result of the future object if available.
206
218
 
219
+ If the output file is missing and the task is running on a queuing system backend, this also
220
+ detects jobs which died without producing output (e.g. walltime TIMEOUT, OOM, NODE_FAIL or an
221
+ external scancel) by periodically querying the job status via pysqa and fails the future
222
+ instead of leaving it pending forever.
223
+
207
224
  Args:
208
225
  task_key (str): The key of the task.
209
226
  future_obj (Future): The future object associated with the task.
210
227
  cache_directory (str): The directory where the HDF5 files are stored.
228
+ queue_id (int, optional): The queuing system ID of the task, if submitted via pysqa.
229
+ pysqa_config_directory (str, optional): path to the pysqa config directory.
230
+ backend (str, optional): name of the backend used to spawn tasks ["slurm", "flux"].
231
+ status_check_dict (dict): Dictionary tracking when each task's job status was last queried,
232
+ used to throttle calls to the queuing system.
211
233
  duplicate_dict (dict): The dictionary mapping task keys to their associated duplicate future objects.
212
234
  Returns:
213
235
  Future: The updated future object.
@@ -215,8 +237,32 @@ def _check_task_output(
215
237
  """
216
238
  file_name = os.path.join(cache_directory, task_key + "_o.h5")
217
239
  if not os.path.exists(file_name):
218
- return future_obj
219
- exec_flag, no_error_flag, result = get_output(file_name=file_name)
240
+ if (
241
+ backend is None
242
+ or queue_id is None
243
+ or validate_function is None
244
+ or not validate_function(
245
+ task_key=task_key,
246
+ file_name=file_name,
247
+ queue_id=queue_id,
248
+ pysqa_config_directory=pysqa_config_directory,
249
+ backend=backend,
250
+ status_check_dict=status_check_dict,
251
+ )
252
+ ):
253
+ return future_obj
254
+ exec_flag, no_error_flag, result = (
255
+ True,
256
+ False,
257
+ RuntimeError(
258
+ f"executorlib: queue job {queue_id} for task {task_key} terminated without "
259
+ "producing output (timeout, out-of-memory, node failure or cancellation)."
260
+ ),
261
+ )
262
+ else:
263
+ exec_flag, no_error_flag, result = get_output(file_name=file_name)
264
+ if status_check_dict is not None:
265
+ status_check_dict.pop(task_key, None)
220
266
  _update_future(
221
267
  future_obj=future_obj,
222
268
  exec_flag=exec_flag,
@@ -321,7 +367,9 @@ def _refresh_memory_dict(
321
367
  cache_dir_dict: dict,
322
368
  process_dict: dict,
323
369
  duplicate_dict: Optional[dict] = None,
370
+ status_check_dict: Optional[dict] = None,
324
371
  terminate_function: Optional[Callable] = None,
372
+ validate_function: Optional[Callable] = None,
325
373
  pysqa_config_directory: Optional[str] = None,
326
374
  backend: Optional[str] = None,
327
375
  refresh_rate: float = 0.01,
@@ -334,7 +382,10 @@ def _refresh_memory_dict(
334
382
  cache_dir_dict (dict): dictionary with task keys and cache directories
335
383
  process_dict (dict): dictionary with task keys and process reference.
336
384
  duplicate_dict (dict): dictionary with task keys and duplicate future objects.
385
+ status_check_dict (dict): dictionary with task keys and the last time their queuing system
386
+ job status was queried, used to throttle detection of jobs that died without output.
337
387
  terminate_function (callable): The function to terminate the tasks.
388
+ validate_function (callable): The function to validate the tasks.
338
389
  pysqa_config_directory (str): path to the pysqa config directory (only for pysqa based backend).
339
390
  backend (str): name of the backend used to spawn tasks.
340
391
  refresh_rate (float): The rate at which to refresh the result. Defaults to 0.01.
@@ -351,11 +402,19 @@ def _refresh_memory_dict(
351
402
  pysqa_config_directory=pysqa_config_directory,
352
403
  backend=backend,
353
404
  )
405
+ if status_check_dict is not None:
406
+ for key in cancelled_lst:
407
+ status_check_dict.pop(key, None)
354
408
  memory_updated_dict = {
355
409
  key: _check_task_output(
356
410
  task_key=key,
357
411
  future_obj=value,
358
412
  cache_directory=cache_dir_dict[key],
413
+ queue_id=process_dict.get(key),
414
+ pysqa_config_directory=pysqa_config_directory,
415
+ backend=backend,
416
+ validate_function=validate_function,
417
+ status_check_dict=status_check_dict,
359
418
  duplicate_dict=duplicate_dict,
360
419
  )
361
420
  for key, value in memory_dict.items()
@@ -381,7 +440,7 @@ def _cancel_processes(
381
440
  pysqa_config_directory (str): path to the pysqa config directory (only for pysqa based backend).
382
441
  backend (str): name of the backend used to spawn tasks.
383
442
  """
384
- if terminate_function is not None and terminate_function == terminate_subprocess:
443
+ if terminate_function is not None and terminate_function == subprocess_terminate:
385
444
  for task in process_dict.values():
386
445
  terminate_function(task=task)
387
446
  elif terminate_function is not None and backend is not None:
@@ -443,7 +502,9 @@ def _shutdown_executor(
443
502
  process_dict: dict,
444
503
  cache_dir_dict: dict,
445
504
  duplicate_dict: Optional[dict] = None,
505
+ status_check_dict: Optional[dict] = None,
446
506
  terminate_function: Optional[Callable] = None,
507
+ validate_function: Optional[Callable] = None,
447
508
  pysqa_config_directory: Optional[str] = None,
448
509
  backend: Optional[str] = None,
449
510
  refresh_rate: float = 0.01,
@@ -466,6 +527,9 @@ def _shutdown_executor(
466
527
  process_dict (dict): Mapping of task keys to process handles or queue IDs.
467
528
  duplicate_dict (dict): Mapping of task keys to lists of duplicate Future objects.
468
529
  cache_dir_dict (dict): Mapping of task keys to the cache directory for each task.
530
+ status_check_dict (dict): Mapping of task keys to the last time their queuing system job
531
+ status was queried, used to throttle detection of jobs that died without output.
532
+ validate_function (Callable, optional): Function used to validate the tasks.
469
533
  terminate_function (Callable, optional): Function used to terminate running processes.
470
534
  pysqa_config_directory (str, optional): Path to the pysqa config directory.
471
535
  backend (str, optional): Name of the backend ("slurm", "flux", or None for subprocess).
@@ -478,7 +542,9 @@ def _shutdown_executor(
478
542
  cache_dir_dict=cache_dir_dict,
479
543
  process_dict=process_dict,
480
544
  duplicate_dict=duplicate_dict,
545
+ status_check_dict=status_check_dict,
481
546
  terminate_function=terminate_function,
547
+ validate_function=validate_function,
482
548
  pysqa_config_directory=pysqa_config_directory,
483
549
  backend=backend,
484
550
  refresh_rate=refresh_rate,
@@ -493,7 +559,9 @@ def _shutdown_executor(
493
559
  cache_dir_dict=cache_dir_dict,
494
560
  process_dict=process_dict,
495
561
  duplicate_dict=duplicate_dict,
562
+ status_check_dict=status_check_dict,
496
563
  terminate_function=terminate_function,
564
+ validate_function=validate_function,
497
565
  pysqa_config_directory=pysqa_config_directory,
498
566
  backend=backend,
499
567
  refresh_rate=refresh_rate,
@@ -512,7 +580,9 @@ def _shutdown_executor(
512
580
  cache_dir_dict=cache_dir_dict,
513
581
  process_dict=process_dict,
514
582
  duplicate_dict=duplicate_dict,
583
+ status_check_dict=status_check_dict,
515
584
  terminate_function=terminate_function,
585
+ validate_function=validate_function,
516
586
  pysqa_config_directory=pysqa_config_directory,
517
587
  backend=backend,
518
588
  refresh_rate=refresh_rate,
@@ -4,12 +4,12 @@ from typing import Optional
4
4
 
5
5
  from pysqa import QueueAdapter
6
6
 
7
+ from executorlib.standalone.command_pysqa import pysqa_execute_command, pysqa_terminate
7
8
  from executorlib.standalone.hdf import dump, get_queue_id
8
9
  from executorlib.standalone.inputcheck import check_file_exists
9
10
  from executorlib.standalone.interactive.spawner import (
10
11
  set_current_directory_in_environment,
11
12
  )
12
- from executorlib.standalone.scheduler import pysqa_execute_command, terminate_with_pysqa
13
13
 
14
14
 
15
15
  def execute_with_pysqa(
@@ -122,7 +122,7 @@ def terminate_tasks_in_cache(
122
122
  for f in hdf5_file_lst:
123
123
  queue_id = get_queue_id(f)
124
124
  if queue_id is not None:
125
- terminate_with_pysqa(
125
+ pysqa_terminate(
126
126
  queue_id=queue_id,
127
127
  config_directory=pysqa_config_directory,
128
128
  backend=backend,
@@ -148,7 +148,7 @@ def terminate_task_in_cache(
148
148
  file_name = os.path.join(cache_directory, cache_key + "_i.h5")
149
149
  queue_id = get_queue_id(file_name=file_name)
150
150
  if queue_id is not None:
151
- terminate_with_pysqa(
151
+ pysqa_terminate(
152
152
  queue_id=queue_id,
153
153
  config_directory=pysqa_config_directory,
154
154
  backend=backend,
@@ -10,7 +10,7 @@ from executorlib.standalone.interactive.spawner import (
10
10
  )
11
11
 
12
12
 
13
- def execute_in_subprocess(
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 terminate_subprocess(task):
68
+ def subprocess_terminate(task):
69
69
  """
70
70
  Terminate a subprocess and wait for it to complete.
71
71
 
@@ -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
- execute_in_subprocess,
16
- terminate_subprocess,
15
+ subprocess_execute,
16
+ subprocess_terminate,
17
17
  )
18
18
 
19
19
  try:
20
- from executorlib.standalone.scheduler import terminate_with_pysqa
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 = execute_in_subprocess # type: ignore
25
- terminate_with_pysqa = None # type: ignore
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 != execute_in_subprocess:
130
- terminate_function = terminate_with_pysqa # type: ignore
134
+ if execute_function != subprocess_execute:
135
+ terminate_function = pysqa_terminate # type: ignore
131
136
  else:
132
- terminate_function = terminate_subprocess # type: ignore
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,
@@ -379,10 +379,9 @@ def _set_init_function(
379
379
  ) -> Optional[Exception]:
380
380
  interface_initialization_exception = None
381
381
  if init_function is not None and interface.status:
382
- try:
383
- _ = interface.send_and_receive_dict(
384
- input_dict={"init": True, "fn": init_function, "args": (), "kwargs": {}}
385
- )
386
- except Exception as init_exception:
387
- interface_initialization_exception = init_exception
382
+ output = interface.send_and_receive_dict(
383
+ input_dict={"init": True, "fn": init_function, "args": (), "kwargs": {}}
384
+ )
385
+ if "error" in output:
386
+ interface_initialization_exception = output["error"]
388
387
  return interface_initialization_exception
@@ -101,13 +101,13 @@ def _execute_task_without_cache(
101
101
  Returns:
102
102
  bool: True if the task was submitted successfully, False otherwise.
103
103
  """
104
- try:
105
- future_obj.set_result(interface.send_and_receive_dict(input_dict=task_dict))
106
- except Exception as thread_exception:
107
- if isinstance(thread_exception, ExecutorlibSocketError):
108
- return False
109
- else:
110
- future_obj.set_exception(exception=thread_exception)
104
+ output = interface.send_and_receive_dict(input_dict=task_dict)
105
+ if "result" in output:
106
+ future_obj.set_result(output["result"])
107
+ elif isinstance(output["error"], ExecutorlibSocketError):
108
+ return False
109
+ else:
110
+ future_obj.set_exception(exception=output["error"])
111
111
  return True
112
112
 
113
113
 
@@ -141,18 +141,17 @@ def _execute_task_with_cache(
141
141
  )
142
142
  file_name = os.path.abspath(os.path.join(cache_directory, task_key + "_o.h5"))
143
143
  if file_name not in get_cache_files(cache_directory=cache_directory):
144
- try:
145
- time_start = time.time()
146
- result = interface.send_and_receive_dict(input_dict=task_dict)
147
- data_dict["output"] = result
144
+ time_start = time.time()
145
+ output = interface.send_and_receive_dict(input_dict=task_dict)
146
+ if "result" in output:
147
+ data_dict["output"] = output["result"]
148
148
  data_dict["runtime"] = time.time() - time_start
149
149
  dump(file_name=file_name, data_dict=data_dict)
150
- future_obj.set_result(result)
151
- except Exception as thread_exception:
152
- if isinstance(thread_exception, ExecutorlibSocketError):
153
- return False
154
- else:
155
- future_obj.set_exception(exception=thread_exception)
150
+ future_obj.set_result(output["result"])
151
+ elif isinstance(output["error"], ExecutorlibSocketError):
152
+ return False
153
+ else:
154
+ future_obj.set_exception(exception=output["error"])
156
155
  else:
157
156
  _, _, result = get_output(file_name=file_name)
158
157
  future_obj.set_result(result)
@@ -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
- terminate_with_pysqa(
165
+ pysqa_terminate(
166
166
  queue_id=self._process,
167
167
  config_directory=self._config_directory,
168
168
  backend=self._backend,
@@ -189,7 +189,7 @@ class PysqaSpawner(BaseSpawner):
189
189
  if self._cwd is not None:
190
190
  working_directory = os.path.join(self._cwd, hash)
191
191
  else:
192
- working_directory = os.path.abspath(hash)
192
+ working_directory = os.path.join(os.path.abspath("executorlib_cache"), hash)
193
193
  set_current_directory_in_environment()
194
194
  return queue_adapter.submit_job(
195
195
  command=" ".join(self.generate_command(command_lst=command_lst)),
@@ -242,8 +242,6 @@ def create_pysqa_block_allocation_scheduler(
242
242
  executor_kwargs["cwd"] = os.path.abspath(cache_directory)
243
243
  if cache_directory is not None:
244
244
  executor_kwargs["cache_directory"] = os.path.abspath(cache_directory)
245
- else:
246
- executor_kwargs["cache_directory"] = os.path.abspath(".")
247
245
  executor_kwargs["hostname_localhost"] = hostname_localhost
248
246
  executor_kwargs["log_obj_size"] = log_obj_size
249
247
  executor_kwargs["pmi_mode"] = pmi_mode
@@ -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