cluster_tools 4.2.4__tar.gz → 4.3.0__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 (44) hide show
  1. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/PKG-INFO +1 -1
  2. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/__init__.py +7 -1
  3. cluster_tools-4.3.0/cluster_tools/_utils/cfut_options.py +108 -0
  4. cluster_tools-4.3.0/cluster_tools/_utils/output_wait_thread.py +127 -0
  5. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/string_.py +0 -4
  6. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executor_protocol.py +1 -0
  7. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executors/batching.py +3 -2
  8. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executors/dask.py +29 -25
  9. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executors/multiprocessing_.py +29 -62
  10. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executors/sequential.py +25 -26
  11. cluster_tools-4.3.0/cluster_tools/output_store.py +94 -0
  12. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/remote.py +20 -21
  13. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/schedulers/cluster_executor.py +106 -89
  14. cluster_tools-4.3.0/cluster_tools/version.py +1 -0
  15. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools.egg-info/PKG-INFO +1 -1
  16. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools.egg-info/SOURCES.txt +3 -1
  17. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/pyproject.toml +1 -1
  18. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_all.py +176 -16
  19. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_slurm.py +34 -8
  20. cluster_tools-4.2.4/cluster_tools/_utils/file_wait_thread.py +0 -101
  21. cluster_tools-4.2.4/cluster_tools/version.py +0 -1
  22. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/LICENSE +0 -0
  23. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/README.md +0 -0
  24. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/call.py +0 -0
  25. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/multiprocessing_logging_handler.py +0 -0
  26. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/pickling.py +0 -0
  27. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/reflection.py +0 -0
  28. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/tailf.py +0 -0
  29. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/_utils/warning.py +0 -0
  30. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executors/multiprocessing_pickle.py +0 -0
  31. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/executors/sequential_pickle.py +0 -0
  32. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/schedulers/__init__.py +0 -0
  33. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/schedulers/kube.py +0 -0
  34. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/schedulers/pbs.py +0 -0
  35. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools/schedulers/slurm.py +0 -0
  36. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools.egg-info/dependency_links.txt +0 -0
  37. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools.egg-info/requires.txt +0 -0
  38. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/cluster_tools.egg-info/top_level.txt +0 -0
  39. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/setup.cfg +0 -0
  40. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_batching.py +0 -0
  41. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_dask.py +0 -0
  42. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_deref_main.py +0 -0
  43. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_kubernetes.py +0 -0
  44. {cluster_tools-4.2.4 → cluster_tools-4.3.0}/tests/test_multiprocessing.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cluster_tools
3
- Version: 4.2.4
3
+ Version: 4.3.0
4
4
  Summary: Utility library for easily distributing code execution on clusters
5
5
  Author-email: scalable minds <hello@scalableminds.com>
6
6
  License-Expression: MIT
@@ -7,6 +7,10 @@ from cluster_tools.executors.multiprocessing_ import MultiprocessingExecutor
7
7
  from cluster_tools.executors.multiprocessing_pickle import MultiprocessingPickleExecutor
8
8
  from cluster_tools.executors.sequential import SequentialExecutor
9
9
  from cluster_tools.executors.sequential_pickle import SequentialPickleExecutor
