registry_download.py (5732B)
1 #!/usr/bin/env python3 2 """Production runner: compiler download latency, native memory and error controls. 3 4 Localhost observations include request parsing, authentication and socket output; 5 these are regression ceilings, not predictions of production network latency. 6 """ 7 import argparse 8 import concurrent.futures 9 import hashlib 10 import json 11 from pathlib import Path 12 import signal 13 import socket 14 import struct 15 import subprocess 16 import tempfile 17 import threading 18 import time 19 from process_rss import ChildRSS 20 from rts_metrics import parse_rts_stats 21 22 23 def main(): 24 parser = argparse.ArgumentParser(description=__doc__) 25 parser.add_argument('runner', type=Path) 26 parser.add_argument('compiler', type=Path) 27 parser.add_argument('--report', type=Path, required=True) 28 args = parser.parse_args() 29 data = args.compiler.read_bytes() 30 digest = hashlib.sha256(data).hexdigest() 31 route = '/_arboricx/bundle/hash/' + digest 32 rows = [] 33 with tempfile.TemporaryDirectory(prefix='registry-download-') as temporary: 34 root = Path(temporary) 35 obj = root / 'store' / 'objects' / digest[:3] / (digest + '.arboricx') 36 obj.parent.mkdir(parents=True) 37 obj.write_bytes(data) 38 stopped = threading.Event() 39 peak = [0] 40 errors = [] 41 with (root / 'server.log').open('w+') as log: 42 import os 43 proc = subprocess.Popen([str(args.runner.resolve())], cwd=root, stdout=log, stderr=log, 44 env=dict(os.environ, GHCRTS='-T -s')) 45 def monitor(): 46 try: 47 meter = ChildRSS(proc) 48 while not stopped.is_set(): 49 value = meter.sample() 50 if value is not None: 51 peak[0] = max(peak[0], value) 52 if value > 128 * 1024: 53 raise AssertionError('128 MiB RSS guard exceeded') 54 stopped.wait(.01) 55 except Exception as error: 56 errors.append(repr(error)) 57 proc.kill() 58 watcher = threading.Thread(target=monitor, daemon=True) 59 watcher.start() 60 def request(path=route, method='GET', status=200, body=data): 61 begin = time.monotonic() 62 with socket.create_connection(('127.0.0.1', 8080), timeout=10) as peer: 63 peer.sendall(f'{method} {path} HTTP/1.1\r\nHost: local\r\n\r\n'.encode()) 64 first = peer.recv(65536) 65 first_byte = time.monotonic() - begin 66 chunks = [first] 67 while chunk := peer.recv(65536): 68 chunks.append(chunk) 69 total = time.monotonic() - begin 70 headers, received = b''.join(chunks).split(b'\r\n\r\n', 1) 71 assert int(headers.split()[1]) == status, headers 72 assert received == (b'' if method == 'HEAD' else body) 73 assert f'Content-Length: {len(body)}'.encode() in headers 74 return dict(first_byte_seconds=first_byte, total_seconds=total) 75 try: 76 for _ in range(100): 77 assert proc.poll() is None, 'server exited' 78 try: 79 request('/_arboricx/health', body=b'OK\n') 80 break 81 except OSError: 82 time.sleep(.1) 83 else: 84 raise AssertionError('startup timeout') 85 for _ in range(3): 86 rows.append(request()) 87 request(method='HEAD') 88 with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: 89 rows.extend(pool.map(lambda _: request(), range(8))) 90 # Corruption is rejected before 200 headers; HEAD authenticates too. 91 obj.write_bytes(b'X' + data[1:]) 92 request(status=404, body=b'Artifact unavailable\n') 93 request(method='HEAD', status=404, body=b'Artifact unavailable\n') 94 obj.write_bytes(data + b'x') 95 request(status=404, body=b'Artifact unavailable\n') 96 obj.write_bytes(data) 97 request('/_arboricx/bundle/hash/' + '0' * 64, status=404, body=b'Artifact unavailable\n') 98 # Reset after headers while the body may still be in flight. 99 with socket.create_connection(('127.0.0.1', 8080), timeout=10) as peer: 100 peer.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024) 101 peer.sendall(f'GET {route} HTTP/1.1\r\n\r\n'.encode()) 102 assert peer.recv(1) == b'H' 103 peer.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack('ii', 1, 0)) 104 request('/_arboricx/health', body=b'OK\n') 105 rows.append(request()) 106 assert list((root / 'store' / 'tmp').iterdir()) == [] 107 assert all(row['total_seconds'] < 5 for row in rows), rows 108 finally: 109 if proc.poll() is None: 110 proc.send_signal(signal.SIGINT) 111 proc.wait(timeout=15) 112 stopped.set() 113 watcher.join() 114 log.seek(0) 115 stats = parse_rts_stats(log.read()) 116 assert not errors, errors 117 assert stats['maximum_live_bytes'] < 16 * 1024 * 1024, stats 118 report = dict(sha256=digest, bytes=len(data), requests=rows, peak_rss_kib=peak[0], rts=stats) 119 args.report.write_text(json.dumps(report, indent=2) + '\n') 120 print(json.dumps(report, indent=2)) 121 122 123 if __name__ == '__main__': 124 main()