executorlib 1.10.2__tar.gz → 1.10.4__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.2 → executorlib-1.10.4}/PKG-INFO +10 -10
  2. {executorlib-1.10.2 → executorlib-1.10.4}/pyproject.toml +12 -10
  3. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/__init__.py +29 -0
  4. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/_version.py +2 -2
  5. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/hdf.py +14 -10
  6. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/interactive/communication.py +4 -2
  7. executorlib-1.10.4/src/executorlib/standalone/queue.py +36 -0
  8. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/select.py +2 -1
  9. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/serialize.py +16 -11
  10. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/file/shared.py +159 -113
  11. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/file/spawner_pysqa.py +56 -0
  12. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/blockallocation.py +33 -23
  13. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/shared.py +33 -22
  14. executorlib-1.10.2/src/executorlib/standalone/queue.py +0 -19
  15. {executorlib-1.10.2 → executorlib-1.10.4}/.gitignore +0 -0
  16. {executorlib-1.10.2 → executorlib-1.10.4}/LICENSE +0 -0
  17. {executorlib-1.10.2 → executorlib-1.10.4}/README.md +0 -0
  18. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/api.py +0 -0
  19. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/backend/__init__.py +0 -0
  20. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/backend/cache_parallel.py +0 -0
  21. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/backend/cache_serial.py +0 -0
  22. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/backend/interactive_parallel.py +0 -0
  23. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/backend/interactive_serial.py +0 -0
  24. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/executor/__init__.py +0 -0
  25. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/executor/base.py +0 -0
  26. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/executor/flux.py +0 -0
  27. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/executor/single.py +0 -0
  28. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/executor/slurm.py +0 -0
  29. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/__init__.py +0 -0
  30. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/batched.py +0 -0
  31. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/command.py +0 -0
  32. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/command_pysqa.py +0 -0
  33. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/error.py +0 -0
  34. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/inputcheck.py +0 -0
  35. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/interactive/__init__.py +0 -0
  36. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/interactive/arguments.py +0 -0
  37. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/interactive/backend.py +0 -0
  38. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/interactive/spawner.py +0 -0
  39. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/standalone/validate.py +0 -0
  40. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/__init__.py +0 -0
  41. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/base.py +0 -0
  42. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/file/__init__.py +0 -0
  43. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/file/backend.py +0 -0
  44. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/file/spawner_subprocess.py +0 -0
  45. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/file/task_scheduler.py +0 -0
  46. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/__init__.py +0 -0
  47. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/dependency.py +0 -0
  48. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/dependency_plot.py +0 -0
  49. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/onetoone.py +0 -0
  50. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/spawner_flux.py +0 -0
  51. {executorlib-1.10.2 → executorlib-1.10.4}/src/executorlib/task_scheduler/interactive/spawner_pysqa.py +0 -0
  52. {executorlib-1.10.2 → executorlib-1.10.4}/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.2
3
+ Version: 1.10.4
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.4; extra == 'all'
58
+ Requires-Dist: pydantic<=2.14.1,>=2.5.3; extra == 'all'
59
+ Requires-Dist: pygraphviz<=2.0.3,>=1.10; extra == 'all'
60
+ Requires-Dist: pysqa==1.0.1; 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.4; extra == 'cluster'
65
+ Requires-Dist: pysqa==1.0.1; 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.3,>=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.3,>=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.14.1,>=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.31.0",
3
+ "hatchling>=1.27.0,<=1.32.4",
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.3",
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.3",
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.4",
56
+ "pysqa==1.0.1",
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.14.1",
61
61
  ]
62
62
  all = [
63
63
  "mpi4py>=3.1.4,<=4.1.2",
64
- "pysqa==0.4.4",
64
+ "pysqa==1.0.1",
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.14.1",
67
+ "pygraphviz>=1.10,<=2.0.3",
68
68
  "networkx>=2.8.8,<=3.6.1",
69
69
  "ipython>=7.33.0,<=9.9.0",
70
70
  ]
