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.
Files changed (52) hide show
  1. {executorlib-1.10.1 → executorlib-1.10.3}/PKG-INFO +10 -10
  2. {executorlib-1.10.1 → executorlib-1.10.3}/pyproject.toml +16 -12
  3. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/__init__.py +29 -0
  4. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/_version.py +2 -2
  5. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/single.py +2 -2
  6. executorlib-1.10.3/src/executorlib/standalone/command_pysqa.py +148 -0
  7. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/communication.py +9 -9
  8. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/serialize.py +16 -11
  9. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/shared.py +101 -23
  10. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/spawner_pysqa.py +59 -3
  11. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/spawner_subprocess.py +2 -2
  12. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/task_scheduler.py +13 -8
  13. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/blockallocation.py +36 -27
  14. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/shared.py +29 -19
  15. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/spawner_pysqa.py +2 -2
  16. executorlib-1.10.1/src/executorlib/standalone/scheduler.py +0 -65
  17. {executorlib-1.10.1 → executorlib-1.10.3}/.gitignore +0 -0
  18. {executorlib-1.10.1 → executorlib-1.10.3}/LICENSE +0 -0
  19. {executorlib-1.10.1 → executorlib-1.10.3}/README.md +0 -0
  20. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/api.py +0 -0
  21. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/__init__.py +0 -0
  22. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/cache_parallel.py +0 -0
  23. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/cache_serial.py +0 -0
  24. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/interactive_parallel.py +0 -0
  25. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/backend/interactive_serial.py +0 -0
  26. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/__init__.py +0 -0
  27. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/base.py +0 -0
  28. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/flux.py +0 -0
  29. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/executor/slurm.py +0 -0
  30. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/__init__.py +0 -0
  31. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/batched.py +0 -0
  32. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/command.py +0 -0
  33. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/error.py +0 -0
  34. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/hdf.py +0 -0
  35. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/inputcheck.py +0 -0
  36. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/__init__.py +0 -0
  37. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/arguments.py +0 -0
  38. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/backend.py +0 -0
  39. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/interactive/spawner.py +0 -0
  40. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/queue.py +0 -0
  41. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/select.py +0 -0
  42. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/standalone/validate.py +0 -0
  43. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/__init__.py +0 -0
  44. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/base.py +0 -0
  45. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/__init__.py +0 -0
  46. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/file/backend.py +0 -0
  47. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/__init__.py +0 -0
  48. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/dependency.py +0 -0
  49. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/dependency_plot.py +0 -0
  50. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/onetoone.py +0 -0
  51. {executorlib-1.10.1 → executorlib-1.10.3}/src/executorlib/task_scheduler/interactive/spawner_flux.py +0 -0
  52. {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.4
1
+ Metadata-Version: 2.5
2
2
  Name: executorlib
3
- Version: 1.10.1
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.1.0,>=25.0.0
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.4,>=2.5.3; extra == 'all'
59
- Requires-Dist: pygraphviz<=1.14,>=1.10; extra == 'all'
60
- Requires-Dist: pysqa==0.4.3; extra == 'all'
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.4.3; extra == 'cluster'
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.14,>=1.10; extra == 'graph'
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.14,>=1.10; extra == 'graphnotebook'
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.4,>=2.5.3; extra == 'validation'
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.30.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.1.0",
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.1.0",
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.14",
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.14",
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.4.3",
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.4",
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.4.3",
64
+ "pysqa==1.0.0",
65
65
  "h5py>=3.6.0,<=3.16.0",
66
- "pydantic>=2.5.3,<=2.13.4",
67
- "pygraphviz>=1.10,<=1.14",
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.1'
22
- __version_tuple__ = version_tuple = (1, 10, 1)
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
- 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
  """
@@ -152,9 +150,11 @@ class SocketInterface:
152
150
  """
153
151
  result = None
154
152
  if self._spawner.poll():
155
- result = self.send_and_receive_dict(
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 and the serialized data.
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
- binary_all = cloudpickle.dumps(
72
- {
73
- "fn": fn,
74
- "args": fn_args,
75
- "kwargs": fn_kwargs,
76
- }
77
- )
78
- task_key = _get_function_name(fn=fn) + _get_hash(binary=binary_all)
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 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,
@@ -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 task_key not in memory_dict:
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
- process_dict[task_key] = execute_function(
155
- command=get_cache_execute_command(
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
- cores=task_resource_dict["cores"],
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
- exclusive=task_resource_dict.get("exclusive", False),
160
- openmpi_oversubscribe=task_resource_dict.get(
161
- "openmpi_oversubscribe", False
162
- ),
163
- pmi_mode=pmi_mode,
164
- ),
165
- file_name=file_name,
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
- return future_obj
219
- exec_flag, no_error_flag, result = get_output(file_name=file_name)
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 == terminate_subprocess:
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,
@@ -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
- terminate_with_pysqa(
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
- terminate_with_pysqa(
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
@@ -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,
@@ -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
- self_id = random.getrandbits(128)
84
- self._self_id = self_id
83
+ self._self_id = random.getrandbits(128)
85
84
  _interrupt_bootup_dict[self._self_id] = False
86
- alive_workers = [max_workers]
87
- alive_workers_lock = Lock()
88
- bootup_events = [Event() for _ in range(self._max_workers)]
89
- bootup_events[0].set()
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=executor_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._process_kwargs,
142
+ kwargs=self._worker_kwargs(worker_id),
133
143
  )
134
- for _ in range(max_workers - self._max_workers)
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
- 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
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
@@ -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
- 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):
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=thread_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
- file_name = os.path.abspath(os.path.join(cache_directory, task_key + "_o.h5"))
143
- if file_name not in get_cache_files(cache_directory=cache_directory):
144
- try:
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
- result = interface.send_and_receive_dict(input_dict=task_dict)
147
- data_dict["output"] = result
148
- data_dict["runtime"] = time.time() - time_start
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):
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=thread_exception)
156
- else:
157
- _, _, result = get_output(file_name=file_name)
158
- future_obj.set_result(result)
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
- terminate_with_pysqa(
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