10
+ from cluster_tools.output_store import (
11
+ FileOutputStore, # noqa: F401 unused import
12
+ OutputStore, # noqa: F401 unused import
13
+ )
10
14
  from cluster_tools.schedulers.cluster_executor import (
11
15
  ClusterExecutor, # noqa: F401 `cluster_tools.schedulers.cluster_executor.ClusterExecutor` imported but unused;
12
16
  RemoteOutOfMemoryException, # noqa: F401 `cluster_tools.schedulers.cluster_executor.ClusterExecutor` imported but unused;
@@ -108,7 +112,9 @@ def get_executor(environment: str, **kwargs: Any) -> "Executor":
108
112
  return KubernetesExecutor(**kwargs)
109
113
  elif environment == "dask":
110
114
  if "client" in kwargs:
111
- return DaskExecutor(kwargs["client"])
115
+ return DaskExecutor(
116
+ kwargs["client"], output_store=kwargs.get("output_store")
117
+ )
112
118
  else:
113
119
  return DaskExecutor.from_config(**kwargs)
114
120
  elif environment == "multiprocessing":
@@ -0,0 +1,108 @@
1
+ """Handling of the `__cfut_options` kwarg and the `map_to_futures` getters, which
2
+ select where a job's output is persisted. Shared by all executors."""
3
+
4
+ import logging
5
+ import os
6
+ import warnings
7
+ from collections.abc import Callable
8
+ from typing import Any, TypedDict, TypeVar, cast
9
+
10
+ from typing_extensions import NotRequired, ParamSpec
11
+
12
+ from cluster_tools._utils import pickling
13
+ from cluster_tools.output_store import OutputStore
14
+
15
+ _T = TypeVar("_T")
16
+ _P = ParamSpec("_P")
17
+ _S = TypeVar("_S")
18
+
19
+
20
+ # A job's output is persisted as the pickled tuple `(True, result)` in the executor's
21
+ # `OutputStore` (only in the success case, so that it can serve as a checkpoint).
22
+ # `output_key` selects where; for the default `FileOutputStore` it is a file path.
23
+ # `output_pickle_path` is a deprecated alias.
24
+ class CFutDict(TypedDict):
25
+ output_key: NotRequired[str]
26
+ output_pickle_path: NotRequired[str | os.PathLike]
27
+
28
+
29
+ def parse_cfut_options(kwargs: dict[str, Any]) -> str | None:
30
+ """Removes `__cfut_options` from kwargs and returns the output key, if any."""
31
+ if "__cfut_options" not in kwargs:
32
+ return None
33
+ options = cast(CFutDict, kwargs["__cfut_options"])
34
+ del kwargs["__cfut_options"]
35
+ output_key = options.get("output_key")
36
+ output_pickle_path = options.get("output_pickle_path")
37
+ if output_key is None and output_pickle_path is None:
38
+ raise ValueError("__cfut_options must contain output_key.")
39
+ if output_key is not None and output_pickle_path is not None:
40
+ raise ValueError(
41
+ "__cfut_options must not contain both output_key and output_pickle_path."
42
+ )
43
+ if output_key is not None:
44
+ return output_key
45
+ warnings.warn(
46
+ "The output_pickle_path option of __cfut_options is deprecated, "
47
+ "use output_key instead.",
48
+ DeprecationWarning,
49
+ stacklevel=3,
50
+ )
51
+ return str(output_pickle_path)
52
+
53
+
54
+ def resolve_output_key_getter(
55
+ output_pickle_path_getter: Callable[[_S], os.PathLike] | None,
56
+ output_key_getter: Callable[[_S], str] | None,
57
+ ) -> Callable[[_S], str] | None:
58
+ """Merges the `map_to_futures` getters into a single key getter."""
59
+ if output_key_getter is not None and output_pickle_path_getter is not None:
60
+ raise ValueError(
61
+ "Specify either output_key_getter or output_pickle_path_getter, not both."
62
+ )
63
+ if output_key_getter is not None:
64
+ return output_key_getter
65
+ if output_pickle_path_getter is not None:
66
+ warnings.warn(
67
+ "The output_pickle_path_getter argument of map_to_futures is deprecated, "
68
+ "use output_key_getter instead.",
69
+ DeprecationWarning,
70
+ stacklevel=3,
71
+ )
72
+ getter = output_pickle_path_getter
73
+ return lambda arg: str(getter(arg))
74
+ return None
75
+
76
+
77
+ def cfut_options_kwargs(
78
+ arg: _S,
79
+ output_pickle_path_getter: Callable[[_S], os.PathLike] | None,
80
+ output_key_getter: Callable[[_S], str] | None,
81
+ ) -> dict[str, CFutDict]:
82
+ """Builds the `__cfut_options` kwarg for `submit` from the `map_to_futures` getters."""
83
+ key_getter = resolve_output_key_getter(output_pickle_path_getter, output_key_getter)
84
+ if key_getter is None:
85
+ return {}
86
+ return {"__cfut_options": {"output_key": key_getter(arg)}}
87
+
88
+
89
+ def execute_and_persist(
90
+ output_store: OutputStore,
91
+ output_key: str,
92
+ fn: Callable[_P, _T],
93
+ *args: _P.args,
94
+ **kwargs: _P.kwargs,
95
+ ) -> _T:
96
+ """Runs `fn` in the job and stores its result. Used by the in-process executors;
97
+ the cluster executors do the same in `cluster_tools.remote`."""
98
+ try:
99
+ result = fn(*args, **kwargs)
100
+ except Exception as exc:
101
+ logging.warning(f"Job computation failed with:\n{exc.__repr__()}")
102
+ raise exc
103
+ else:
104
+ # Only store the result in the success case, since the output
105
+ # is used as a checkpoint. The cluster executor also stores failures,
106
+ # since it needs them to transport the exception back.
107
+ output_store.write(output_key, pickling.dumps((True, result)), success=True)
108
+ return result
@@ -0,0 +1,127 @@
1
+ import logging
2
+ import threading
3
+ import time
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from cluster_tools.output_store import OutputStore
9
+ from cluster_tools.schedulers.cluster_executor import ClusterExecutor
10
+
11
+
12
+ class OutputWaitThread(threading.Thread):
13
+ """A thread that polls the executor's output store waiting for a set of
14
+ output keys to be written. When a key is written, it invokes a callback.
15
+ """
16
+
17
+ MAX_RETRY = 30
18
+
19
+ def __init__(
20
+ self,
21
+ callback: Callable[[str, bool], None],
22
+ executor: "ClusterExecutor",
23
+ interval: int = 2,
24
+ ):
25
+ """The callable ``callback`` will be invoked with the value
26
+ associated with each key that is written.
27
+ ``interval`` specifies the polling rate.
28
+ """
29
+ threading.Thread.__init__(self)
30
+ self.callback = callback
31
+ self.interval = interval
32
+ self.waiting: dict[str, str] = {}
33
+ self.retryMap: dict[str, int] = {}
34
+ self.lock = threading.Lock()
35
+ self.shutdown = False
36
+ self.executor = executor
37
+
38
+ def stop(self) -> None:
39
+ """Stop the thread soon."""
40
+ with self.lock:
41
+ self.shutdown = True
42
+
43
+ def waitFor(self, key: str, value: str) -> None:
44
+ """Adds a new output key (and its associated callback value) to
45
+ the set of keys being waited upon.
46
+ """
47
+ with self.lock:
48
+ self.waiting[key] = value
49
+
50
+ @staticmethod
51
+ def _poll_single(store: "OutputStore", key: str) -> set[str]:
52
+ try:
53
+ return store.poll([key])
54
+ except Exception:
55
+ logging.warning(
56
+ f"Polling the output store for {key} failed.", exc_info=True
57
+ )
58
+ return set()
59
+
60
+ def run(self) -> None:
61
+ def handle_completed_job(job_id: str, key: str, failed_early: bool) -> None:
62
+ # The callback must not raise, since that would stop this thread and
63
+ # leave all other jobs pending.
64
+ try:
65
+ self.callback(job_id, failed_early)
66
+ except Exception:
67
+ logging.exception(f"Handling the completion of job {job_id} failed.")
68
+ del self.waiting[key]
69
+
70
+ while True:
71
+ with self.lock:
72
+ if self.shutdown:
73
+ return
74
+
75
+ pending_tasks = self.executor.get_pending_tasks()
76
+ store = self.executor.output_store
77
+
78
+ # Don't check status of pending tasks, since this
79
+ # can vastly slow down the polling.
80
+ keys_to_check = [
81
+ key
82
+ for key, job_id in self.waiting.items()
83
+ if job_id not in pending_tasks
84
+ ]
85
+ # Check for outputs in one batch as a fast indicator for job completion.
86
+ # A store can be remote, so a failing poll must not kill this thread;
87
+ # it is retried on the next iteration.
88
+ try:
89
+ written_keys = store.poll(keys_to_check)
90
+ except Exception:
91
+ logging.warning(
92
+ "Polling the output store failed, retrying.", exc_info=True
93
+ )
94
+ written_keys = set()
95
+
96
+ for key in keys_to_check:
97
+ job_id = self.waiting[key]
98
+
99
+ if key in written_keys:
100
+ handle_completed_job(job_id, key, False)
101
+ elif self.executor is not None:
102
+ status = self.executor.check_job_state(job_id)
103
+
104
+ # We have to re-check for the output since this could be written in the mean time
105
+ if key in self._poll_single(store, key):
106
+ handle_completed_job(job_id, key, False)
107
+ else:
108
+ if status == "completed":
109
+ self.retryMap[key] = self.retryMap.get(key, 0)
110
+ self.retryMap[key] += 1
111
+
112
+ if self.retryMap[key] <= OutputWaitThread.MAX_RETRY:
113
+ # Retry by looping again
114
+ logging.warning(
115
+ f"Job state is completed, but output {key} couldn't be found. Retrying {self.retryMap[key]}/{OutputWaitThread.MAX_RETRY}"
116
+ )
117
+ else:
118
+ logging.error(
119
+ f"Job state is completed, but output {key} couldn't be found."
120
+ )
121
+ handle_completed_job(job_id, key, True)
122
+
123
+ elif status == "failed":
124
+ handle_completed_job(job_id, key, True)
125
+ elif status == "ignore":
126
+ pass
127
+ time.sleep(self.interval)
@@ -18,7 +18,3 @@ def random_string(
18
18
  length: int = 32, chars: str = (string.ascii_letters + string.digits)
19
19
  ) -> str:
20
20
  return "".join(random_generator.choice(chars) for i in range(length))
21
-
22
-
23
- def with_preliminary_postfix(name: str) -> str:
24
- return f"{name}.preliminary"
@@ -31,6 +31,7 @@ class Executor(Protocol, AbstractContextManager["Executor"]):
31
31
  fn: Callable[[_S], _T],
32
32
  args: Iterable[_S],
33
33
  output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
34
+ output_key_getter: Callable[[_S], str] | None = None,
34
35
  ) -> list[Future[_T]]: ...
