hypershell 2.5.1__py3-none-any.whl

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.
@@ -0,0 +1,378 @@
1
+ # SPDX-FileCopyrightText: 2024 Geoffrey Lentner
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Run full cluster with server and managed clients."""
5
+
6
+
7
+ # type annotations
8
+ from __future__ import annotations
9
+ from typing import IO, Optional, Iterable, Dict, Callable, Type
10
+ from types import TracebackType
11
+
12
+ # standard libs
13
+ import sys
14
+ import shlex
15
+ from functools import cached_property
16
+
17
+ # external libs
18
+ from cmdkit.app import Application
19
+ from cmdkit.cli import Interface, ArgumentError
20
+
21
+ # internal libs
22
+ from hypershell.core.config import config, blame
23
+ from hypershell.core.queue import QueueConfig
24
+ from hypershell.core.logging import Logger
25
+ from hypershell.core.template import DEFAULT_TEMPLATE
26
+ from hypershell.core.exceptions import get_shared_exception_mapping
27
+ from hypershell.data import initdb, checkdb
28
+ from hypershell.client import DEFAULT_NUM_TASKS, DEFAULT_DELAY, DEFAULT_SIGNALWAIT
29
+ from hypershell.server import DEFAULT_BUNDLESIZE, DEFAULT_ATTEMPTS
30
+ from hypershell.submit import DEFAULT_BUNDLEWAIT
31
+ from hypershell.cluster.ssh import run_ssh, SSHCluster, NodeList, DEFAULT_REMOTE_EXE
32
+ from hypershell.cluster.local import run_local, LocalCluster
33
+ from hypershell.cluster.remote import (run_cluster, RemoteCluster, AutoScalingCluster,
34
+ DEFAULT_AUTOSCALE_FACTOR, DEFAULT_AUTOSCALE_PERIOD,
35
+ DEFAULT_AUTOSCALE_MIN_SIZE, DEFAULT_AUTOSCALE_MAX_SIZE,
36
+ DEFAULT_AUTOSCALE_INIT_SIZE)
37
+
38
+ # public interface
39
+ __all__ = ['run_local', 'run_cluster', 'run_ssh',
40
+ 'LocalCluster', 'RemoteCluster', 'AutoScalingCluster', 'SSHCluster',
41
+ 'DEFAULT_REMOTE_EXE', 'DEFAULT_AUTOSCALE_FACTOR', 'DEFAULT_AUTOSCALE_PERIOD',
42
+ 'DEFAULT_AUTOSCALE_MIN_SIZE', 'DEFAULT_AUTOSCALE_MAX_SIZE', 'DEFAULT_AUTOSCALE_INIT_SIZE',
43
+ 'ClusterApp', ]
44
+
45
+ # initialize logger
46
+ log = Logger.with_name(__name__)
47
+
48
+
49
+ APP_NAME = 'hs cluster'
50
+ APP_USAGE = """\
51
+ Usage:
52
+ hs cluster [-h] [FILE | --restart | --forever] [-N NUM] [-t CMD] [-b SIZE] [-w SEC]
53
+ [-p PORT] [-r NUM [--eager]] [-f PATH] [--capture | [-o PATH] [-e PATH]]
54
+ [--ssh [HOST... | --ssh-group NAME] [--env] | --mpi | --launcher=ARGS...]
55
+ [--no-db | --initdb] [--no-confirm] [-d SEC] [-T SEC] [-W SEC] [-S SEC]
56
+ [--autoscaling [MODE] [-P SEC] [-F VALUE] [-I NUM] [-X NUM] [-Y NUM]]
57
+
58
+ Start cluster locally, over SSH, or with a custom launcher.\
59
+ """
60
+
61
+ APP_HELP = f"""\
62
+ {APP_USAGE}
63
+
64
+ Arguments:
65
+ FILE Path to input task file (default: <stdin>).
66
+
67
+ Modes:
68
+ --ssh HOST... Launch directly with SSH host(s).
69
+ --mpi Same as --launcher=mpirun.
70
+ --launcher ARGS... Use specific launch interface.
71
+
72
+ Options:
73
+ -N, --num-tasks NUM Number of task executors per client (default: {DEFAULT_NUM_TASKS}).
74
+ -t, --template CMD Command-line template pattern (default: "{DEFAULT_TEMPLATE}").
75
+ -p, --port NUM Port number (default: {QueueConfig.port}).
76
+ -b, --bundlesize SIZE Size of task bundle (default: {DEFAULT_BUNDLESIZE}).
77
+ -w, --bundlewait SEC Seconds to wait before flushing tasks (default: {DEFAULT_BUNDLEWAIT}).
78
+ -r, --max-retries NUM Auto-retry failed tasks (default: {DEFAULT_ATTEMPTS - 1}).
79
+ --eager Schedule failed tasks before new tasks.
80
+ --no-db Disable database (submit directly to clients).
81
+ --initdb Auto-initialize database.
82
+ --no-confirm Disable client confirmation of task bundle received.
83
+ --forever Schedule forever.
84
+ --restart Start scheduling from last completed task.
85
+ --ssh-args ARGS Command-line arguments for SSH.
86
+ --ssh-group NAME SSH nodelist group in config.
87
+ -E, --env Send environment variables.
88
+ --remote-exe PATH Path to executable on remote hosts.
89
+ -d, --delay-start SEC Delay time for launching clients (default: {DEFAULT_DELAY}).
90
+ -c, --capture Capture individual task <stdout> and <stderr>.
91
+ -o, --output PATH File path for task outputs (default: <stdout>).
92
+ -e, --errors PATH File path for task errors (default: <stderr>).
93
+ -f, --failures PATH File path to write failed task args (default: <none>).
94
+ -T, --timeout SEC Automatically shutdown clients if no tasks received (default: never).
95
+ -W, --task-timeout SEC Task-level walltime limit (default: none).
96
+ -S, --signalwait SEC Task-level signal escalation wait period (default: {DEFAULT_SIGNALWAIT}).
97
+ -A, --autoscaling [MODE] Enable autoscaling (default: disabled). Used with --launcher.
98
+ -F, --factor VALUE Scaling factor (default: 1).
99
+ -P, --period SEC Scaling period in seconds (default: {DEFAULT_AUTOSCALE_PERIOD}).
100
+ -I, --init-size SIZE Initial size of cluster (default: {DEFAULT_AUTOSCALE_INIT_SIZE}).
101
+ -X, --min-size SIZE Minimum size of cluster (default: {DEFAULT_AUTOSCALE_MIN_SIZE}).
102
+ -Y, --max-size SIZE Maximum size of cluster (default: {DEFAULT_AUTOSCALE_MAX_SIZE}).
103
+ -h, --help Show this message and exit.\
104
+ """
105
+
106
+
107
+ class ClusterApp(Application):
108
+ """Run managed cluster."""
109
+
110
+ name = APP_NAME
111
+ interface = Interface(APP_NAME, APP_USAGE, APP_HELP)
112
+
113
+ filepath: str
114
+ interface.add_argument('filepath', nargs='?', default=None)
115
+
116
+ num_tasks: int = 1
117
+ interface.add_argument('-N', '--num-tasks', type=int, default=num_tasks)
118
+
119
+ template: str = DEFAULT_TEMPLATE
120
+ interface.add_argument('-t', '--template', default=template)
121
+
122
+ bundlesize: int = config.server.bundlesize
123
+ interface.add_argument('-b', '--bundlesize', type=int, default=bundlesize)
124
+
125
+ bundlewait: int = config.submit.bundlewait
126
+ interface.add_argument('-w', '--bundlewait', type=int, default=bundlewait)
127
+
128
+ delay_start: float = DEFAULT_DELAY
129
+ interface.add_argument('-d', '--delay-start', type=float, default=delay_start)
130
+
131
+ eager_mode: bool = False
132
+ max_retries: int = DEFAULT_ATTEMPTS - 1
133
+ interface.add_argument('-r', '--max-retries', type=int, default=max_retries)
134
+ interface.add_argument('--eager', action='store_true', dest='eager_mode')
135
+
136
+ in_memory: bool = False
137
+ auto_initdb: bool = False
138
+ db_interface = interface.add_mutually_exclusive_group()
139
+ db_interface.add_argument('--no-db', action='store_true', dest='in_memory')
140
+ db_interface.add_argument('--initdb', action='store_true', dest='auto_initdb')
141
+
142
+ no_confirm: bool = False
143
+ interface.add_argument('--no-confirm', action='store_true')
144
+
145
+ forever_mode: bool = False
146
+ interface.add_argument('--forever', action='store_true', dest='forever_mode')
147
+
148
+ restart_mode: bool = False
149
+ interface.add_argument('--restart', action='store_true', dest='restart_mode')
150
+
151
+ ssh_mode: str = None
152
+ mpi_mode: bool = False
153
+ launch_mode: str = None
154
+ mode_interface = interface.add_mutually_exclusive_group()
155
+ mode_interface.add_argument('--ssh', nargs='?', const='<default>', default=None, dest='ssh_mode')
156
+ mode_interface.add_argument('--mpi', action='store_true', dest='mpi_mode')
157
+ mode_interface.add_argument('--launcher', default=None, dest='launch_mode')
158
+
159
+ ssh_args: str = ''
160
+ interface.add_argument('--ssh-args', default=ssh_args)
161
+
162
+ ssh_group: str = None
163
+ interface.add_argument('--ssh-group', default=None)
164
+
165
+ remote_exe: str = sys.argv[0]
166
+ interface.add_argument('--remote-exe', default=remote_exe)
167
+
168
+ export_env: bool = False
169
+ interface.add_argument('-E', '--env', action='store_true', dest='export_env')
170
+
171
+ port: int = QueueConfig.port
172
+ interface.add_argument('-p', '--port', default=port, type=int)
173
+
174
+ capture: bool = False
175
+ output_path: str = None
176
+ errors_path: str = None
177
+ interface.add_argument('-o', '--output', default=None, dest='output_path')
178
+ interface.add_argument('-e', '--errors', default=None, dest='errors_path')
179
+ interface.add_argument('-c', '--capture', action='store_true')
180
+
181
+ failure_path: str = None
182
+ interface.add_argument('-f', '--failures', default=None, dest='failure_path')
183
+
184
+ task_timeout: int = config.task.timeout
185
+ client_timeout: int = config.client.timeout
186
+ interface.add_argument('-T', '--timeout', type=int, default=client_timeout, dest='client_timeout')
187
+ interface.add_argument('-W', '--task-timeout', type=int, default=task_timeout, dest='task_timeout')
188
+
189
+ task_signalwait: int = config.task.signalwait
190
+ interface.add_argument('-S', '--signalwait', type=int, default=task_signalwait, dest='task_signalwait')
191
+
192
+ autoscaling_policy: str = None
193
+ autoscaling_factor: float = config.autoscale.factor
194
+ autoscaling_period: int = config.autoscale.period
195
+ autoscaling_minimum: int = config.autoscale.size.min
196
+ autoscaling_maximum: int = config.autoscale.size.max
197
+ autoscaling_initial: int = config.autoscale.size.init
198
+ interface.add_argument('-A', '--autoscaling', nargs='?', default=None,
199
+ const=config.autoscale.policy, dest='autoscaling_policy',)
200
+ interface.add_argument('-F', '--factor', type=float, default=autoscaling_factor, dest='autoscaling_factor')
201
+ interface.add_argument('-P', '--period', type=int, default=autoscaling_period, dest='autoscaling_period')
202
+ interface.add_argument('-X', '--min-size', type=int, default=autoscaling_minimum, dest='autoscaling_minimum')
203
+ interface.add_argument('-Y', '--max-size', type=int, default=autoscaling_maximum, dest='autoscaling_maximum')
204
+ interface.add_argument('-I', '--init-size', type=int, default=autoscaling_initial, dest='autoscaling_initial')
205
+
206
+ exceptions = {
207
+ **get_shared_exception_mapping(__name__)
208
+ }
209
+
210
+ def run(self: ClusterApp) -> None:
211
+ """Run cluster."""
212
+ launcher = self.launchers.get(self.mode)
213
+ launcher(source=self.source, num_tasks=self.num_tasks, template=self.template,
214
+ bundlesize=self.bundlesize, bundlewait=self.bundlewait, max_retries=self.max_retries,
215
+ in_memory=self.in_memory, no_confirm=self.no_confirm, forever_mode=self.forever_mode,
216
+ restart_mode=self.restart_mode, redirect_failures=self.failure_stream,
217
+ delay_start=self.delay_start, capture=self.capture,
218
+ client_timeout=self.client_timeout, task_timeout=self.task_timeout,
219
+ task_signalwait=self.task_signalwait)
220
+
221
+ def run_local(self: ClusterApp, **options) -> None:
222
+ """Run local cluster."""
223
+ run_local(**options, redirect_output=self.output_stream, redirect_errors=self.errors_stream)
224
+
225
+ def run_launch(self: ClusterApp, **options) -> None:
226
+ """Run remote cluster with custom launcher."""
227
+ run_cluster(**options, launcher=self.launch_mode,
228
+ remote_exe=self.remote_exe, bind=('0.0.0.0', self.port))
229
+
230
+ def run_mpi(self: ClusterApp, **options) -> None:
231
+ """Run remote cluster with 'mpirun'."""
232
+ run_cluster(**options, launcher='mpirun',
233
+ remote_exe=self.remote_exe, bind=('0.0.0.0', self.port))
234
+
235
+ def run_ssh(self: ClusterApp, **options) -> None:
236
+ """Run remote cluster with SSH."""
237
+ if self.ssh_group:
238
+ nodelist = NodeList.from_config(self.ssh_group)
239
+ else:
240
+ nodelist = NodeList.from_cmdline(self.ssh_mode if self.ssh_mode != '<default>' else None)
241
+ run_ssh(**options, launcher='ssh', launcher_args=shlex.split(self.ssh_args), nodelist=nodelist,
242
+ remote_exe=self.remote_exe, bind=('0.0.0.0', self.port), export_env=self.export_env)
243
+
244
+ def run_autoscaling(self: ClusterApp, **options) -> None:
245
+ """Run remote cluster with custom launcher and autoscaling."""
246
+ run_cluster(**options, autoscaling=True, launcher=(self.launch_mode or ''),
247
+ policy=self.autoscaling_policy, factor=self.autoscaling_factor,
248
+ period=self.autoscaling_period, init_size=self.autoscaling_initial,
249
+ min_size=self.autoscaling_minimum, max_size=self.autoscaling_maximum,
250
+ remote_exe=self.remote_exe, bind=('0.0.0.0', self.port))
251
+
252
+ @cached_property
253
+ def launchers(self: ClusterApp) -> Dict[str, Callable]:
254
+ """Map of launchers."""
255
+ return {
256
+ 'autoscaling': self.run_autoscaling,
257
+ 'local': self.run_local,
258
+ 'launch': self.run_launch,
259
+ 'mpi': self.run_mpi,
260
+ 'ssh': self.run_ssh
261
+ }
262
+
263
+ @cached_property
264
+ def mode(self: ClusterApp) -> str:
265
+ """The launch mode to run the cluster."""
266
+ if self.autoscaling_policy is not None:
267
+ return 'autoscaling'
268
+ for name in ['ssh', 'mpi', 'launch']:
269
+ if getattr(self, f'{name}_mode'):
270
+ return name
271
+ else:
272
+ return 'local'
273
+
274
+ def check_arguments(self: ClusterApp) -> None:
275
+ """Various checks on input arguments."""
276
+ given_filepath = self.filepath is not None
277
+ if self.filepath is None and not self.restart_mode:
278
+ self.filepath = '-' # NOTE: assume STDIN
279
+ if self.restart_mode and self.in_memory:
280
+ raise ArgumentError('Cannot restart without database (given --no-db)')
281
+ if self.output_path and self.mode != 'local':
282
+ raise ArgumentError('Cannot specify -o/--output PATH with remote clients')
283
+ if self.errors_path and self.mode != 'local':
284
+ raise ArgumentError('Cannot specify -e/--errors PATH with remote clients')
285
+ if self.capture and self.output_path:
286
+ raise ArgumentError('Cannot specify -c/--capture with -o/--output')
287
+ if self.capture and self.errors_path:
288
+ raise ArgumentError('Cannot specify -c/--capture with -e/--error')
289
+ if self.in_memory and self.forever_mode:
290
+ raise ArgumentError('Using --forever with --no-db is invalid')
291
+ if self.in_memory and self.restart_mode:
292
+ raise ArgumentError('Using --restart with --no-db is invalid')
293
+ if self.forever_mode and self.restart_mode:
294
+ raise ArgumentError('Using --forever with --restart is invalid')
295
+ if self.ssh_args and not self.ssh_mode:
296
+ raise ArgumentError('Unexpected --ssh-args when not in --ssh mode')
297
+ if self.ssh_group and self.ssh_mode != '<default>':
298
+ raise ArgumentError('Cannot specify --ssh with target with --ssh-group')
299
+ if self.autoscaling_policy is not None and self.ssh_mode:
300
+ raise ArgumentError('Cannot use --autoscaling with --ssh mode')
301
+ if self.autoscaling_policy is not None and self.mpi_mode:
302
+ raise ArgumentError('Cannot use --autoscaling with --mpi mode')
303
+ if self.autoscaling_policy is not None and self.in_memory:
304
+ raise ArgumentError('Cannot use --autoscaling without database (given --no-db)')
305
+ if self.autoscaling_policy is not None and self.restart_mode:
306
+ log.warning('Using --restart is redundant with --autoscaling (implies --forever)')
307
+ if self.autoscaling_policy is not None and self.forever_mode:
308
+ log.warning('Using --forever is redundant with --autoscaling')
309
+ if self.autoscaling_policy is not None and given_filepath:
310
+ log.warning(f'Task file ({self.input_stream.name}) in use but server will '
311
+ f'not halt (--autoscaling implies --forever)')
312
+ if self.autoscaling_policy is not None and self.client_timeout is not None and self.autoscaling_minimum > 0:
313
+ log.warning(f'Use of --autoscaling with --timeout={self.client_timeout} for clients and '
314
+ f'--min-size={self.autoscaling_minimum} will cause repeated client stop/start cycles')
315
+ autoscaling_enabled = self.autoscaling_policy is not None
316
+ policy_is_dynamic = (
317
+ self.autoscaling_policy in ('dynamic', 'DYNAMIC') or
318
+ config.autoscale.policy in ('dynamic', 'DYNAMIC')
319
+ )
320
+ if autoscaling_enabled and policy_is_dynamic and self.client_timeout is None:
321
+ if config.autoscale.policy.lower() == 'dynamic':
322
+ label = blame(config, 'autoscale', 'policy')
323
+ log.warning(f'Use of --autoscaling with policy set to \'dynamic\' ({label}) without client '
324
+ f'--timeout does not allow for scaling down after task pressure subsides')
325
+ else:
326
+ log.warning(f'Use of --autoscaling=dynamic without client --timeout does not allow '
327
+ f'for scaling down after task pressure subsides')
328
+
329
+ @cached_property
330
+ def output_stream(self: ClusterApp) -> IO:
331
+ """IO stream to write task outputs."""
332
+ return sys.stdout if not self.output_path else open(self.output_path, mode='w')
333
+
334
+ @cached_property
335
+ def errors_stream(self: ClusterApp) -> IO:
336
+ """IO stream to write task errors."""
337
+ return sys.stderr if not self.errors_path else open(self.errors_path, mode='w')
338
+
339
+ @cached_property
340
+ def failure_stream(self: ClusterApp) -> Optional[IO]:
341
+ """IO stream to write failed task args."""
342
+ return None if not self.failure_path else open(self.failure_path, mode='w')
343
+
344
+ @cached_property
345
+ def input_stream(self: ClusterApp) -> Optional[IO]:
346
+ """IO stream to read task command-line args."""
347
+ if self.restart_mode:
348
+ return None
349
+ else:
350
+ return sys.stdin if self.filepath == '-' else open(self.filepath, mode='r')
351
+
352
+ @cached_property
353
+ def source(self: ClusterApp) -> Iterable[str]:
354
+ """Input source for task command-line args."""
355
+ return [] if self.restart_mode else self.input_stream
356
+
357
+ def __enter__(self: ClusterApp) -> ClusterApp:
358
+ """Set up resources and attributes."""
359
+ self.check_arguments()
360
+ if config.database.provider == 'sqlite' or self.auto_initdb:
361
+ initdb() # Auto-initialize if local sqlite provider
362
+ elif not self.in_memory:
363
+ checkdb()
364
+ return self
365
+
366
+ def __exit__(self: ClusterApp,
367
+ exc_type: Optional[Type[Exception]],
368
+ exc_val: Optional[Exception],
369
+ exc_tb: Optional[TracebackType]) -> None:
370
+ """Close IO streams if not standard streams."""
371
+ if self.input_stream and self.input_stream is not sys.stdin:
372
+ self.input_stream.close()
373
+ if self.output_stream is not sys.stdout:
374
+ self.output_stream.close()
375
+ if self.errors_stream is not sys.stderr:
376
+ self.errors_stream.close()
377
+ if self.failure_stream:
378
+ self.failure_stream.close()
@@ -0,0 +1,209 @@
1
+ # SPDX-FileCopyrightText: 2024 Geoffrey Lentner
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Local cluster implementation."""
5
+
6
+
7
+ # type annotations
8
+ from __future__ import annotations
9
+ from typing import Iterable, IO
10
+
11
+ # standard libs
12
+ import time
13
+ import secrets
14
+
15
+ # internal libs
16
+ from hypershell.core.thread import Thread
17
+ from hypershell.core.logging import Logger
18
+ from hypershell.core.template import DEFAULT_TEMPLATE
19
+ from hypershell.submit import DEFAULT_BUNDLEWAIT
20
+ from hypershell.server import ServerThread, DEFAULT_BUNDLESIZE, DEFAULT_ATTEMPTS
21
+ from hypershell.client import ClientThread, DEFAULT_DELAY, DEFAULT_SIGNALWAIT, set_client_standalone
22
+
23
+ # public interface
24
+ __all__ = ['run_local', 'LocalCluster']
25
+
26
+ # initialize logger
27
+ log = Logger.with_name('hypershell.cluster')
28
+
29
+
30
+ def run_local(**options) -> None:
31
+ """
32
+ Run local cluster until completion.
33
+
34
+ All function arguments are forwarded directly into a
35
+ :class:`~hypershell.cluster.local.LocalCluster` thread.
36
+
37
+ Example:
38
+ >>> from hypershell.cluster import run_local
39
+ >>> run_local(['echo AAA', 'echo BBB', 'echo CCC'],
40
+ ... num_tasks=16, in_memory=True, no_confirm=True)
41
+
42
+ See Also:
43
+ - :class:`~hypershell.cluster.local.LocalCluster`
44
+ """
45
+ thread = LocalCluster.new(**options)
46
+ try:
47
+ thread.join()
48
+ except Exception:
49
+ thread.stop()
50
+ raise
51
+
52
+
53
+ class LocalCluster(Thread):
54
+ """
55
+ Run server with single local client thread.
56
+
57
+ Args:
58
+ source (Iterable[str], optional):
59
+ Any iterable of command-line tasks.
60
+ A new `source` results in a :class:`~hypershell.submit.SubmitThread` populating
61
+ either the database or the queue directly depending on `in_memory`.
62
+
63
+ num_tasks (int, optional):
64
+ Number of parallel task executor threads.
65
+ See :const:`~hypershell.client.DEFAULT_NUM_TASKS`.
66
+
67
+ template (str, optional):
68
+ Template command pattern.
69
+ See :const:`~hypershell.client.DEFAULT_TEMPLATE`.
70
+
71
+ bundlesize (int optional):
72
+ Size of task bundles returned to server.
73
+ See :const:`~hypershell.server.DEFAULT_BUNDLESIZE`.
74
+
75
+ bundlewait (int optional):
76
+ Waiting period in seconds before forcing return of task bundle to server.
77
+ See :const:`~hypershell.server.DEFAULT_BUNDLEWAIT`.
78
+
79
+ in_memory (bool, optional):
80
+ If True, revert to basic in-memory queue.
81
+
82
+ no_confirm (bool, optional):
83
+ Disable client confirmation of tasks received.
84
+
85
+ forever_mode (bool, optional):
86
+ Regardless of `source`, if enabled schedule forever.
87
+ Conflicts with `restart_mode` and `in_memory`. Default is `False`.
88
+
89
+ restart_mode (bool, optional):
90
+ If `source` is empty, this option allows for the server to continue
91
+ with scheduling from the database until complete.
92
+ Conflicts with `in_memory`. Default is `False`.
93
+
94
+ max_retries (int, optional):
95
+ Number of allowed task retries.
96
+ See :const:`~hypershell.server.DEFAULT_ATTEMPTS`.
97
+
98
+ eager (bool, optional):
99
+ When enabled tasks are retried immediately ahead scheduling new tasks.
100
+ See :const:`~hypershell.server.DEFAULT_EAGER_MODE`.
101
+
102
+ redirect_failures (IO, optional):
103
+ Open file-like object to write failed tasks.
104
+
105
+ redirect_output (IO, optional):
106
+ Optional file-like object for <stdout> redirect.
107
+
108
+ redirect_errors (IO, optional):
109
+ Optional file-like object for <stderr> redirect.
110
+
111
+ delay_start (float, optional):
112
+ Delay in seconds before connecting to server.
113
+ See :const:`~hypershell.client.DEFAULT_DELAY`.
114
+
115
+ capture (bool, optional):
116
+ Isolate task <stdout> and <stderr> in discrete files.
117
+ Defaults to `False`.
118
+
119
+ client_timeout (int, optional):
120
+ Timeout in seconds before disconnecting from server.
121
+ By default, the client waits for server tor request disconnect.
122
+
123
+ task_timeout (int, optional):
124
+ Task-level walltime limit in seconds.
125
+ By default, the client waits indefinitely on tasks.
126
+
127
+ task_signalwait (int, optional):
128
+ Signal escalation waiting period in seconds on task timeout.
129
+ See :const:`~hypershell.client.DEFAULT_SIGNALWAIT`.
130
+
131
+ Example:
132
+ >>> from hypershell.cluster import LocalCluster
133
+ >>> cluster = LocalCluster.new(
134
+ ... ['echo AAA', 'echo BBB', 'echo CCC'],
135
+ ... num_tasks=16, in_memory=True, no_confirm=True
136
+ ... )
137
+ >>> cluster.join()
138
+
139
+ See Also:
140
+ - :class:`~hypershell.server.ServerThread`
141
+ - :class:`~hypershell.client.ClientThread`
142
+ - :meth:`~hypershell.cluster.local.run_local`
143
+ """
144
+
145
+ server: ServerThread
146
+ client: ClientThread
147
+
148
+ def __init__(self: LocalCluster,
149
+ source: Iterable[str] = None,
150
+ num_tasks: int = 1,
151
+ template: str = DEFAULT_TEMPLATE,
152
+ bundlesize: int = DEFAULT_BUNDLESIZE,
153
+ bundlewait: int = DEFAULT_BUNDLEWAIT,
154
+ in_memory: bool = False,
155
+ no_confirm: bool = False,
156
+ forever_mode: bool = False,
157
+ restart_mode: bool = False,
158
+ max_retries: int = DEFAULT_ATTEMPTS,
159
+ eager: bool = False,
160
+ redirect_failures: IO = None,
161
+ redirect_output: IO = None,
162
+ redirect_errors: IO = None,
163
+ delay_start: float = DEFAULT_DELAY,
164
+ capture: bool = False,
165
+ client_timeout: int = None,
166
+ task_timeout: int = None,
167
+ task_signalwait: int = DEFAULT_SIGNALWAIT) -> None:
168
+ """Initialize with server and single client thread."""
169
+ auth = secrets.token_hex(64)
170
+ self.server = ServerThread(source=source,
171
+ bundlesize=bundlesize,
172
+ bundlewait=bundlewait,
173
+ auth=auth,
174
+ in_memory=in_memory,
175
+ no_confirm=no_confirm,
176
+ max_retries=max_retries,
177
+ eager=eager,
178
+ forever_mode=forever_mode,
179
+ restart_mode=restart_mode,
180
+ redirect_failures=redirect_failures)
181
+ self.client = ClientThread(num_tasks=num_tasks,
182
+ template=template,
183
+ bundlesize=bundlesize,
184
+ bundlewait=bundlewait,
185
+ auth=auth,
186
+ no_confirm=no_confirm,
187
+ redirect_output=redirect_output,
188
+ redirect_errors=redirect_errors,
189
+ delay_start=delay_start,
190
+ capture=capture,
191
+ client_timeout=client_timeout,
192
+ task_timeout=task_timeout,
193
+ task_signalwait=task_signalwait)
194
+ super().__init__(name='hypershell-cluster')
195
+
196
+ def run_with_exceptions(self: LocalCluster) -> None:
197
+ """Start child threads, wait."""
198
+ set_client_standalone(False)
199
+ self.server.start()
200
+ time.sleep(2) # NOTE: give the server a chance to start
201
+ self.client.start()
202
+ self.client.join()
203
+ self.server.join()
204
+
205
+ def stop(self: LocalCluster, wait: bool = False, timeout: int = None) -> None:
206
+ """Stop child threads before main thread."""
207
+ self.server.stop(wait=wait, timeout=timeout)
208
+ self.client.stop(wait=wait, timeout=timeout)
209
+ super().stop(wait=wait, timeout=timeout)