#!/usr/bin/env python3 """Research 046: 029's principle 2 ("the commit is the replica") put to its breaking experiment on one Pi. An owner (pure-run --store, with shim.so preloaded) takes `add` messages of entries.aup; every sync of its store ends a cut that the shipper sends over pure link to the follower, which does the same ops under its own directory; the owner's sync returns only once the follower saved the cut (synchronous). Both stores on /dev/shm (what is tested is identity and taking over, not the medium). run.py identity EVENTS EVERY the owner takes EVENTS messages; every EVERY cuts it pauses and the two stores are compared byte for byte (names and contents), and once more at the end run.py takeover RUNS EVENTS RUNS times: the owner (and in every other run its shipper too, as when its machine dies) killed with SIGKILL after a random number of messages up to EVENTS; the follower's store opened by pure-run as the owner: it must open, hold every message the owner said done, none it was not sent, and the entries in order run.py torn RUNS EVENTS [B] as takeover, the whole node killed every time, the cuts sent as records of at most B bytes (512): a kill leaves the follower part of a cut, as a crash leaves a disk run.py price EVENTS the owner's time for EVENTS messages with and without the shim, and what went on the link One JSON line a run (or a sample).""" import json import os import random import select import shutil import signal import socket import struct import subprocess import sys import threading import time from pathlib import Path HERE = Path(__file__).resolve().parent PURE = HERE.parents[2] AUP = Path('/home/eunhhu/work/aup/build/aup') WORK = Path(os.environ.get('RUN_WORK', '/dev/shm/pure-046')) RUN = PURE / 'build' / 'pure-run' ENV = dict(os.environ, PURE_CRYPTO=str(PURE / 'build' / 'pure-crypto.so')) def build(): WORK.mkdir(parents=True, exist_ok=True) cc = ['cc', '-O2', '-std=gnu17', '-Wall', '-Wextra'] subprocess.run(cc + ['-fPIC', '-shared', str(HERE / 'shim.c'), '-ldl', '-pthread', '-o', str(WORK / 'shim.so')], check=True) for name in ('shipper', 'follower'): subprocess.run(cc + [f'-I{PURE / "link"}', str(HERE / f'{name}.c'), str(PURE / 'build' / 'libpure-link.a'), '-lm', '-pthread', '-ldl', '-o', str(WORK / name)], check=True) subprocess.run([str(AUP), 'build', str(HERE / 'entries.aup'), '-o', str(WORK / 'entries.pmod')], check=True) keys = json.loads(subprocess.run([str(WORK / 'follower'), 'keys', str(WORK)], capture_output=True, text=True, check=True, env=ENV).stdout) return keys def free_port(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(('127.0.0.1', 0)) port = s.getsockname()[1] s.close() return port def message(i): # not whole: a logged change takes its 8 bytes, so the log outgrows the list's items within the run return 'message add ' + struct.pack('= stop_after: break if stop_after is None: self.owner.stdin.close() except BrokenPipeError: pass return sent def stop_follower(self): self.follower.send_signal(signal.SIGTERM) out, err = self.follower.communicate(timeout=60) return json.loads(out.strip().splitlines()[-1]) if out.strip() else {'error': err[-500:]} def opened(store): """The store opened by pure-run as its owner: the views (total, summed) it shows, or why it did not open.""" r = subprocess.run([str(RUN), str(WORK / 'entries.pmod'), '--views', '--store', str(store)], input='show\n', capture_output=True, text=True, env=ENV, timeout=120) lines = [l for l in r.stdout.splitlines() if l.startswith('{')] if r.returncode or not lines: return {'opened': False, 'exit': r.returncode, 'stderr': r.stderr[-300:]} shown = json.loads(lines[-1]) return {'opened': True, 'fault': shown.get('fault'), **shown['views']} def summed(n): # every partial sum a multiple of 0.25 below 2^50: exact as a binary64 return float(sum(i % 1000 for i in range(n))) + 0.25 * n def identity(keys, events, every): run = Run(keys, every=every) feeder = threading.Thread(target=run.feed, args=(events,), daemon=True) feeder.start() samples, differing = 0, [] while True: ready, _, _ = select.select([run.paused], [], [], 0.2) if ready: cut = int(os.read(run.paused, 64).split()[0]) diff = stores_differ(run.owner_dir, run.follower_dir) samples += 1 if diff: differing.append({'cut': cut, 'differences': diff[:5]}) os.write(run.resume, b'g') elif run.owner.poll() is not None: break feeder.join() run.reader.join(timeout=10) owner_exit = run.owner.wait() shipper_out = run.shipper.communicate(timeout=60)[0].strip().splitlines() end = stores_differ(run.owner_dir, run.follower_dir) follower = run.stop_follower() stats = json.loads((WORK / 'shim-stats.json').read_text()) files = {p.name: p.stat().st_size for p in sorted(run.owner_dir.iterdir())} print(json.dumps({'run': 'identity', 'events': events, 'done': run.done, 'owner_exit': owner_exit, 'every': every, 'samples': samples, 'samples_differing': len(differing), 'first_differing': differing[:3], 'end_differences': end[:5], 'shim': stats, 'shipper': json.loads(shipper_out[-1]) if shipper_out else None, 'follower': follower, 'owner_files': files, 'follower_opened': opened(run.follower_dir)}), flush=True) def takeover(keys, runs, events, torn=False): for k in range(runs): whole_node = torn or k % 2 == 1 # the shipper dies with the owner stop_after = random.randint(1, events) run = Run(keys) sent = run.feed(events, stop_after=stop_after) deadline = time.time() + 120 while run.done < max(1, sent // 2) and time.time() < deadline and run.owner.poll() is None: time.sleep(0.001) # the owner is working through what was sent: kill it at a moment it does not choose time.sleep(random.random() * 0.05) run.owner.send_signal(signal.SIGKILL) if whole_node: run.shipper.send_signal(signal.SIGKILL) run.owner.wait() done = run.done if not whole_node: run.shipper.communicate(timeout=60) else: run.shipper.wait() time.sleep(1.0) # what was on the way reaches the follower follower = run.stop_follower() took = opened(run.follower_dir) good = (took.get('opened') and took.get('fault') == 'none' and done <= took.get('total', -1) <= sent and took.get('summed') == summed(int(took.get('total', 0)))) print(json.dumps({'run': 'takeover', 'k': k, 'whole_node': whole_node, 'record': os.environ.get('SHIPPER_RECORD'), 'sent': sent, 'owner_said_done': done, 'follower_took': took, 'follower': follower, 'passed': bool(good), 'owner_opened': opened(run.owner_dir)}), flush=True) def price(keys, events): out = {} for shim in (False, True): run = Run(keys, shim=shim) start = time.perf_counter() run.feed(events) run.owner.wait() run.reader.join(timeout=10) seconds = time.perf_counter() - start run.owner.stdin = None shipper = run.shipper if not shim: shipper.terminate() lines = shipper.communicate(timeout=60)[0].strip().splitlines() run.stop_follower() key = 'with_shim' if shim else 'without' out[key] = {'seconds': round(seconds, 3), 'us_per_event': round(1e6 * seconds / events, 1), 'done': run.done} if shim: out['shim'] = json.loads((WORK / 'shim-stats.json').read_text()) out['shipper'] = json.loads(lines[-1]) if lines else None print(json.dumps({'run': 'price', 'events': events, **out}), flush=True) def main(): keys = build() what = sys.argv[1] if what == 'identity': identity(keys, int(sys.argv[2]), int(sys.argv[3])) elif what == 'takeover': takeover(keys, int(sys.argv[2]), int(sys.argv[3])) elif what == 'torn': # the whole node killed, its cuts sent as small records: the follower left with part of one os.environ['SHIPPER_RECORD'] = sys.argv[4] if len(sys.argv) > 4 else '512' ENV['SHIPPER_RECORD'] = os.environ['SHIPPER_RECORD'] takeover(keys, int(sys.argv[2]), int(sys.argv[3]), torn=True) elif what == 'price': price(keys, int(sys.argv[2])) if __name__ == '__main__': main()