nodeocc 1.0.26__tar.gz → 1.0.27__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.
- {nodeocc-1.0.26 → nodeocc-1.0.27}/PKG-INFO +5 -1
- {nodeocc-1.0.26 → nodeocc-1.0.27}/README.md +4 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/pyproject.toml +1 -1
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/controller/controller.py +7 -21
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/nodeocc.egg-info/PKG-INFO +5 -1
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/nodeocc.egg-info/SOURCES.txt +1 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/readers/slurmreader.py +5 -1
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/view/curses_multiwindow.py +101 -3
- nodeocc-1.0.27/src/view/transport.py +46 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/setup.cfg +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/setup.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/__init__.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/model/infrastructure.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/model/job.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/nodeocc.egg-info/dependency_links.txt +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/nodeocc.egg-info/entry_points.txt +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/nodeocc.egg-info/requires.txt +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/nodeocc.egg-info/top_level.txt +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/readers/compute_wait_time.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/view/__init__.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/view/slurm_list.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/view/slurm_viz.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/view/styles.py +0 -0
- {nodeocc-1.0.26 → nodeocc-1.0.27}/src/view/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nodeocc
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.27
|
|
4
4
|
Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
|
|
5
5
|
Author: Matteo Boschini
|
|
6
6
|
Maintainer: Lorenzo Bonicelli, Aniello Panariello
|
|
@@ -31,4 +31,8 @@ Contents:
|
|
|
31
31
|
|
|
32
32
|
Run it with: `python controller/controller.py`
|
|
33
33
|
|
|
34
|
+
Press `D` in the interactive view to open or close the debug screen. It shows
|
|
35
|
+
the local role, master PID and port, snapshot status, and local package
|
|
36
|
+
versions. Use the up and down keys to scroll.
|
|
37
|
+
|
|
34
38
|
Requirements: pandas, pycurses.
|
|
@@ -13,4 +13,8 @@ Contents:
|
|
|
13
13
|
|
|
14
14
|
Run it with: `python controller/controller.py`
|
|
15
15
|
|
|
16
|
+
Press `D` in the interactive view to open or close the debug screen. It shows
|
|
17
|
+
the local role, master PID and port, snapshot status, and local package
|
|
18
|
+
versions. Use the up and down keys to scroll.
|
|
19
|
+
|
|
16
20
|
Requirements: pandas, pycurses.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nodeocc"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.27"
|
|
8
8
|
authors = [{ name = "Matteo Boschini" }]
|
|
9
9
|
maintainers = [{ name = "Lorenzo Bonicelli" }, { name = "Aniello Panariello" }]
|
|
10
10
|
description = "TUI application for viewing the status of GPU allocations on a Slurm cluster"
|
|
@@ -30,6 +30,7 @@ from model.infrastructure import Infrastructure
|
|
|
30
30
|
from model.job import Job
|
|
31
31
|
import asyncio
|
|
32
32
|
import importlib.metadata
|
|
33
|
+
from view.transport import send as send_datagram, receive as receive_datagram
|
|
33
34
|
|
|
34
35
|
program_name = 'nodeocc'
|
|
35
36
|
version_number = importlib.metadata.version(program_name)
|
|
@@ -121,7 +122,7 @@ def send_msg(instance: Singleton, msg, n_extra_msgs=0, is_extra=False):
|
|
|
121
122
|
msg = (str(msg_len) + begin_delim).encode('utf-8') + msg + END_DELIM_ENCODED
|
|
122
123
|
instance.timeme(f"- msg encoded with len {msg_len}")
|
|
123
124
|
|
|
124
|
-
instance.sock
|
|
125
|
+
send_datagram(instance.sock, instance.port, msg)
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
def update_data_master(instance: Singleton):
|
|
@@ -139,24 +140,11 @@ def update_data_master(instance: Singleton):
|
|
|
139
140
|
running_jobs = [j.to_nested_dict() for j in jobs if j.state == 'CG' or j.state == 'R']
|
|
140
141
|
queue_jobs = [j.to_nested_dict() for j in jobs if j.state != 'R' and j.state != 'CG']
|
|
141
142
|
queue_jobs = sorted(queue_jobs, key=lambda x: x['priority'], reverse=True)
|
|
142
|
-
maxlen = min(JOB_LIMIT_PER_MSG, len(running_jobs) + len(queue_jobs))
|
|
143
|
-
|
|
144
|
-
queue_jobs, extra_queue_jobs = queue_jobs[:maxlen - len(running_jobs)], queue_jobs[maxlen - len(running_jobs):]
|
|
145
143
|
cur_timestamp = time.time()
|
|
146
144
|
msg = json.dumps({'inf': inf.to_nested_dict(), 'jobs': running_jobs + queue_jobs, 'ts': str(cur_timestamp)})
|
|
147
|
-
|
|
148
|
-
N_EXTRA_MSGS = (len(extra_queue_jobs) // JOB_LIMIT_PER_MSG) + 1 if len(extra_queue_jobs) > 0 else 0
|
|
149
|
-
send_msg(instance, msg, n_extra_msgs=N_EXTRA_MSGS)
|
|
145
|
+
send_msg(instance, msg)
|
|
150
146
|
instance.timeme("- main broadcast")
|
|
151
147
|
|
|
152
|
-
if extra_queue_jobs:
|
|
153
|
-
# get batches of JOB_LIMIT_PER_MSG jobs
|
|
154
|
-
for job_idx in range(N_EXTRA_MSGS):
|
|
155
|
-
i = job_idx * JOB_LIMIT_PER_MSG
|
|
156
|
-
msg = json.dumps({'inf': inf.to_nested_dict(), 'jobs': extra_queue_jobs[i:i + JOB_LIMIT_PER_MSG], 'ts': str(cur_timestamp)})
|
|
157
|
-
send_msg(instance, msg, is_extra=True, n_extra_msgs=N_EXTRA_MSGS - job_idx - 1)
|
|
158
|
-
instance.timeme(f"- extra broadcast #{job_idx+1}")
|
|
159
|
-
|
|
160
148
|
return inf, jobs
|
|
161
149
|
|
|
162
150
|
|
|
@@ -178,7 +166,7 @@ async def get_data_slave(instance):
|
|
|
178
166
|
try:
|
|
179
167
|
# listen for data from master
|
|
180
168
|
# instance.sock.settimeout(6.5)
|
|
181
|
-
data
|
|
169
|
+
data = receive_datagram(instance.sock, MAX_BUF)
|
|
182
170
|
decoded_data = data.decode('utf-8', errors='replace')
|
|
183
171
|
if BEGIN_DELIM not in decoded_data:
|
|
184
172
|
if EXTRA_MSG_BEGIN_DELIM in decoded_data:
|
|
@@ -198,7 +186,7 @@ async def get_data_slave(instance):
|
|
|
198
186
|
total_msg_n = n_msgs_remaining
|
|
199
187
|
msg_store = {i: [] for i in range(total_msg_n)}
|
|
200
188
|
while n_msgs_remaining > 0:
|
|
201
|
-
data
|
|
189
|
+
data = receive_datagram(instance.sock, MAX_BUF)
|
|
202
190
|
decoded_extra = data.decode('utf-8', errors='replace')
|
|
203
191
|
if EXTRA_MSG_BEGIN_DELIM not in decoded_extra:
|
|
204
192
|
instance.log("Received non-extra packet while waiting for extra packets, skipping")
|
|
@@ -239,8 +227,6 @@ async def get_data_slave(instance):
|
|
|
239
227
|
if n_timeout > TIMEOUT_MAX:
|
|
240
228
|
try_open_socket_as_slave(instance, force=True)
|
|
241
229
|
|
|
242
|
-
time.sleep(random.randint(100, 10000) / 1000)
|
|
243
|
-
|
|
244
230
|
except Exception as e:
|
|
245
231
|
instance.err(f"Exception while receiving data from master: {e}")
|
|
246
232
|
instance.err(traceback.format_exc())
|
|
@@ -272,7 +258,6 @@ async def get_all():
|
|
|
272
258
|
if orig_instance.is_master:
|
|
273
259
|
inf, jobs = update_data_master(orig_instance)
|
|
274
260
|
|
|
275
|
-
time.sleep(5)
|
|
276
261
|
else:
|
|
277
262
|
# loop = asyncio.get_event_loop()
|
|
278
263
|
orig_instance.timeme(f"- listening for data")
|
|
@@ -345,6 +330,7 @@ def _main():
|
|
|
345
330
|
orig_instance.log(f"Starting master daemon")
|
|
346
331
|
|
|
347
332
|
while True:
|
|
333
|
+
cycle_start = time.monotonic()
|
|
348
334
|
orig_instance.timeme(f"Updating...")
|
|
349
335
|
try:
|
|
350
336
|
update_data_master(orig_instance)
|
|
@@ -352,7 +338,7 @@ def _main():
|
|
|
352
338
|
orig_instance.err(f"Exception: {e}")
|
|
353
339
|
orig_instance.err(traceback.format_exc())
|
|
354
340
|
|
|
355
|
-
time.sleep(5)
|
|
341
|
+
time.sleep(max(0, 5 - (time.monotonic() - cycle_start)))
|
|
356
342
|
|
|
357
343
|
else:
|
|
358
344
|
# configure singleton
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nodeocc
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.27
|
|
4
4
|
Summary: TUI application for viewing the status of GPU allocations on a Slurm cluster
|
|
5
5
|
Author: Matteo Boschini
|
|
6
6
|
Maintainer: Lorenzo Bonicelli, Aniello Panariello
|
|
@@ -31,4 +31,8 @@ Contents:
|
|
|
31
31
|
|
|
32
32
|
Run it with: `python controller/controller.py`
|
|
33
33
|
|
|
34
|
+
Press `D` in the interactive view to open or close the debug screen. It shows
|
|
35
|
+
the local role, master PID and port, snapshot status, and local package
|
|
36
|
+
versions. Use the up and down keys to scroll.
|
|
37
|
+
|
|
34
38
|
Requirements: pandas, pycurses.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import re
|
|
3
|
+
from collections import defaultdict
|
|
3
4
|
from io import StringIO
|
|
4
5
|
|
|
5
6
|
import pandas as pd
|
|
@@ -377,8 +378,11 @@ def read_jobs():
|
|
|
377
378
|
line['NICE']
|
|
378
379
|
), axis=1).tolist()
|
|
379
380
|
|
|
381
|
+
joblets_by_job = defaultdict(list)
|
|
382
|
+
for joblet in joblets:
|
|
383
|
+
joblets_by_job[joblet.jobid].append(joblet)
|
|
380
384
|
for j in jobs:
|
|
381
|
-
j.joblets = [
|
|
385
|
+
j.joblets = joblets_by_job[j.jobid]
|
|
382
386
|
return jobs, joblets
|
|
383
387
|
|
|
384
388
|
|
|
@@ -14,6 +14,9 @@ from logging.handlers import RotatingFileHandler
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from view import update_views
|
|
16
16
|
import stat
|
|
17
|
+
import sys
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from importlib import metadata
|
|
17
20
|
|
|
18
21
|
a_filter_values = [None, 'me', 'prod', 'stud', 'cvcs']
|
|
19
22
|
|
|
@@ -33,6 +36,7 @@ BUTTON_ACTIONS = {
|
|
|
33
36
|
'show_prio': [ord('p')],
|
|
34
37
|
'refresh': [ord('y')],
|
|
35
38
|
'info': [ord('i')],
|
|
39
|
+
'debug_view': [ord('D')],
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
RIGHT_WIDTH_GPU = 33
|
|
@@ -53,7 +57,10 @@ def is_nodeocc_process(pid):
|
|
|
53
57
|
try:
|
|
54
58
|
cmdline = ' '.join(psutil.Process(int(pid)).cmdline())
|
|
55
59
|
return ('nodeocc' in cmdline) or ('controller.py' in cmdline)
|
|
56
|
-
except
|
|
60
|
+
except psutil.AccessDenied:
|
|
61
|
+
# Other users' command lines may be hidden on shared login nodes.
|
|
62
|
+
return psutil.pid_exists(int(pid))
|
|
63
|
+
except (psutil.Error, ValueError):
|
|
57
64
|
return False
|
|
58
65
|
|
|
59
66
|
|
|
@@ -72,7 +79,10 @@ def process_has_udp_port(pid, port):
|
|
|
72
79
|
for conn in proc.connections(kind='udp'):
|
|
73
80
|
if conn.laddr and conn.laddr.port == int(port):
|
|
74
81
|
return True
|
|
75
|
-
except
|
|
82
|
+
except psutil.AccessDenied:
|
|
83
|
+
# Socket ownership is commonly visible only to the process owner.
|
|
84
|
+
return psutil.pid_exists(int(pid))
|
|
85
|
+
except (psutil.Error, ValueError):
|
|
76
86
|
return False
|
|
77
87
|
return False
|
|
78
88
|
|
|
@@ -174,6 +184,11 @@ class Singleton:
|
|
|
174
184
|
self.rens = ""
|
|
175
185
|
|
|
176
186
|
self.fetch_fn = None
|
|
187
|
+
self.debug_view = False
|
|
188
|
+
self.debug_offset = 0
|
|
189
|
+
self.last_success = None
|
|
190
|
+
self.last_fetch_duration = None
|
|
191
|
+
self.last_error = None
|
|
177
192
|
self.view_mode = 'gpu'
|
|
178
193
|
self.job_id_type = 'agg'
|
|
179
194
|
|
|
@@ -324,6 +339,7 @@ class Singleton:
|
|
|
324
339
|
self._ctime = _ntime
|
|
325
340
|
|
|
326
341
|
def err(self, msg):
|
|
342
|
+
self.last_error = str(msg)
|
|
327
343
|
if self.args.debug or self.args.master_only:
|
|
328
344
|
logging.error(msg)
|
|
329
345
|
|
|
@@ -337,11 +353,14 @@ class Singleton:
|
|
|
337
353
|
if self.fetch_fn is not None:
|
|
338
354
|
inf, jobs, avg_wait_time = await self.fetch_fn() # a_filter_values[self.a_filter])
|
|
339
355
|
master_dead = inf is None
|
|
356
|
+
if not master_dead:
|
|
357
|
+
self.last_success = time.time()
|
|
340
358
|
self.inf = inf if inf is not None else self.inf
|
|
341
359
|
self.jobs = jobs if jobs is not None else self.jobs
|
|
342
360
|
self.avg_wait_time = avg_wait_time if avg_wait_time is not None else self.avg_wait_time
|
|
343
361
|
|
|
344
362
|
_delta_t = time.time() - _ctime
|
|
363
|
+
self.last_fetch_duration = _delta_t
|
|
345
364
|
self.log(f"Fetch took {_delta_t:.2f} seconds")
|
|
346
365
|
return master_dead
|
|
347
366
|
|
|
@@ -459,6 +478,18 @@ def handle_keys(stdscr, instance):
|
|
|
459
478
|
|
|
460
479
|
instance.mouse_state = {}
|
|
461
480
|
|
|
481
|
+
if k in BUTTON_ACTIONS['debug_view']:
|
|
482
|
+
instance.debug_view = not instance.debug_view
|
|
483
|
+
instance.debug_offset = 0
|
|
484
|
+
stdscr.clear()
|
|
485
|
+
return
|
|
486
|
+
if instance.debug_view:
|
|
487
|
+
if k in BUTTON_ACTIONS['down_1']:
|
|
488
|
+
instance.debug_offset += 1
|
|
489
|
+
elif k in BUTTON_ACTIONS['up_1']:
|
|
490
|
+
instance.debug_offset = max(0, instance.debug_offset - 1)
|
|
491
|
+
return
|
|
492
|
+
|
|
462
493
|
# process input
|
|
463
494
|
# RIGHT
|
|
464
495
|
if k in BUTTON_ACTIONS['right_1']:
|
|
@@ -507,7 +538,70 @@ def handle_keys(stdscr, instance):
|
|
|
507
538
|
instance.voff -= ((height - 4) // 2) + 1
|
|
508
539
|
|
|
509
540
|
|
|
541
|
+
def debug_lines(instance):
|
|
542
|
+
"""Collect local diagnostics without querying Slurm or the network."""
|
|
543
|
+
if not hasattr(instance, '_package_versions'):
|
|
544
|
+
instance._package_versions = {}
|
|
545
|
+
for name in ('nodeocc', 'pandas', 'numpy', 'psutil'):
|
|
546
|
+
try:
|
|
547
|
+
instance._package_versions[name] = metadata.version(name)
|
|
548
|
+
except metadata.PackageNotFoundError:
|
|
549
|
+
instance._package_versions[name] = 'not installed'
|
|
550
|
+
port = getattr(instance, 'port', None)
|
|
551
|
+
master_pid = str(os.getpid()) if instance.is_master else 'unknown'
|
|
552
|
+
try:
|
|
553
|
+
for path in instance.get_port_files():
|
|
554
|
+
if port_from_portfile(path) == port:
|
|
555
|
+
master_pid = Path(path).read_text().strip() or 'unknown'
|
|
556
|
+
break
|
|
557
|
+
except OSError:
|
|
558
|
+
pass
|
|
559
|
+
try:
|
|
560
|
+
master_owner = psutil.Process(int(master_pid)).username()
|
|
561
|
+
except (ValueError, psutil.Error, OSError, KeyError):
|
|
562
|
+
master_owner = 'unknown'
|
|
563
|
+
last_success = (datetime.fromtimestamp(instance.last_success).strftime('%Y-%m-%d %H:%M:%S')
|
|
564
|
+
if instance.last_success else 'never')
|
|
565
|
+
duration = (f'{instance.last_fetch_duration:.2f} s'
|
|
566
|
+
if instance.last_fetch_duration is not None else 'not available')
|
|
567
|
+
lines = [
|
|
568
|
+
'Connection',
|
|
569
|
+
f' Host: {socket.getfqdn()}',
|
|
570
|
+
f' Role: {"master" if instance.is_master else "client"}',
|
|
571
|
+
f' Master PID: {master_pid}',
|
|
572
|
+
f' Master owner: {master_owner}',
|
|
573
|
+
f' Master UDP port: {port if port is not None else "unknown"}',
|
|
574
|
+
f' Base path: {instance.basepath}',
|
|
575
|
+
'', 'Snapshot',
|
|
576
|
+
f' Last successful update: {last_success}',
|
|
577
|
+
f' Last {"update" if instance.is_master else "receive wait"}: {duration}',
|
|
578
|
+
f' Jobs: {len(instance.jobs) if instance.jobs is not None else "unknown"}',
|
|
579
|
+
f' Nodes: {len(instance.inf.nodes) if instance.inf is not None else "unknown"}',
|
|
580
|
+
f' Last error: {instance.last_error or "none recorded"}',
|
|
581
|
+
'', 'Environment',
|
|
582
|
+
f' Python: {sys.version.split()[0]}',
|
|
583
|
+
f' Executable: {sys.executable}',
|
|
584
|
+
f' Process PID: {os.getpid()}',
|
|
585
|
+
' Package versions (local environment):',
|
|
586
|
+
]
|
|
587
|
+
lines.extend(f' {name}: {version}' for name, version in instance._package_versions.items())
|
|
588
|
+
return lines
|
|
589
|
+
|
|
590
|
+
|
|
510
591
|
def update_screen(stdscr, instance):
|
|
592
|
+
if instance.debug_view:
|
|
593
|
+
lines, columns = stdscr.getmaxyx()
|
|
594
|
+
content = debug_lines(instance)
|
|
595
|
+
instance.debug_offset = min(instance.debug_offset, max(0, len(content) - max(1, lines - 2)))
|
|
596
|
+
stdscr.erase()
|
|
597
|
+
try:
|
|
598
|
+
stdscr.addnstr(0, 0, 'DEBUG [D: close] [up/down: scroll] [Q: quit]', max(0, columns - 1))
|
|
599
|
+
for row, line in enumerate(content[instance.debug_offset:instance.debug_offset + max(0, lines - 2)], 1):
|
|
600
|
+
stdscr.addnstr(row, 0, line, max(0, columns - 1))
|
|
601
|
+
except curses.error:
|
|
602
|
+
pass
|
|
603
|
+
stdscr.refresh()
|
|
604
|
+
return
|
|
511
605
|
update_views(stdscr, instance, a_filter_values[instance.a_filter])
|
|
512
606
|
xoffset = 0
|
|
513
607
|
instance.xoffset = xoffset
|
|
@@ -584,7 +678,9 @@ def update_screen(stdscr, instance):
|
|
|
584
678
|
stdscr.addstr(0, left_width + 5, 'INFO', curses.color_pair(2) | (curses.A_REVERSE if instance.view_mode == 'info' else 0))
|
|
585
679
|
stdscr.addstr(0, left_width + 9, ']', curses.color_pair(2))
|
|
586
680
|
instance.add_button(0, left_width + 2, '[I:INFO]', BUTTON_ACTIONS['info'][0])
|
|
587
|
-
stdscr.addstr(0, left_width +
|
|
681
|
+
stdscr.addstr(0, left_width + 11, '[D:DEBUG]', curses.color_pair(2))
|
|
682
|
+
instance.add_button(0, left_width + 11, '[D:DEBUG]', BUTTON_ACTIONS['debug_view'][0])
|
|
683
|
+
stdscr.addstr(0, left_width + 20, '─' * max(0, columns - 15 - left_width - 20), curses.color_pair(2))
|
|
588
684
|
stdscr.addstr(0, columns - 15, '[G:', curses.color_pair(2))
|
|
589
685
|
stdscr.addstr(0, columns - 12, 'GPU', curses.color_pair(2) | (curses.A_REVERSE if instance.view_mode == 'gpu' else 0))
|
|
590
686
|
stdscr.addstr(0, columns - 12 + 3, 'RAM', curses.color_pair(2) | (curses.A_REVERSE if instance.view_mode == 'ram' else 0))
|
|
@@ -656,6 +752,8 @@ async def update_screen_info(stdscr, instance: Singleton):
|
|
|
656
752
|
try_open_socket_as_slave(instance)
|
|
657
753
|
|
|
658
754
|
update_screen(stdscr, instance)
|
|
755
|
+
if instance.is_master:
|
|
756
|
+
await asyncio.sleep(max(0, 5 - (instance.last_fetch_duration or 0)))
|
|
659
757
|
|
|
660
758
|
|
|
661
759
|
async def wait_first(futures, instance):
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Fragment large broadcast messages to fit in UDP datagrams."""
|
|
2
|
+
import uuid
|
|
3
|
+
import zlib
|
|
4
|
+
|
|
5
|
+
PREFIX = b'nodeocc-fragment:'
|
|
6
|
+
CHUNK_SIZE = 60000
|
|
7
|
+
COMPRESSED = b'nodeocc-zlib:'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def send(sock, port, data):
|
|
11
|
+
compressed = COMPRESSED + zlib.compress(data, level=1)
|
|
12
|
+
if len(compressed) < len(data):
|
|
13
|
+
data = compressed
|
|
14
|
+
if len(data) <= CHUNK_SIZE:
|
|
15
|
+
sock.sendto(data, ('<broadcast>', port))
|
|
16
|
+
return
|
|
17
|
+
message_id = uuid.uuid4().hex.encode('ascii')
|
|
18
|
+
count = (len(data) + CHUNK_SIZE - 1) // CHUNK_SIZE
|
|
19
|
+
for index in range(count):
|
|
20
|
+
header = PREFIX + message_id + b':' + str(index).encode() + b':' + str(count).encode() + b':'
|
|
21
|
+
sock.sendto(header + data[index * CHUNK_SIZE:(index + 1) * CHUNK_SIZE], ('<broadcast>', port))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def receive(sock, max_buf):
|
|
25
|
+
fragments = {}
|
|
26
|
+
while True:
|
|
27
|
+
data, _ = sock.recvfrom(max_buf)
|
|
28
|
+
if not data.startswith(PREFIX):
|
|
29
|
+
return zlib.decompress(data[len(COMPRESSED):]) if data.startswith(COMPRESSED) else data
|
|
30
|
+
try:
|
|
31
|
+
message_id, index, count, chunk = data[len(PREFIX):].split(b':', 3)
|
|
32
|
+
index, count = int(index), int(count)
|
|
33
|
+
if count < 2 or count > 10000 or not 0 <= index < count:
|
|
34
|
+
continue
|
|
35
|
+
except ValueError:
|
|
36
|
+
continue
|
|
37
|
+
# Retain only one message, so lost fragments cannot accumulate forever.
|
|
38
|
+
if message_id not in fragments:
|
|
39
|
+
fragments = {message_id: (count, {})}
|
|
40
|
+
expected, parts = fragments[message_id]
|
|
41
|
+
if count != expected:
|
|
42
|
+
continue
|
|
43
|
+
parts[index] = chunk
|
|
44
|
+
if len(parts) == count:
|
|
45
|
+
data = b''.join(parts[i] for i in range(count))
|
|
46
|
+
return zlib.decompress(data[len(COMPRESSED):]) if data.startswith(COMPRESSED) else data
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|