35
36
 
36
37
  def map(
@@ -111,10 +111,11 @@ class BatchingExecutor:
111
111
  fn: Callable[[_S], _T],
112
112
  args: Iterable[_S],
113
113
  output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
114
+ output_key_getter: Callable[[_S], str] | None = None,
114
115
  ) -> list[Future[_T]]:
115
- if output_pickle_path_getter is not None:
116
+ if output_key_getter is not None or output_pickle_path_getter is not None:
116
117
  raise NotImplementedError(
117
- "BatchingExecutor does not support output_pickle_path_getter"
118
+ "BatchingExecutor does not support output_key_getter or output_pickle_path_getter"
118
119
  )
119
120
 
120
121
  items = list(args)
@@ -7,7 +7,6 @@ from concurrent import futures
7
7
  from concurrent.futures import Future
8
8
  from functools import partial
9
9
  from multiprocessing import Queue, get_context
10
- from pathlib import Path
11
10
  from typing import (
12
11
  TYPE_CHECKING,
13
12
  Any,
@@ -19,8 +18,13 @@ from weakref import ReferenceType, ref
19
18
 
20
19
  from typing_extensions import ParamSpec
21
20
 
21
+ from cluster_tools._utils.cfut_options import (
22
+ cfut_options_kwargs,
23
+ execute_and_persist,
24
+ parse_cfut_options,
25
+ )
22
26
  from cluster_tools._utils.warning import enrich_future_with_uncaught_warning
23
- from cluster_tools.executors.multiprocessing_ import CFutDict, MultiprocessingExecutor
27
+ from cluster_tools.output_store import FileOutputStore, OutputStore
24
28
 
25
29
  if TYPE_CHECKING:
26
30
  from distributed import Client
@@ -108,7 +112,10 @@ class DaskExecutor(futures.Executor):
108
112
  is_shutting_down = False
109
113
 
110
114
  def __init__(
111
- self, client: "Client", job_resources: dict[str, Any] | None = None
115
+ self,
116
+ client: "Client",
117
+ job_resources: dict[str, Any] | None = None,
118
+ output_store: OutputStore | None = None,
112
119
  ) -> None:
113
120
  try:
114
121
  import distributed # noqa: F401 unused import
@@ -127,6 +134,7 @@ class DaskExecutor(futures.Executor):
127
134
  self.client = client
128
135
  self.pending_futures = set()
129
136
  self.job_resources = job_resources
137
+ self.output_store = FileOutputStore() if output_store is None else output_store
130
138
 
131
139
  if self.job_resources is not None:
132
140
  # `mem` needs to be a number for dask, so we need to parse it
@@ -151,6 +159,7 @@ class DaskExecutor(futures.Executor):
151
159
  def from_config(
152
160
  cls,
153
161
  job_resources: dict[str, str],
162
+ output_store: OutputStore | None = None,
154
163
  **_kwargs: Any,
155
164
  ) -> "DaskExecutor":
156
165
  from distributed import Client
@@ -161,7 +170,7 @@ class DaskExecutor(futures.Executor):
161
170
  address = os.environ.get("DASK_ADDRESS", None)
162
171
 
163
172
  client = Client(address=address)
164
- return cls(client, job_resources=job_resources)
173
+ return cls(client, job_resources=job_resources, output_store=output_store)
165
174
 
166
175
  @classmethod
167
176
  def as_completed(cls, futures: list[Future[_T]]) -> Iterator[Future[_T]]:
@@ -175,17 +184,14 @@ class DaskExecutor(futures.Executor):
175
184
  *args: _P.args,
176
185
  **kwargs: _P.kwargs,
177
186
  ) -> Future[_T]:
