tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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()