@@ -102,6 +102,8 @@ ignore = [
102
102
  "PLR0912",
103
103
  # Too many arguments in function definition
104
104
  "PLR0913",
105
+ # Too many statements
106
+ "PLR0915",
105
107
  # Too many not-keyword-only arguments,
106
108
  "PLR0917",
107
109
  # Magic value used in comparison
@@ -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.2'
22
- __version_tuple__ = version_tuple = (1, 10, 2)
21
+ __version__ = version = '1.10.4'
22
+ __version_tuple__ = version_tuple = (1, 10, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -31,18 +31,22 @@ def dump(file_name: Optional[str], data_dict: dict) -> None:
31
31
  """
32
32
  if file_name is not None:
33
33
  file_name_abs = os.path.abspath(file_name)
34
+ # Serialize all values before opening the file, so a serialization error does
35
+ # not leave an incomplete HDF5 file behind.
36
+ binary_dict = {
37
+ group_dict[data_key]: cloudpickle.dumps(data_value)
38
+ for data_key, data_value in data_dict.items()
39
+ if data_key in group_dict
40
+ }
34
41
  os.makedirs(os.path.dirname(file_name_abs), exist_ok=True)
35
42
  with h5py.File(file_name_abs, "a") as fname:
36
- for data_key, data_value in data_dict.items():
37
- if data_key in group_dict:
38
- with contextlib.suppress(ValueError):
39
- fname.create_dataset(
40
- name="/" + group_dict[data_key],
41
- data=np.frombuffer(
42
- cloudpickle.dumps(data_value), dtype=np.uint8
43
- ),
44
- compression="gzip",
45
- )
43
+ for group_name, binary in binary_dict.items():
44
+ with contextlib.suppress(ValueError):
45
+ fname.create_dataset(
46
+ name="/" + group_name,
47
+ data=np.frombuffer(binary, dtype=np.uint8),
48
+ compression="gzip",
49
+ )
46
50
 
47
51
 
48
52
  def load(file_name: str) -> dict:
@@ -150,9 +150,11 @@ class SocketInterface:
150
150
  """
151
151
  result = None
152
152
  if self._spawner.poll():
153
- result = self.send_and_receive_dict(
153
+ output = self.send_and_receive_dict(
154
154
  input_dict={"shutdown": True, "wait": wait}
155
- )["result"]
155
+ )
156
+ if "result" in output:
157
+ result = output["result"]
156
158
  self._spawner.shutdown(wait=wait)
157
159
  self._reset_socket()
158
160
  return result
@@ -0,0 +1,36 @@
1
+ import queue
2
+ from typing import Any
3
+
4
+
5
+ def put_front(que: queue.Queue, item: Any):
6
+ """
7
+ Insert an item at the front of the queue, ahead of any items already waiting, and wake a consumer blocked in
8
+ get(). Equivalent to queue.Queue.put() except the item is placed at the front rather than the back; mutating
9
+ que.queue directly instead would silently skip the notification, leaving a waiting consumer asleep forever.
10
+
11
+ Args:
12
+ que (queue.Queue): Queue with task objects which should be executed
13
+ item (Any): item to place at the front of the queue
14
+ """
15
+ with que.not_full:
16
+ que.queue.insert(0, item)
17
+ que.unfinished_tasks += 1
18
+ que.not_empty.notify()
19
+
20
+
21
+ def cancel_items_in_queue(que: queue.Queue):
22
+ """
23
+ Cancel items which are still waiting in the queue. If the executor is busy tasks remain in the queue, so the future
24
+ objects have to be cancelled when the executor shuts down.
25
+
26
+ Args:
27
+ que (queue.Queue): Queue with task objects which should be executed
28
+ """
29
+ while True:
30
+ try:
31
+ item = que.get_nowait()
32
+ if isinstance(item, dict) and "future" in item:
33
+ item["future"].cancel()
34
+ que.task_done()
35
+ except queue.Empty:
36
+ break
@@ -21,9 +21,10 @@ class FutureSelector(Future):
21
21
  future (Future): The underlying future whose result is a collection.
22
22
  selector (int | str): Index or key used to extract an element from the result.
23
23
  """
24
+ # Future.__init__() is not called, as its attributes would be forwarded to the
25
+ # wrapped future by __setattr__() and reset the state of an already completed future.
24
26
  self._future = future
25
27
  self._selector = selector
26
- super().__init__()
27
28
 
28
29
  def __getattr__(self, attr: str) -> Any:
29
30
  """Delegate attribute access to the wrapped future."""
@@ -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:
@@ -1,7 +1,7 @@
1
1
  import contextlib
2
2
  import os
3
3
  import queue
4
- from concurrent.futures import Future
4
+ from concurrent.futures import CancelledError, Future
5
5
  from time import sleep
6
6
  from typing import Any, Callable, Optional
7
7
 
@@ -118,75 +118,93 @@ def execute_tasks_h5(
118
118
  future_queue.join()
119
119
  break
120
120
  elif task_dict is not None:
121
- task_args, task_kwargs, future_wait_key_lst = _convert_args_and_kwargs(
122
- task_dict=task_dict,
123
- memory_dict=memory_dict,
124
- file_name_dict=file_name_dict,
125
- )
126
- task_resource_dict, cache_key, cache_directory, error_log_file = (
127
- _get_task_input(
128
- task_resource_dict=task_dict["resource_dict"].copy(),
129
- executor_kwargs=executor_kwargs,
121
+ try:
122
+ task_args, task_kwargs, future_wait_key_lst = _convert_args_and_kwargs(
123
+ task_dict=task_dict,
124
+ memory_dict=memory_dict,
125
+ file_name_dict=file_name_dict,
126
+ duplicate_dict=duplicate_dict,
130
127
  )
131
- )
132
- task_key, data_dict = serialize_funct(
133
- fn=task_dict["fn"],
134
- fn_args=task_args,
135
- fn_kwargs=task_kwargs,
136
- resource_dict=task_resource_dict,
137
- cache_key=cache_key,
138
- )
139
- data_dict["error_log_file"] = error_log_file
140
- if task_key not in memory_dict:
141
- if os.path.join(
142
- cache_directory, task_key + "_o.h5"
143
- ) not in get_cache_files(cache_directory=cache_directory):
144
- file_name = os.path.join(cache_directory, task_key + "_i.h5")
145
- if not disable_dependencies:
146
- task_dependent_lst = [
147
- process_dict[k]
148
- for k in future_wait_key_lst
149
- if k in process_dict
150
- ]
151
- else:
152
- if len(future_wait_key_lst) > 0:
153
- task_dict["future"].set_exception(
154
- ValueError(
155
- "Future objects are not supported as input if disable_dependencies=True."
128
+ task_resource_dict, cache_key, cache_directory, error_log_file = (
129
+ _get_task_input(
130
+ task_resource_dict=task_dict["resource_dict"].copy(),
131
+ executor_kwargs=executor_kwargs,
132
+ )
133
+ )
134
+ task_key, data_dict, serialize_exception = serialize_funct(
135
+ fn=task_dict["fn"],
136
+ fn_args=task_args,
137
+ fn_kwargs=task_kwargs,
138
+ resource_dict=task_resource_dict,
139
+ cache_key=cache_key,
140
+ )
141
+ data_dict["error_log_file"] = error_log_file
142
+ if serialize_exception is not None:
143
+ task_dict["future"].set_exception(serialize_exception)
144
+ elif task_key not in memory_dict:
145
+ if os.path.join(
146
+ cache_directory, task_key + "_o.h5"
147
+ ) not in get_cache_files(cache_directory=cache_directory):
148
+ file_name = os.path.join(cache_directory, task_key + "_i.h5")
149
+ if not disable_dependencies:
150
+ task_dependent_lst = [
151
+ process_dict[k]
152
+ for k in future_wait_key_lst
153
+ if k in process_dict
154
+ ]
155
+ else:
156
+ if len(future_wait_key_lst) > 0:
157
+ task_dict["future"].set_exception(
158
+ ValueError(
159
+ "Future objects are not supported as input if disable_dependencies=True."
160
+ )
156
161
  )
162
+ continue
163
+ task_dependent_lst = []
164
+ try:
165
+ process_dict[task_key] = execute_function(
166
+ command=get_cache_execute_command(
167
+ file_name=file_name,
168
+ cores=task_resource_dict["cores"],
169
+ backend=backend,
170
+ exclusive=task_resource_dict.get(
171
+ "exclusive", False
172
+ ),
173
+ openmpi_oversubscribe=task_resource_dict.get(
174
+ "openmpi_oversubscribe", False
175
+ ),
176
+ pmi_mode=pmi_mode,
177
+ ),
178
+ file_name=file_name,
179
+ data_dict=data_dict,
180
+ task_dependent_lst=task_dependent_lst,
181
+ resource_dict=task_resource_dict,
182
+ config_directory=pysqa_config_directory,
183
+ backend=backend,
184
+ cache_directory=cache_directory,
157
185
  )
158
- task_dependent_lst = []
159
- process_dict[task_key] = execute_function(
160
- command=get_cache_execute_command(
161
- file_name=file_name,
162
- cores=task_resource_dict["cores"],
163
- backend=backend,
164
- exclusive=task_resource_dict.get("exclusive", False),
165
- openmpi_oversubscribe=task_resource_dict.get(
166
- "openmpi_oversubscribe", False
167
- ),
168
- pmi_mode=pmi_mode,
169
- ),
170
- file_name=file_name,
171
- data_dict=data_dict,
172
- task_dependent_lst=task_dependent_lst,
173
- resource_dict=task_resource_dict,
174
- config_directory=pysqa_config_directory,
175
- backend=backend,
176
- cache_directory=cache_directory,
177
- )
178
- file_name = os.path.join(cache_directory, task_key + "_o.h5")
179
- file_name_dict[task_key] = file_name
180
- queue_id = get_queue_id(file_name=file_name)
181
- if queue_id is not None:
182
- process_dict[task_key] = queue_id
183
- memory_dict[task_key] = task_dict["future"]
184
- cache_dir_dict[task_key] = cache_directory
185
- elif memory_dict[task_key] != task_dict["future"]:
186
- if task_key not in duplicate_dict:
187
- duplicate_dict[task_key] = []
188
- duplicate_dict[task_key].append(task_dict["future"])
189
- future_queue.task_done()
186
+ except Exception as e:
187
+ task_dict["future"].set_exception(e)
188
+ memory_dict[task_key] = task_dict["future"]
189
+ continue
190
+ file_name = os.path.join(cache_directory, task_key + "_o.h5")
191
+ file_name_dict[task_key] = file_name
192
+ queue_id = get_queue_id(file_name=file_name)
193
+ if queue_id is not None:
194
+ process_dict[task_key] = queue_id
195
+ memory_dict[task_key] = task_dict["future"]
196
+ cache_dir_dict[task_key] = cache_directory
197
+ elif memory_dict[task_key] != task_dict["future"]:
198
+ if task_key not in duplicate_dict:
199
+ duplicate_dict[task_key] = []
200
+ duplicate_dict[task_key].append(task_dict["future"])
201
+ except Exception as e:
202
+ # Fail only the current task, never the scheduler thread, e.g. when
203
+ # one of the input futures of this task failed before submission.
204
+ if not task_dict["future"].done():
205
+ task_dict["future"].set_exception(e)
206
+ finally:
207
+ future_queue.task_done()
190
208
  else:
191
209
  memory_dict = _refresh_memory_dict(
192
210
  memory_dict=memory_dict,
@@ -300,7 +318,10 @@ def _update_future(
300
318
 
301
319
 
302
320
  def _convert_args_and_kwargs(
303
- task_dict: dict, memory_dict: dict, file_name_dict: dict
321
+ task_dict: dict,
322
+ memory_dict: dict,
323
+ file_name_dict: dict,
324
+ duplicate_dict: Optional[dict] = None,
304
325
  ) -> tuple[list, dict, list]:
305
326
  """
306
327
  Convert the arguments and keyword arguments in a task dictionary to the appropriate types.
@@ -309,59 +330,84 @@ def _convert_args_and_kwargs(
309
330
  task_dict (dict): The task dictionary containing the arguments and keyword arguments.
310
331
  memory_dict (dict): The dictionary mapping future objects to their associated task keys.
311
332
  file_name_dict (dict): The dictionary mapping task keys to their corresponding file names.
333
+ duplicate_dict (dict, optional): The dictionary mapping task keys to their associated duplicate future objects.
312
334
 
313
335
  Returns:
314
336
  Tuple[list, dict, list]: A tuple containing the converted arguments, converted keyword arguments, and a list of future wait keys.
315
337
 
338
+ Raises:
339
+ Exception: The exception of an input future which already failed, so the dependent task fails as well.
340
+
316
341
  """
317
- task_args = []
318
- task_kwargs = {}
319
- future_wait_key_lst = []
320
- for arg in task_dict["args"]:
321
- selector = None
322
- if isinstance(arg, Future):
323
- if hasattr(arg, "_future") and hasattr(arg, "_selector"):
324
- selector = arg._selector
325
- future = arg._future
326
- else:
327
- future = arg
328
- match_found = False
329
- for k, v in memory_dict.items():
330
- if future == v:
331
- task_args.append(
332
- FutureItem(file_name=file_name_dict[k], selector=selector)
333
- )
334
- future_wait_key_lst.append(k)
335
- match_found = True
336
- break
337
- if not match_found:
338
- task_args.append(future.result())
339
- else:
340
- task_args.append(arg)
341
- for key, arg in task_dict["kwargs"].items():
342
- selector = None
343
- if isinstance(arg, Future):
344
- if hasattr(arg, "_future") and hasattr(arg, "_selector"):
345
- selector = arg._selector
346
- future = arg._future
347
- else:
348
- future = arg
349
- match_found = False
350
- for k, v in memory_dict.items():
351
- if future == v:
352
- task_kwargs[key] = FutureItem(
353
- file_name=file_name_dict[k], selector=selector
354
- )
355
- future_wait_key_lst.append(k)
356
- match_found = True
357
- break
358
- if not match_found:
359
- task_kwargs[key] = future.result()
360
- else:
361
- task_kwargs[key] = arg
342
+ if duplicate_dict is None:
343
+ duplicate_dict = {}
344
+ future_wait_key_lst: list = []
345
+ task_args = [
346
+ _convert_future(
347
+ arg=arg,
348
+ memory_dict=memory_dict,
349
+ file_name_dict=file_name_dict,
350
+ duplicate_dict=duplicate_dict,
351
+ future_wait_key_lst=future_wait_key_lst,
352
+ )
353
+ for arg in task_dict["args"]
354
+ ]
355
+ task_kwargs = {
356
+ key: _convert_future(
357
+ arg=arg,
358
+ memory_dict=memory_dict,
359
+ file_name_dict=file_name_dict,
360
+ duplicate_dict=duplicate_dict,
361
+ future_wait_key_lst=future_wait_key_lst,
362
+ )
363
+ for key, arg in task_dict["kwargs"].items()
364
+ }
362
365
  return task_args, task_kwargs, future_wait_key_lst
363
366
 
364
367
 
368
+ def _convert_future(
369
+ arg: Any,
370
+ memory_dict: dict,
371
+ file_name_dict: dict,
372
+ duplicate_dict: dict,
373
+ future_wait_key_lst: list,
374
+ ) -> Any:
375
+ """
376
+ Convert a single argument, replacing future objects by FutureItem objects or their results.
377
+
378
+ Args:
379
+ arg (Any): The argument to convert.
380
+ memory_dict (dict): The dictionary mapping task keys to their associated future objects.
381
+ file_name_dict (dict): The dictionary mapping task keys to their corresponding file names.
382
+ duplicate_dict (dict): The dictionary mapping task keys to their associated duplicate future objects.
383
+ future_wait_key_lst (list): List of task keys the current task depends on, extended in place.
384
+
385
+ Returns:
386
+ Any: The converted argument.
387
+
388
+ Raises:
389
+ Exception: The exception of the input future, if it already failed.
390
+
391
+ """
392
+ if not isinstance(arg, Future):
393
+ return arg
394
+ if hasattr(arg, "_future") and hasattr(arg, "_selector"):
395
+ selector = arg._selector
396
+ future = arg._future
397
+ else:
398
+ selector = None
399
+ future = arg
400
+ if future.done():
401
+ exception = CancelledError() if future.cancelled() else future.exception()
402
+ if exception is not None:
403
+ raise exception
404
+ for k, v in memory_dict.items():
405
+ if k in file_name_dict and (future == v or future in duplicate_dict.get(k, [])):
406
+ future_wait_key_lst.append(k)
407
+ return FutureItem(file_name=file_name_dict[k], selector=selector)
408
+ return arg.result()
409
+
410
+
365
411
  def _refresh_memory_dict(
366
412
  memory_dict: dict,
367
413
  cache_dir_dict: dict,
@@ -2,6 +2,7 @@ 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
 
7
8
  from executorlib.standalone.command_pysqa import pysqa_execute_command, pysqa_terminate
@@ -154,3 +155,58 @@ def terminate_task_in_cache(
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
@@ -15,7 +15,7 @@ from executorlib.standalone.interactive.communication import (
15
15
  interface_bootup,
16
16
  )
17
17
  from executorlib.standalone.interactive.spawner import BaseSpawner, MpiExecSpawner
18
- from executorlib.standalone.queue import cancel_items_in_queue
18
+ from executorlib.standalone.queue import cancel_items_in_queue, put_front
19
19
  from executorlib.task_scheduler.base import TaskSchedulerBase, validate_resource_dict
20
20
  from executorlib.task_scheduler.interactive.shared import (
21
21
  execute_task_dict,
@@ -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
@@ -120,18 +122,26 @@ class BlockAllocationTaskScheduler(TaskSchedulerBase):
120
122
  ):
121
123
  if self._max_workers > max_workers:
122
124
  for _ in range(self._max_workers - max_workers):
123
- self._future_queue.queue.insert(0, {"shutdown": True, "wait": True})
125
+ put_front(self._future_queue, {"shutdown": True, "wait": True})
124
126
  while len(self._process) > max_workers:
125
127
  self._process = [
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()
@@ -101,13 +101,17 @@ def _execute_task_without_cache(
101
101
  Returns:
102
102
  bool: True if the task was submitted successfully, False otherwise.
103
103
  """
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
104
+ try:
105
+ output = interface.send_and_receive_dict(input_dict=task_dict)
106
+ except Exception as e:
107
+ future_obj.set_exception(exception=e)
109
108
  else:
110
- future_obj.set_exception(exception=output["error"])
109
+ if "result" in output:
110
+ future_obj.set_result(output["result"])
111
+ elif isinstance(output["error"], ExecutorlibSocketError):
112
+ return False
113
+ else:
114
+ future_obj.set_exception(exception=output["error"])
111
115
  return True
112
116
 
113
117
 
@@ -132,27 +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
- 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
- data_dict["runtime"] = time.time() - time_start
149
- dump(file_name=file_name, data_dict=data_dict)
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"])
146
+ if serialize_exception is not None:
147
+ future_obj.set_exception(exception=serialize_exception)
155
148
  else:
156
- _, _, result = get_output(file_name=file_name)
157
- future_obj.set_result(result)
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):
151
+ time_start = time.time()
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):
163
+ return False
164
+ else:
165
+ future_obj.set_exception(exception=output["error"])
166
+ else:
167
+ _, _, result = get_output(file_name=file_name)
168
+ future_obj.set_result(result)
158
169
  return True
@@ -1,19 +0,0 @@
1
- import queue
2
-
3
-
4
- def cancel_items_in_queue(que: queue.Queue):
5
- """
6
- Cancel items which are still waiting in the queue. If the executor is busy tasks remain in the queue, so the future
7
- objects have to be cancelled when the executor shuts down.
8
-
9
- Args:
10
- que (queue.Queue): Queue with task objects which should be executed
11
- """
12
- while True:
13
- try:
14
- item = que.get_nowait()
15
- if isinstance(item, dict) and "future" in item:
16
- item["future"].cancel()
17
- que.task_done()
18
- except queue.Empty:
19
- break
File without changes
File without changes
File without changes