178
- if "__cfut_options" in kwargs:
179
- output_pickle_path = cast(CFutDict, kwargs["__cfut_options"])[
180
- "output_pickle_path"
181
- ]
182
- del kwargs["__cfut_options"]
183
-
187
+ output_key = parse_cfut_options(kwargs)
188
+ if output_key is not None:
184
189
  __fn = cast(
185
190
  Callable[_P, _T],
186
191
  partial(
187
- MultiprocessingExecutor._execute_and_persist_function,
188
- Path(output_pickle_path),
192
+ execute_and_persist,
193
+ self.output_store,
194
+ output_key,
189
195
  __fn,
190
196
  ),
191
197
  )
@@ -245,20 +251,18 @@ class DaskExecutor(futures.Executor):
245
251
  _S
246
252
  ], # TODO change: allow more than one arg per call # noqa FIX002 Line contains TODO
247
253
  output_pickle_path_getter: Callable[[_S], os.PathLike] | None = None,
254
+ output_key_getter: Callable[[_S], str] | None = None,
248
255
  ) -> list[Future[_T]]:
249
- if output_pickle_path_getter is not None:
250
- futs = [
251
- self.submit( # type: ignore[call-arg]
252
- fn,
253
- arg,
254
- __cfut_options={
255
- "output_pickle_path": output_pickle_path_getter(arg)
256
- },
257
- )
258
- for arg in args
259
- ]
260
- else:
261
- futs = [self.submit(fn, arg) for arg in args]
256
+ futs = [
257
+ self.submit( # type: ignore[call-arg]
258
+ fn,
259
+ arg,
260
+ **cfut_options_kwargs(
261
+ arg, output_pickle_path_getter, output_key_getter
262
+ ),
263
+ )
264
+ for arg in args
265
+ ]
262
266
 
