hypershell 2.6.4__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.
- hypershell/__init__.py +115 -0
- hypershell/client.py +1255 -0
- hypershell/cluster/__init__.py +378 -0
- hypershell/cluster/local.py +210 -0
- hypershell/cluster/remote.py +721 -0
- hypershell/cluster/ssh.py +352 -0
- hypershell/config.py +440 -0
- hypershell/core/__init__.py +4 -0
- hypershell/core/config.py +359 -0
- hypershell/core/exceptions.py +120 -0
- hypershell/core/fsm.py +82 -0
- hypershell/core/heartbeat.py +70 -0
- hypershell/core/logging.py +201 -0
- hypershell/core/platform.py +121 -0
- hypershell/core/queue.py +155 -0
- hypershell/core/remote.py +192 -0
- hypershell/core/signal.py +79 -0
- hypershell/core/sys.py +39 -0
- hypershell/core/tag.py +47 -0
- hypershell/core/template.py +201 -0
- hypershell/core/thread.py +56 -0
- hypershell/core/types.py +32 -0
- hypershell/data/__init__.py +137 -0
- hypershell/data/core.py +232 -0
- hypershell/data/model.py +674 -0
- hypershell/server.py +1207 -0
- hypershell/submit.py +815 -0
- hypershell/task.py +1141 -0
- hypershell-2.6.4.dist-info/LICENSE +201 -0
- hypershell-2.6.4.dist-info/METADATA +117 -0
- hypershell-2.6.4.dist-info/RECORD +33 -0
- hypershell-2.6.4.dist-info/WHEEL +4 -0
- hypershell-2.6.4.dist-info/entry_points.txt +4 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 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,210 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 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
|
+
while not self.server.queue.ready:
|
|
201
|
+
time.sleep(0.1)
|
|
202
|
+
self.client.start()
|
|
203
|
+
self.client.join()
|
|
204
|
+
self.server.join()
|
|
205
|
+
|
|
206
|
+
def stop(self: LocalCluster, wait: bool = False, timeout: int = None) -> None:
|
|
207
|
+
"""Stop child threads before main thread."""
|
|
208
|
+
self.server.stop(wait=wait, timeout=timeout)
|
|
209
|
+
self.client.stop(wait=wait, timeout=timeout)
|
|
210
|
+
super().stop(wait=wait, timeout=timeout)
|