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
hypershell/submit.py
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Geoffrey Lentner
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Submit tasks to the database.
|
|
6
|
+
|
|
7
|
+
Any iterable of command lines can be submitted directly.
|
|
8
|
+
Example:
|
|
9
|
+
>>> from hypershell.submit import submit_from
|
|
10
|
+
>>> submit_from(['echo AA', 'echo BB', 'echo CC'])
|
|
11
|
+
|
|
12
|
+
A file stream is a valid iterable to pass to `submit_from`.
|
|
13
|
+
Use `submit_file` with the file path as shorthand.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from hypershell.submit import submit_file
|
|
17
|
+
>>> submit_file('/path/to/commandlines.txt')
|
|
18
|
+
|
|
19
|
+
Embed a `SubmitThread` in your application directly as the `ServerThread` does.
|
|
20
|
+
Call `stop()` to stop early.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> import sys
|
|
24
|
+
>>> from hypershell.submit import SubmitThread
|
|
25
|
+
>>> submit_thread = SubmitThread.new(sys.stdin, bundlesize=10)
|
|
26
|
+
|
|
27
|
+
Note:
|
|
28
|
+
In order for the `SubmitThread` to actively monitor the state set by `stop` and
|
|
29
|
+
halt execution (a requirement because of how CPython does threading), the implementation
|
|
30
|
+
uses a finite state machine. *You should not instantiate this machine directly*.
|
|
31
|
+
|
|
32
|
+
Warning:
|
|
33
|
+
Because the `SubmitThread` checks state actively to decide whether to halt, if your
|
|
34
|
+
`source` is blocking (e.g., `sys.stdin`) it will not be able to halt immediately. If
|
|
35
|
+
your main program exits however, the thread will be stopped regardless because it
|
|
36
|
+
runs as a `daemon`.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# type annotations
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
from typing import List, Iterable, Iterator, IO, Optional, Dict, Callable, Type, Final
|
|
43
|
+
from types import TracebackType
|
|
44
|
+
|
|
45
|
+
# standard libs
|
|
46
|
+
import io
|
|
47
|
+
import sys
|
|
48
|
+
import functools
|
|
49
|
+
from enum import Enum
|
|
50
|
+
from datetime import datetime
|
|
51
|
+
from queue import Queue, Empty as QueueEmpty, Full as QueueFull
|
|
52
|
+
|
|
53
|
+
# external libs
|
|
54
|
+
from cmdkit.config import ConfigurationError
|
|
55
|
+
from cmdkit.app import Application
|
|
56
|
+
from cmdkit.cli import Interface, ArgumentError
|
|
57
|
+
|
|
58
|
+
# internal libs
|
|
59
|
+
from hypershell.core.logging import Logger
|
|
60
|
+
from hypershell.core.config import config, default
|
|
61
|
+
from hypershell.core.fsm import State, StateMachine
|
|
62
|
+
from hypershell.core.queue import QueueClient, QueueConfig
|
|
63
|
+
from hypershell.core.thread import Thread
|
|
64
|
+
from hypershell.core.template import Template, DEFAULT_TEMPLATE
|
|
65
|
+
from hypershell.core.exceptions import get_shared_exception_mapping
|
|
66
|
+
from hypershell.core.types import JSONValue
|
|
67
|
+
from hypershell.data.model import Task
|
|
68
|
+
from hypershell.data import initdb, checkdb
|
|
69
|
+
from hypershell.task import Tag
|
|
70
|
+
|
|
71
|
+
# public interface
|
|
72
|
+
__all__ = ['submit_from', 'submit_file', 'SubmitThread', 'LiveSubmitThread',
|
|
73
|
+
'SubmitApp', 'DEFAULT_BUNDLESIZE', 'DEFAULT_BUNDLEWAIT', 'DEFAULT_TEMPLATE']
|
|
74
|
+
|
|
75
|
+
# initialize logger
|
|
76
|
+
log = Logger.with_name(__name__)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class LoaderState(State, Enum):
|
|
80
|
+
"""Finite states of loader machine."""
|
|
81
|
+
START = 0
|
|
82
|
+
GET = 1
|
|
83
|
+
PUT = 2
|
|
84
|
+
FINAL = 3
|
|
85
|
+
HALT = 4
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Loader(StateMachine):
|
|
89
|
+
"""Enqueue tasks from iterable source."""
|
|
90
|
+
|
|
91
|
+
task: Task
|
|
92
|
+
source: Iterator[str]
|
|
93
|
+
queue: Queue[Optional[Task]]
|
|
94
|
+
template: Template
|
|
95
|
+
count: int
|
|
96
|
+
tags: Dict[str, str]
|
|
97
|
+
|
|
98
|
+
state = LoaderState.START
|
|
99
|
+
states = LoaderState
|
|
100
|
+
|
|
101
|
+
def __init__(self: Loader,
|
|
102
|
+
source: Iterable[str],
|
|
103
|
+
queue: Queue[Optional[Task]],
|
|
104
|
+
template: str = DEFAULT_TEMPLATE,
|
|
105
|
+
tags: Dict[str, str] = None) -> None:
|
|
106
|
+
"""Initialize source to read tasks and submit to database."""
|
|
107
|
+
self.template = Template(template)
|
|
108
|
+
self.source = map(self.template.expand, map(str.strip, map(str, source)))
|
|
109
|
+
self.queue = queue
|
|
110
|
+
self.tags = tags
|
|
111
|
+
self.count = 0
|
|
112
|
+
|
|
113
|
+
@functools.cached_property
|
|
114
|
+
def actions(self: Loader) -> Dict[LoaderState, Callable[[], LoaderState]]:
|
|
115
|
+
return {
|
|
116
|
+
LoaderState.START: self.start,
|
|
117
|
+
LoaderState.GET: self.get_task,
|
|
118
|
+
LoaderState.PUT: self.put_task,
|
|
119
|
+
LoaderState.FINAL: self.finalize,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def start() -> LoaderState:
|
|
124
|
+
"""Jump to GET state."""
|
|
125
|
+
log.debug('Started (loader)')
|
|
126
|
+
return LoaderState.GET
|
|
127
|
+
|
|
128
|
+
def get_task(self: Loader) -> LoaderState:
|
|
129
|
+
"""Get the next task from the source."""
|
|
130
|
+
try:
|
|
131
|
+
self.task = Task.new(args=next(self.source), tag=self.tags)
|
|
132
|
+
log.trace(f'Loaded task ({self.task.args})')
|
|
133
|
+
return LoaderState.PUT
|
|
134
|
+
except StopIteration:
|
|
135
|
+
return LoaderState.FINAL
|
|
136
|
+
|
|
137
|
+
def put_task(self: Loader) -> LoaderState:
|
|
138
|
+
"""Enqueue loaded task."""
|
|
139
|
+
try:
|
|
140
|
+
self.queue.put(self.task, timeout=1)
|
|
141
|
+
self.count += 1
|
|
142
|
+
return LoaderState.GET
|
|
143
|
+
except QueueFull:
|
|
144
|
+
return LoaderState.PUT
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def finalize() -> LoaderState:
|
|
148
|
+
"""Return HALT."""
|
|
149
|
+
log.debug('Done (loader)')
|
|
150
|
+
return LoaderState.HALT
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class LoaderThread(Thread):
|
|
154
|
+
"""Run loader within dedicated thread."""
|
|
155
|
+
|
|
156
|
+
def __init__(self: LoaderThread,
|
|
157
|
+
source: Iterable[str],
|
|
158
|
+
queue: Queue[Optional[Task]],
|
|
159
|
+
template: str = DEFAULT_TEMPLATE,
|
|
160
|
+
tags: Dict[str, str] = None) -> None:
|
|
161
|
+
"""Initialize machine."""
|
|
162
|
+
super().__init__(name='hypershell-submit-loader')
|
|
163
|
+
self.machine = Loader(source=source, queue=queue, template=template, tags=tags)
|
|
164
|
+
|
|
165
|
+
def run_with_exceptions(self: LoaderThread) -> None:
|
|
166
|
+
"""Run machine."""
|
|
167
|
+
self.machine.run()
|
|
168
|
+
|
|
169
|
+
def stop(self: LoaderThread, wait: bool = False, timeout: int = None) -> None:
|
|
170
|
+
"""Stop machine."""
|
|
171
|
+
log.warning('Stopping (loader)')
|
|
172
|
+
self.machine.halt()
|
|
173
|
+
super().stop(wait=wait, timeout=timeout)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class DatabaseCommitterState(State, Enum):
|
|
177
|
+
"""Finite states for database submitter."""
|
|
178
|
+
START = 0
|
|
179
|
+
GET = 1
|
|
180
|
+
COMMIT = 2
|
|
181
|
+
FINAL = 3
|
|
182
|
+
HALT = 4
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
DEFAULT_BUNDLESIZE: Final[int] = default.submit.bundlesize
|
|
186
|
+
"""Default size of task bundles."""
|
|
187
|
+
|
|
188
|
+
DEFAULT_BUNDLEWAIT: Final[int] = default.submit.bundlewait
|
|
189
|
+
"""Default waiting period before forcing task bundle push."""
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class DatabaseCommitter(StateMachine):
|
|
193
|
+
"""Commit tasks from local queue to database."""
|
|
194
|
+
|
|
195
|
+
queue: Queue[Optional[Task]]
|
|
196
|
+
tasks: List[Task]
|
|
197
|
+
bundlesize: int
|
|
198
|
+
bundlewait: int
|
|
199
|
+
previous_submit: datetime
|
|
200
|
+
|
|
201
|
+
state = DatabaseCommitterState.START
|
|
202
|
+
states = DatabaseCommitterState
|
|
203
|
+
|
|
204
|
+
def __init__(self: DatabaseCommitter,
|
|
205
|
+
queue: Queue[Optional[Task]],
|
|
206
|
+
bundlesize: int = DEFAULT_BUNDLESIZE,
|
|
207
|
+
bundlewait: int = DEFAULT_BUNDLEWAIT) -> None:
|
|
208
|
+
"""Initialize with task queue and buffering parameters."""
|
|
209
|
+
self.queue = queue
|
|
210
|
+
self.tasks = []
|
|
211
|
+
self.bundlesize = bundlesize
|
|
212
|
+
self.bundlewait = bundlewait
|
|
213
|
+
|
|
214
|
+
@functools.cached_property
|
|
215
|
+
def actions(self: DatabaseCommitter) -> Dict[DatabaseCommitterState, Callable[[], DatabaseCommitterState]]:
|
|
216
|
+
return {
|
|
217
|
+
DatabaseCommitterState.START: self.start,
|
|
218
|
+
DatabaseCommitterState.GET: self.get_task,
|
|
219
|
+
DatabaseCommitterState.COMMIT: self.commit,
|
|
220
|
+
DatabaseCommitterState.FINAL: self.finalize,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
def start(self: DatabaseCommitter) -> DatabaseCommitterState:
|
|
224
|
+
"""Jump to GET state."""
|
|
225
|
+
log.debug('Started (committer: database)')
|
|
226
|
+
self.previous_submit = datetime.now()
|
|
227
|
+
return DatabaseCommitterState.GET
|
|
228
|
+
|
|
229
|
+
def get_task(self: DatabaseCommitter) -> DatabaseCommitterState:
|
|
230
|
+
"""Get tasks from local queue and check buffer."""
|
|
231
|
+
try:
|
|
232
|
+
task = self.queue.get(timeout=1)
|
|
233
|
+
except QueueEmpty:
|
|
234
|
+
return DatabaseCommitterState.GET
|
|
235
|
+
if task is not None:
|
|
236
|
+
self.tasks.append(task)
|
|
237
|
+
since_last = (datetime.now() - self.previous_submit).total_seconds()
|
|
238
|
+
if len(self.tasks) >= self.bundlesize or since_last >= self.bundlewait:
|
|
239
|
+
return DatabaseCommitterState.COMMIT
|
|
240
|
+
else:
|
|
241
|
+
return DatabaseCommitterState.GET
|
|
242
|
+
else:
|
|
243
|
+
return DatabaseCommitterState.FINAL
|
|
244
|
+
|
|
245
|
+
def commit(self: DatabaseCommitter) -> DatabaseCommitterState:
|
|
246
|
+
"""Commit tasks to database."""
|
|
247
|
+
if self.tasks:
|
|
248
|
+
Task.add_all(self.tasks)
|
|
249
|
+
log.debug(f'Submitted {len(self.tasks)} tasks')
|
|
250
|
+
self.tasks.clear()
|
|
251
|
+
self.previous_submit = datetime.now()
|
|
252
|
+
return DatabaseCommitterState.GET
|
|
253
|
+
|
|
254
|
+
def finalize(self: DatabaseCommitter) -> DatabaseCommitterState:
|
|
255
|
+
"""Force final commit of tasks and halt."""
|
|
256
|
+
self.commit()
|
|
257
|
+
log.debug('Done (committer: database)')
|
|
258
|
+
return DatabaseCommitterState.HALT
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class DatabaseCommitterThread(Thread):
|
|
262
|
+
"""Run committer within dedicated thread."""
|
|
263
|
+
|
|
264
|
+
def __init__(self: DatabaseCommitterThread,
|
|
265
|
+
queue: Queue[Optional[Task]],
|
|
266
|
+
bundlesize: int = DEFAULT_BUNDLESIZE,
|
|
267
|
+
bundlewait: int = DEFAULT_BUNDLEWAIT) -> None:
|
|
268
|
+
"""Initialize machine."""
|
|
269
|
+
super().__init__(name='hypershell-submit-committer')
|
|
270
|
+
self.machine = DatabaseCommitter(queue=queue, bundlesize=bundlesize, bundlewait=bundlewait)
|
|
271
|
+
|
|
272
|
+
def run_with_exceptions(self: DatabaseCommitterThread) -> None:
|
|
273
|
+
"""Run machine."""
|
|
274
|
+
self.machine.run()
|
|
275
|
+
|
|
276
|
+
def stop(self: DatabaseCommitterThread, wait: bool = False, timeout: int = None) -> None:
|
|
277
|
+
"""Stop machine."""
|
|
278
|
+
log.warning('Stopping (committer: database)')
|
|
279
|
+
self.machine.halt()
|
|
280
|
+
super().stop(wait=wait, timeout=timeout)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class SubmitThread(Thread):
|
|
284
|
+
"""
|
|
285
|
+
Submit tasks to database within dedicated thread.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
source (Iterable[str]):
|
|
289
|
+
Any iterable of command-line tasks.
|
|
290
|
+
|
|
291
|
+
bundlesize (int, optional):
|
|
292
|
+
Size of task bundles.
|
|
293
|
+
See :const:`DEFAULT_BUNDLESIZE`.
|
|
294
|
+
|
|
295
|
+
bundlewait (int, optional):
|
|
296
|
+
Waiting period before forcing task bundle push.
|
|
297
|
+
See :const:`DEFAULT_BUNDLEWAIT`.
|
|
298
|
+
|
|
299
|
+
template (str, optional):
|
|
300
|
+
Task command-line template pattern.
|
|
301
|
+
See :const:`DEFAULT_TEMPLATE`.
|
|
302
|
+
|
|
303
|
+
tags (Dict[str, JSONValue], optional):
|
|
304
|
+
Tag dictionary for all submitted tasks.
|
|
305
|
+
|
|
306
|
+
Example:
|
|
307
|
+
>>> from hypershell.submit import SubmitThread
|
|
308
|
+
>>> submitter = SubmitThread.new(['AAA', 'BBB', 'CCC'],
|
|
309
|
+
... template='my-script {}'
|
|
310
|
+
... tags={'site': 'zzz', 'group': 37})
|
|
311
|
+
>>> submitter.join()
|
|
312
|
+
|
|
313
|
+
See Also:
|
|
314
|
+
- :meth:`submit_from`
|
|
315
|
+
- :meth:`submit_file`
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
source: Iterable[str]
|
|
319
|
+
queue: Queue[Optional[Task]]
|
|
320
|
+
loader: LoaderThread
|
|
321
|
+
committer: DatabaseCommitterThread
|
|
322
|
+
|
|
323
|
+
def __init__(self: SubmitThread,
|
|
324
|
+
source: Iterable[str],
|
|
325
|
+
bundlesize: int = DEFAULT_BUNDLESIZE,
|
|
326
|
+
bundlewait: int = DEFAULT_BUNDLEWAIT,
|
|
327
|
+
template: str = DEFAULT_TEMPLATE,
|
|
328
|
+
tags: Dict[str, JSONValue] = None) -> None:
|
|
329
|
+
"""Initialize queue and child threads."""
|
|
330
|
+
self.source = source
|
|
331
|
+
self.queue = Queue(maxsize=bundlesize)
|
|
332
|
+
self.loader = LoaderThread(source=source, queue=self.queue, template=template, tags=tags)
|
|
333
|
+
self.committer = DatabaseCommitterThread(queue=self.queue, bundlesize=bundlesize, bundlewait=bundlewait)
|
|
334
|
+
super().__init__(name='hypershell-submit')
|
|
335
|
+
|
|
336
|
+
def run_with_exceptions(self: SubmitThread) -> None:
|
|
337
|
+
"""Start child threads, wait."""
|
|
338
|
+
log.debug(f'Started ({self.source_name})')
|
|
339
|
+
self.loader.start()
|
|
340
|
+
self.committer.start()
|
|
341
|
+
self.loader.join()
|
|
342
|
+
self.queue.put(None)
|
|
343
|
+
self.committer.join()
|
|
344
|
+
log.debug('Done')
|
|
345
|
+
|
|
346
|
+
@functools.cached_property
|
|
347
|
+
def source_name(self: SubmitThread) -> str:
|
|
348
|
+
"""Log details of source."""
|
|
349
|
+
if self.source is sys.stdin:
|
|
350
|
+
return '<stdin>'
|
|
351
|
+
elif isinstance(self.source, io.TextIOWrapper):
|
|
352
|
+
return self.source.name
|
|
353
|
+
else:
|
|
354
|
+
return '<iterable>'
|
|
355
|
+
|
|
356
|
+
def stop(self: SubmitThread, wait: bool = False, timeout: int = None) -> None:
|
|
357
|
+
"""Stop child threads before main thread."""
|
|
358
|
+
log.warning('Stopping')
|
|
359
|
+
self.loader.stop(wait=wait, timeout=timeout)
|
|
360
|
+
self.queue.put(None)
|
|
361
|
+
self.committer.stop(wait=wait, timeout=timeout)
|
|
362
|
+
super().stop(wait=wait, timeout=timeout)
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def task_count(self: SubmitThread) -> int:
|
|
366
|
+
"""Count of submitted tasks."""
|
|
367
|
+
return self.loader.machine.count
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
class QueueCommitterState(State, Enum):
|
|
371
|
+
"""Finite states for queue submitter."""
|
|
372
|
+
START = 0
|
|
373
|
+
GET = 1
|
|
374
|
+
PACK = 2
|
|
375
|
+
COMMIT = 3
|
|
376
|
+
FINAL = 4
|
|
377
|
+
HALT = 5
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class QueueCommitter(StateMachine):
|
|
381
|
+
"""Commit tasks from local queue directly to remote server queue."""
|
|
382
|
+
|
|
383
|
+
local: Queue[Optional[Task]]
|
|
384
|
+
client: QueueClient
|
|
385
|
+
|
|
386
|
+
tasks: List[Task]
|
|
387
|
+
bundle: List[bytes]
|
|
388
|
+
|
|
389
|
+
bundlesize: int
|
|
390
|
+
bundlewait: int
|
|
391
|
+
previous_submit: datetime
|
|
392
|
+
|
|
393
|
+
state = QueueCommitterState.START
|
|
394
|
+
states = QueueCommitterState
|
|
395
|
+
|
|
396
|
+
def __init__(self: QueueCommitter, local: Queue[Optional[Task]], client: QueueClient,
|
|
397
|
+
bundlesize: int = DEFAULT_BUNDLESIZE, bundlewait: int = DEFAULT_BUNDLEWAIT) -> None:
|
|
398
|
+
"""Initialize with queue handles and buffering parameters."""
|
|
399
|
+
self.local = local
|
|
400
|
+
self.client = client
|
|
401
|
+
self.tasks = []
|
|
402
|
+
self.bundle = []
|
|
403
|
+
self.bundlesize = bundlesize
|
|
404
|
+
self.bundlewait = bundlewait
|
|
405
|
+
|
|
406
|
+
@functools.cached_property
|
|
407
|
+
def actions(self: QueueCommitter) -> Dict[QueueCommitterState, Callable[[], QueueCommitterState]]:
|
|
408
|
+
return {
|
|
409
|
+
QueueCommitterState.START: self.start,
|
|
410
|
+
QueueCommitterState.GET: self.get_task,
|
|
411
|
+
QueueCommitterState.PACK: self.pack_bundle,
|
|
412
|
+
QueueCommitterState.COMMIT: self.commit,
|
|
413
|
+
QueueCommitterState.FINAL: self.finalize,
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
def start(self: QueueCommitter) -> QueueCommitterState:
|
|
417
|
+
"""Jump to GET state."""
|
|
418
|
+
log.debug('Started (committer: no database)')
|
|
419
|
+
self.previous_submit = datetime.now()
|
|
420
|
+
return QueueCommitterState.GET
|
|
421
|
+
|
|
422
|
+
def get_task(self: QueueCommitter) -> QueueCommitterState:
|
|
423
|
+
"""Get tasks from local queue and check buffer."""
|
|
424
|
+
try:
|
|
425
|
+
task = self.local.get(timeout=1)
|
|
426
|
+
except QueueEmpty:
|
|
427
|
+
return QueueCommitterState.GET
|
|
428
|
+
if task is not None:
|
|
429
|
+
self.tasks.append(task)
|
|
430
|
+
since_last = (datetime.now() - self.previous_submit).total_seconds()
|
|
431
|
+
if len(self.tasks) >= self.bundlesize or since_last >= self.bundlewait:
|
|
432
|
+
return QueueCommitterState.PACK
|
|
433
|
+
else:
|
|
434
|
+
return QueueCommitterState.GET
|
|
435
|
+
else:
|
|
436
|
+
return QueueCommitterState.FINAL
|
|
437
|
+
|
|
438
|
+
def pack_bundle(self: QueueCommitter) -> QueueCommitterState:
|
|
439
|
+
"""Pack tasks into bundle for remote queue."""
|
|
440
|
+
if self.tasks:
|
|
441
|
+
self.bundle = [task.pack() for task in self.tasks]
|
|
442
|
+
return QueueCommitterState.COMMIT
|
|
443
|
+
else:
|
|
444
|
+
return QueueCommitterState.GET
|
|
445
|
+
|
|
446
|
+
def commit(self: QueueCommitter) -> QueueCommitterState:
|
|
447
|
+
"""Commit tasks to server scheduling queue."""
|
|
448
|
+
try:
|
|
449
|
+
if self.tasks:
|
|
450
|
+
self.client.scheduled.put(self.bundle, timeout=2)
|
|
451
|
+
for task in self.tasks:
|
|
452
|
+
log.trace(f'Scheduled task ({task.id})')
|
|
453
|
+
self.tasks = []
|
|
454
|
+
self.bundle = []
|
|
455
|
+
self.previous_submit = datetime.now()
|
|
456
|
+
return QueueCommitterState.GET
|
|
457
|
+
except QueueFull:
|
|
458
|
+
return QueueCommitterState.COMMIT
|
|
459
|
+
|
|
460
|
+
def finalize(self) -> QueueCommitterState:
|
|
461
|
+
"""Force final commit of tasks and halt."""
|
|
462
|
+
self.pack_bundle()
|
|
463
|
+
self.commit()
|
|
464
|
+
log.debug('Done (committer: no database)')
|
|
465
|
+
return QueueCommitterState.HALT
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class QueueCommitterThread(Thread):
|
|
469
|
+
"""Run queue committer within dedicated thread."""
|
|
470
|
+
|
|
471
|
+
def __init__(self: QueueCommitterThread, local: Queue[Optional[Task]], client: QueueClient,
|
|
472
|
+
bundlesize: int = DEFAULT_BUNDLESIZE, bundlewait: int = DEFAULT_BUNDLEWAIT) -> None:
|
|
473
|
+
"""Initialize machine."""
|
|
474
|
+
super().__init__(name='hypershell-submit-committer')
|
|
475
|
+
self.machine = QueueCommitter(local=local, client=client, bundlesize=bundlesize, bundlewait=bundlewait)
|
|
476
|
+
|
|
477
|
+
def run_with_exceptions(self: QueueCommitterThread) -> None:
|
|
478
|
+
"""Run machine."""
|
|
479
|
+
self.machine.run()
|
|
480
|
+
|
|
481
|
+
def stop(self: QueueCommitterThread, wait: bool = False, timeout: int = None) -> None:
|
|
482
|
+
"""Stop machine."""
|
|
483
|
+
log.warning('Stopping (committer: no database)')
|
|
484
|
+
self.machine.halt()
|
|
485
|
+
super().stop(wait=wait, timeout=timeout)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
class LiveSubmitThread(Thread):
|
|
489
|
+
"""
|
|
490
|
+
Submit tasks directly to queue within dedicated thread.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
source (Iterable[str]):
|
|
494
|
+
Any iterable of command-line tasks.
|
|
495
|
+
|
|
496
|
+
queue_config (:class:`~hypershell.core.queue.QueueConfig`):
|
|
497
|
+
QueueConfig instance with `host`, `port`, and `auth`.
|
|
498
|
+
|
|
499
|
+
bundlesize (int, optional):
|
|
500
|
+
Size of task bundles.
|
|
501
|
+
See :const:`DEFAULT_BUNDLESIZE`.
|
|
502
|
+
|
|
503
|
+
bundlewait (int, optional):
|
|
504
|
+
Waiting period before forcing task bundle push.
|
|
505
|
+
See :const:`DEFAULT_BUNDLEWAIT`.
|
|
506
|
+
|
|
507
|
+
template (str, optional):
|
|
508
|
+
Task command-line template pattern.
|
|
509
|
+
See :const:`DEFAULT_TEMPLATE`.
|
|
510
|
+
|
|
511
|
+
tags (Dict[str, JSONValue], optional):
|
|
512
|
+
Tag dictionary for all submitted tasks.
|
|
513
|
+
|
|
514
|
+
Example:
|
|
515
|
+
>>> from hypershell.submit import LiveSubmitThread
|
|
516
|
+
>>> from hypershell.core.queue import QueueConfig
|
|
517
|
+
>>> queue_config = QueueConfig(host='localhost', port=54321, auth='my-secret-key')
|
|
518
|
+
>>> submitter = LiveSubmitThread.new(['AAA', 'BBB', 'CCC'],
|
|
519
|
+
... queue_config=queue_config,
|
|
520
|
+
... template='my-script {}'
|
|
521
|
+
... tags={'site': 'zzz', 'group': 37})
|
|
522
|
+
>>> submitter.join()
|
|
523
|
+
|
|
524
|
+
See Also:
|
|
525
|
+
- :meth:`submit_from`
|
|
526
|
+
- :meth:`submit_file`
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
source: Iterable[str]
|
|
530
|
+
local: Queue[Optional[Task]]
|
|
531
|
+
client: QueueClient
|
|
532
|
+
loader: LoaderThread
|
|
533
|
+
committer: QueueCommitterThread
|
|
534
|
+
|
|
535
|
+
def __init__(self: LiveSubmitThread,
|
|
536
|
+
source: Iterable[str],
|
|
537
|
+
queue_config: QueueConfig,
|
|
538
|
+
template: str = DEFAULT_TEMPLATE,
|
|
539
|
+
bundlesize: int = DEFAULT_BUNDLESIZE,
|
|
540
|
+
bundlewait: int = DEFAULT_BUNDLEWAIT,
|
|
541
|
+
tags: Dict[str, str] = None) -> None:
|
|
542
|
+
"""Initialize queue and child threads."""
|
|
543
|
+
self.source = source
|
|
544
|
+
self.local = Queue(maxsize=bundlesize)
|
|
545
|
+
self.loader = LoaderThread(source=source, queue=self.local, template=template, tags=tags)
|
|
546
|
+
self.client = QueueClient(config=queue_config)
|
|
547
|
+
self.committer = QueueCommitterThread(local=self.local, client=self.client,
|
|
548
|
+
bundlesize=bundlesize, bundlewait=bundlewait)
|
|
549
|
+
super().__init__(name='hypershell-submit')
|
|
550
|
+
|
|
551
|
+
def run_with_exceptions(self: LiveSubmitThread) -> None:
|
|
552
|
+
"""Start child threads, wait."""
|
|
553
|
+
log.debug(f'Started ({self.source_name})')
|
|
554
|
+
with self.client:
|
|
555
|
+
self.loader.start()
|
|
556
|
+
self.committer.start()
|
|
557
|
+
log.trace('Waiting (loader)')
|
|
558
|
+
self.loader.join()
|
|
559
|
+
self.local.put(None)
|
|
560
|
+
log.trace('Waiting (committer)')
|
|
561
|
+
self.committer.join()
|
|
562
|
+
log.debug('Done')
|
|
563
|
+
|
|
564
|
+
@functools.cached_property
|
|
565
|
+
def source_name(self: LiveSubmitThread) -> str:
|
|
566
|
+
"""Log details of source."""
|
|
567
|
+
if self.source is sys.stdin:
|
|
568
|
+
return '<stdin>'
|
|
569
|
+
elif isinstance(self.source, io.TextIOWrapper):
|
|
570
|
+
return self.source.name
|
|
571
|
+
else:
|
|
572
|
+
return '<iterable>'
|
|
573
|
+
|
|
574
|
+
def stop(self: LiveSubmitThread, wait: bool = False, timeout: int = None) -> None:
|
|
575
|
+
"""Stop child threads before main thread."""
|
|
576
|
+
log.warning('Stopping')
|
|
577
|
+
self.loader.stop(wait=wait, timeout=timeout)
|
|
578
|
+
self.local.put(None)
|
|
579
|
+
self.committer.stop(wait=wait, timeout=timeout)
|
|
580
|
+
super().stop(wait=wait, timeout=timeout)
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def task_count(self: LiveSubmitThread) -> int:
|
|
584
|
+
"""Count of submitted tasks."""
|
|
585
|
+
return self.loader.machine.count
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def submit_from(source: Iterable[str],
|
|
589
|
+
queue_config: QueueConfig = None,
|
|
590
|
+
bundlesize: int = DEFAULT_BUNDLESIZE,
|
|
591
|
+
bundlewait: int = DEFAULT_BUNDLEWAIT,
|
|
592
|
+
template: str = DEFAULT_TEMPLATE,
|
|
593
|
+
tags: Dict[str, JSONValue] = None) -> int:
|
|
594
|
+
"""
|
|
595
|
+
Submit all task arguments from `source`, return count of submitted tasks.
|
|
596
|
+
|
|
597
|
+
If `queue_config` is provided, the :class:`LiveSubmitThread` is used to submit
|
|
598
|
+
task bundles directly to the shared queue hosted by the server. Otherwise,
|
|
599
|
+
the :class:`SubmitThread` submits tasks to the database.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
source (Iterable[str]):
|
|
603
|
+
Any iterable of command-line tasks.
|
|
604
|
+
|
|
605
|
+
queue_config (:class:`~hypershell.core.queue.QueueConfig`):
|
|
606
|
+
QueueConfig instance with `host`, `port`, and `auth`.
|
|
607
|
+
|
|
608
|
+
bundlesize (int, optional):
|
|
609
|
+
Size of task bundles.
|
|
610
|
+
See :const:`DEFAULT_BUNDLESIZE`.
|
|
611
|
+
|
|
612
|
+
bundlewait (int, optional):
|
|
613
|
+
Waiting period before forcing task bundle push.
|
|
614
|
+
See :const:`DEFAULT_BUNDLEWAIT`.
|
|
615
|
+
|
|
616
|
+
template (str, optional):
|
|
617
|
+
Task command-line template pattern.
|
|
618
|
+
See :const:`DEFAULT_TEMPLATE`.
|
|
619
|
+
|
|
620
|
+
tags (Dict[str, JSONValue], optional):
|
|
621
|
+
Tag dictionary for all submitted tasks.
|
|
622
|
+
|
|
623
|
+
Example:
|
|
624
|
+
>>> from hypershell.submit import submit_from
|
|
625
|
+
>>> submit_from(['AAA', 'BBB', 'CCC'],
|
|
626
|
+
... template='my-script {}'
|
|
627
|
+
... tags={'site': 'zzz', 'group': 37})
|
|
628
|
+
3
|
|
629
|
+
|
|
630
|
+
See Also:
|
|
631
|
+
- :meth:`submit_file`
|
|
632
|
+
- :class:`SubmitThread`
|
|
633
|
+
- :class:`LiveSubmitThread`
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
task_count (int): Count of submitted tasks.
|
|
637
|
+
"""
|
|
638
|
+
if not queue_config:
|
|
639
|
+
thread = SubmitThread.new(source=source,
|
|
640
|
+
bundlesize=bundlesize,
|
|
641
|
+
bundlewait=bundlewait,
|
|
642
|
+
template=template,
|
|
643
|
+
tags=tags)
|
|
644
|
+
else:
|
|
645
|
+
thread = LiveSubmitThread.new(source=source,
|
|
646
|
+
queue_config=queue_config,
|
|
647
|
+
template=template,
|
|
648
|
+
bundlesize=bundlesize,
|
|
649
|
+
bundlewait=bundlewait,
|
|
650
|
+
tags=tags)
|
|
651
|
+
try:
|
|
652
|
+
thread.join()
|
|
653
|
+
except Exception:
|
|
654
|
+
thread.stop()
|
|
655
|
+
raise
|
|
656
|
+
else:
|
|
657
|
+
return thread.task_count
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def submit_file(path: str,
|
|
661
|
+
queue_config: QueueConfig = None,
|
|
662
|
+
bundlesize: int = DEFAULT_BUNDLESIZE,
|
|
663
|
+
bundlewait: int = DEFAULT_BUNDLEWAIT,
|
|
664
|
+
template: str = DEFAULT_TEMPLATE,
|
|
665
|
+
tags: Dict[str, JSONValue] = None,
|
|
666
|
+
**file_options) -> int:
|
|
667
|
+
"""
|
|
668
|
+
Submit all task arguments by reading them from file `path`.
|
|
669
|
+
|
|
670
|
+
Arguments are forwarded to :func:`submit_from` with the opened file stream
|
|
671
|
+
from `path` as the task `source`.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
path (str):
|
|
675
|
+
Path to file containing command-line tasks.
|
|
676
|
+
|
|
677
|
+
queue_config (:class:`~hypershell.core.queue.QueueConfig`):
|
|
678
|
+
QueueConfig instance with `host`, `port`, and `auth`.
|
|
679
|
+
|
|
680
|
+
bundlesize (int, optional):
|
|
681
|
+
Size of task bundles.
|
|
682
|
+
See :const:`DEFAULT_BUNDLESIZE`.
|
|
683
|
+
|
|
684
|
+
bundlewait (int, optional):
|
|
685
|
+
Waiting period before forcing task bundle push.
|
|
686
|
+
See :const:`DEFAULT_BUNDLEWAIT`.
|
|
687
|
+
|
|
688
|
+
template (str, optional):
|
|
689
|
+
Task command-line template pattern.
|
|
690
|
+
See :const:`DEFAULT_TEMPLATE`.
|
|
691
|
+
|
|
692
|
+
tags (Dict[str, JSONValue], optional):
|
|
693
|
+
Tag dictionary for all submitted tasks.
|
|
694
|
+
|
|
695
|
+
Example:
|
|
696
|
+
>>> from hypershell.submit import submit_from
|
|
697
|
+
>>> from hypershell.core.queue import QueueConfig
|
|
698
|
+
>>> queue_config = QueueConfig(host='my.server.univ.edu', port=54321, auth='my-secret-key')
|
|
699
|
+
>>> submit_file('/tmp/tasks.in',
|
|
700
|
+
... queue_config=queue_config,
|
|
701
|
+
... template='my-script {}'
|
|
702
|
+
... tags={'site': 'zzz', 'group': 37})
|
|
703
|
+
3
|
|
704
|
+
|
|
705
|
+
See Also:
|
|
706
|
+
- :meth:`submit_from`
|
|
707
|
+
- :class:`SubmitThread`
|
|
708
|
+
- :class:`LiveSubmitThread`
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
task_count (int): Count of submitted tasks.
|
|
712
|
+
"""
|
|
713
|
+
with open(path, mode='r', **file_options) as stream:
|
|
714
|
+
return submit_from(stream, queue_config=queue_config, bundlesize=bundlesize,
|
|
715
|
+
bundlewait=bundlewait, template=template, tags=tags)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
APP_NAME = 'hs submit'
|
|
719
|
+
APP_USAGE = f"""\
|
|
720
|
+
Usage:
|
|
721
|
+
{APP_NAME} [-h] [FILE] [-b NUM] [-w SEC] [-t CMD] [--initdb] [--tag TAG [TAG...]]
|
|
722
|
+
Submit tasks from a file.\
|
|
723
|
+
"""
|
|
724
|
+
|
|
725
|
+
APP_HELP = f"""\
|
|
726
|
+
{APP_USAGE}
|
|
727
|
+
|
|
728
|
+
Arguments:
|
|
729
|
+
FILE Path to task file ("-" for <stdin>).
|
|
730
|
+
|
|
731
|
+
Options:
|
|
732
|
+
-t, --template CMD Submit-time template expansion (default: "{DEFAULT_TEMPLATE}").
|
|
733
|
+
-b, --bundlesize NUM Number of lines to buffer (default: {DEFAULT_BUNDLESIZE}).
|
|
734
|
+
-w, --bundlewait SEC Seconds to wait before flushing tasks (default: {DEFAULT_BUNDLEWAIT}).
|
|
735
|
+
--initdb Auto-initialize database.
|
|
736
|
+
--tag TAG... Assign tags as `key:value`.
|
|
737
|
+
-h, --help Show this message and exit.\
|
|
738
|
+
"""
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class SubmitApp(Application):
|
|
742
|
+
"""Submit tasks to the database."""
|
|
743
|
+
|
|
744
|
+
name = APP_NAME
|
|
745
|
+
interface = Interface(APP_NAME, APP_USAGE, APP_HELP)
|
|
746
|
+
|
|
747
|
+
source: IO
|
|
748
|
+
filepath: str
|
|
749
|
+
interface.add_argument('filepath', nargs='?', default='-')
|
|
750
|
+
|
|
751
|
+
bundlesize: int = config.submit.bundlesize
|
|
752
|
+
interface.add_argument('-b', '--bundlesize', type=int, default=bundlesize)
|
|
753
|
+
|
|
754
|
+
bundlewait: int = config.submit.bundlewait
|
|
755
|
+
interface.add_argument('-w', '--bundlewait', type=int, default=bundlewait)
|
|
756
|
+
|
|
757
|
+
template: str = DEFAULT_TEMPLATE
|
|
758
|
+
interface.add_argument('-t', '--template', default=template)
|
|
759
|
+
|
|
760
|
+
auto_initdb: bool = False
|
|
761
|
+
interface.add_argument('--initdb', action='store_true', dest='auto_initdb')
|
|
762
|
+
|
|
763
|
+
tags: Dict[str, JSONValue] = {}
|
|
764
|
+
taglist: List[str] = None
|
|
765
|
+
interface.add_argument('--tag', nargs='*', default=[], dest='taglist')
|
|
766
|
+
|
|
767
|
+
count: int = 0
|
|
768
|
+
|
|
769
|
+
exceptions = {
|
|
770
|
+
**get_shared_exception_mapping(__name__)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
def run(self: SubmitApp) -> None:
|
|
774
|
+
"""Run submit thread."""
|
|
775
|
+
self.submit_all()
|
|
776
|
+
log.info(f'Submitted {self.count} tasks')
|
|
777
|
+
|
|
778
|
+
def submit_all(self: SubmitApp) -> None:
|
|
779
|
+
"""Submit all tasks from source."""
|
|
780
|
+
self.count = submit_from(self.source, template=self.template,
|
|
781
|
+
bundlesize=self.bundlesize, bundlewait=self.bundlewait, tags=self.tags)
|
|
782
|
+
|
|
783
|
+
@staticmethod
|
|
784
|
+
def check_config():
|
|
785
|
+
"""Halt if we are not connected to database."""
|
|
786
|
+
db = config.database.get('file', None) or config.database.get('database', None)
|
|
787
|
+
if config.database.provider == 'sqlite' and db in ('', ':memory:', None):
|
|
788
|
+
raise ConfigurationError('Submitting tasks to in-memory database has no effect')
|
|
789
|
+
|
|
790
|
+
def check_tags(self: SubmitApp) -> None:
|
|
791
|
+
"""Ensure valid tags."""
|
|
792
|
+
self.tags = {} if not self.taglist else Tag.parse_cmdline_list(self.taglist)
|
|
793
|
+
try:
|
|
794
|
+
Task.ensure_valid_tag(self.tags)
|
|
795
|
+
except (ValueError, TypeError) as error:
|
|
796
|
+
raise ArgumentError(str(error)) from error
|
|
797
|
+
|
|
798
|
+
def __enter__(self: SubmitApp) -> SubmitApp:
|
|
799
|
+
"""Open file if not stdin."""
|
|
800
|
+
self.source = sys.stdin if self.filepath == '-' else open(self.filepath, mode='r')
|
|
801
|
+
self.check_config()
|
|
802
|
+
self.check_tags()
|
|
803
|
+
if config.database.provider == 'sqlite' or self.auto_initdb:
|
|
804
|
+
initdb() # Auto-initialize if local sqlite provider
|
|
805
|
+
else:
|
|
806
|
+
checkdb()
|
|
807
|
+
return self
|
|
808
|
+
|
|
809
|
+
def __exit__(self: SubmitApp,
|
|
810
|
+
exc_type: Optional[Type[Exception]],
|
|
811
|
+
exc_val: Optional[Exception],
|
|
812
|
+
exc_tb: Optional[TracebackType]) -> None:
|
|
813
|
+
"""Close file if not stdin."""
|
|
814
|
+
if self.source is not sys.stdin:
|
|
815
|
+
self.source.close()
|