263
267
  return futs
264
268
 
@@ -1,4 +1,3 @@
1
- import logging
2
1
  import multiprocessing
3
2
  import os
4
3
  from collections.abc import Callable, Iterable, Iterator
@@ -6,39 +5,38 @@ from concurrent import futures
6
5
  from concurrent.futures import Future, ProcessPoolExecutor
7
6
  from functools import partial
8
7
  from multiprocessing.context import BaseContext
9
- from pathlib import Path
10
8
  from typing import (
11
9
  Any,
12
- TypedDict,
13
10
  TypeVar,
14
11
  cast,
15
12
  )
16
13
 
17
14
  from typing_extensions import ParamSpec
18
15
 
19
- from cluster_tools._utils import pickling
16
+ from cluster_tools._utils.cfut_options import (
17
+ cfut_options_kwargs,
18
+ execute_and_persist,
19
+ parse_cfut_options,
20
+ )
20
21
  from cluster_tools._utils.multiprocessing_logging_handler import (
21
22
  _MultiprocessingLoggingHandlerPool,
22
23
  )
23
24
  from cluster_tools._utils.warning import enrich_future_with_uncaught_warning
24
-
25
- # The module name includes a _-suffix to avoid name clashes with the standard library multiprocessing module.
26
-
27
-
28
- class CFutDict(TypedDict):
29
- output_pickle_path: str
30
-
25
+ from cluster_tools.output_store import FileOutputStore, OutputStore
31
26
 
32
27
  _T = TypeVar("_T")
33
28
  _P = ParamSpec("_P")
34
29
  _S = TypeVar("_S")
35
30
 
36
31
 
32
+ # The module name includes a _-suffix to avoid name clashes with the standard library multiprocessing module.
33
+
34
+
37
35
  class MultiprocessingExecutor(ProcessPoolExecutor):
38
36
  """
39
37
  Wraps the ProcessPoolExecutor to add various features:
40
38
  - map_to_futures method
41
- - pickling of job's output (see output_pickle_path_getter and output_pickle_path)
39
+ - pickling of job's output (see output_key_getter, output_store and CFutDict)
42
40
  """
43
41
 
44
42
  _mp_context: BaseContext
@@ -51,8 +49,10 @@ class MultiprocessingExecutor(ProcessPoolExecutor):
51
49
  mp_context: BaseContext | None = None,
52
50
  initializer: Callable | None = None,
53
51
  initargs: tuple = (),
52
+ output_store: OutputStore | None = None,
54
53
  **__kwargs: Any,
55
54
  ) -> None:
55
+ self.output_store = FileOutputStore() if output_store is None else output_store
56
56
  if mp_context is None:
57
57
  if start_method is not None:
58
58
  mp_context = multiprocessing.get_context(start_method)
@@ -88,26 +88,21 @@ class MultiprocessingExecutor(ProcessPoolExecutor):
88
88
  *args: _P.args,
89
89
  **kwargs: _P.kwargs,
90
90
  ) -> Future[_T]:
91
- if "__cfut_options" in kwargs:
92
- output_pickle_path = cast(CFutDict, kwargs["__cfut_options"])[
93
- "output_pickle_path"
94
- ]
95
- del kwargs["__cfut_options"]
96
- else:
97
- output_pickle_path = None
91
+ output_key = parse_cfut_options(kwargs)
98
92
 
99
- # Depending on the start_method and output_pickle_path, setup functions may need to be
93
+ # Depending on the start_method and output_key, setup functions may need to be
100
94
  # executed in the new process context, before the actual code is ran.
101
95
  # These wrapper functions consume their arguments from *args, **kwargs and assume
102
96
  # that the next and last argument will be another function that is then called.
103
97
  # Eventually, the actually submitted function will be called.
104
98
 
105
- if output_pickle_path is not None:
99
+ if output_key is not None:
106
100
  __fn = cast(
107
101
  Callable[_P, _T],
108
102
  partial(
109
- MultiprocessingExecutor._execute_and_persist_function,
110
- Path(output_pickle_path),
103
+ execute_and_persist,
104
+ self.output_store,
105
+ output_key,
111
106
  __fn,
112
107
  ),
113
108
  )
@@ -153,51 +148,23 @@ class MultiprocessingExecutor(ProcessPoolExecutor):
153
148
  multiprocessing_logging_setup_fn()
154
149
  return fn(*args, **kwargs)
155
150
 
156
- @staticmethod
157
- def _execute_and_persist_function(
158
- output_pickle_path: Path,
159
- fn: Callable[_P, _T],
160
- *args: _P.args,
161
- **kwargs: _P.kwargs,
162
- ) -> _T:
163
- try:
164
- result = fn(*args, **kwargs)
165
- except Exception as exc:
166
- logging.warning(f"Job computation failed with:\n{exc.__repr__()}")
167
- raise exc
168
- else:
169
- # Only pickle the result in the success case, since the output
170
- # is used as a checkpoint.
171
- # Note that this behavior differs a bit from the cluster executor
172
- # which will always serialize the output (even exceptions) to
173
- # disk. However, the output will have a .preliminary prefix at first
174
- # which is only removed in the success case so that a checkpoint at
175
- # the desired target only exists if the job was successful.
176
- with output_pickle_path.open("wb") as file:
177
- pickling.dump((True, result), file)
178
- return result
179
-
180
151
  def map_to_futures(
181
152
  self,
182
153
  fn: Callable[[_S], _T],
183
154
  args: Iterable[_S],
184
155
  output_pickle_path_getter: Callable[[_S], os.PathLike] | None = None,
156
+ output_key_getter: Callable[[_S], str] | None = None,
185
157
  ) -> list[Future[_T]]:
186
- if output_pickle_path_getter is not None:
187
- futs = [
188
- self.submit( # type: ignore[call-arg]
189
- fn,
190
- arg,
191
- __cfut_options={
192
- "output_pickle_path": output_pickle_path_getter(arg)
193
- },
194
- )
195
- for arg in args
196
- ]
197
- else:
198
- futs = [self.submit(fn, arg) for arg in args]
199
-
200
- return futs
158
+ return [
159
+ self.submit( # type: ignore[call-arg]
160
+ fn,
161
+ arg,
162
+ **cfut_options_kwargs(
163
+ arg, output_pickle_path_getter, output_key_getter
164
+ ),
165
+ )
166
+ for arg in args
167
+ ]
201
168
 
202
169
  def forward_log(self, fut: Future[_T]) -> _T:
203
170
  """
@@ -2,13 +2,17 @@ import warnings
2
2
  from collections.abc import Callable, Iterable, Iterator
3
3
  from concurrent.futures import Executor, Future, as_completed
4
4
  from os import PathLike
5
- from pathlib import Path
6
- from typing import Any, TypeVar, cast
5
+ from typing import Any, TypeVar
7
6
 
8
7
  from typing_extensions import ParamSpec
9
8
 
9
+ from cluster_tools._utils.cfut_options import (
10
+ cfut_options_kwargs,
11
+ execute_and_persist,
12
+ parse_cfut_options,
13
+ )
10
14
  from cluster_tools._utils.warning import enrich_future_with_uncaught_warning
11
- from cluster_tools.executors.multiprocessing_ import CFutDict, MultiprocessingExecutor
15
+ from cluster_tools.output_store import FileOutputStore, OutputStore
12
16
 
13
17
  _T = TypeVar("_T")
14
18
  _S = TypeVar("_S")
@@ -22,9 +26,10 @@ class SequentialExecutor(Executor):
22
26
 
23
27
  def __init__(
24
28
  self,
29
+ output_store: OutputStore | None = None,
25
30
  **__kwargs: Any,
26
31
  ) -> None:
27
- pass
32
+ self.output_store = FileOutputStore() if output_store is None else output_store
28
33
 
29
34
  def submit( # type: ignore[override]
30
35
  self,
@@ -33,13 +38,11 @@ class SequentialExecutor(Executor):
33
38
  **kwargs: _P.kwargs,
34
39
  ) -> Future[_T]:
35
40
  fut: Future[_T] = Future()
36
- if "__cfut_options" in kwargs:
37
- output_pickle_path = cast(CFutDict, kwargs["__cfut_options"])[
38
- "output_pickle_path"
39
- ]
40
- del kwargs["__cfut_options"]
41
- result = MultiprocessingExecutor._execute_and_persist_function(
42
- Path(output_pickle_path),
41
+ output_key = parse_cfut_options(kwargs)
42
+ if output_key is not None:
43
+ result = execute_and_persist(
44
+ self.output_store,
45
+ output_key,
43
46
  __fn,
44
47
  *args,
45
48
  **kwargs,
@@ -60,22 +63,18 @@ class SequentialExecutor(Executor):
60
63
  fn: Callable[[_S], _T],
61
64
  args: Iterable[_S],
62
65
  output_pickle_path_getter: Callable[[_S], PathLike] | None = None,
66
+ output_key_getter: Callable[[_S], str] | None = None,
63
67
  ) -> list[Future[_T]]:
64
- if output_pickle_path_getter is not None:
65
- futs = [
66
- self.submit( # type: ignore[call-arg]
67
- fn,
68
- arg,
69
- __cfut_options={
70
- "output_pickle_path": output_pickle_path_getter(arg)
71
- },
72
- )
73
- for arg in args
74
- ]
75
- else:
76
- futs = [self.submit(fn, arg) for arg in args]
77
-
78
- return futs
68
+ return [
69
+ self.submit( # type: ignore[call-arg]
70
+ fn,
71
+ arg,
72
+ **cfut_options_kwargs(
73
+ arg, output_pickle_path_getter, output_key_getter
74
+ ),
75
+ )
76
+ for arg in args
77
+ ]
79
78
 
80
79
  def map( # type: ignore[override]
81
80
  self,