#!/usr/bin/env python3
"""A daemon that has already served many peers must still answer the next one.
python3 -B test_daemon_longlived.py
⛔ THE E2E FOR hyper-games #72's WALL. test_handshake_pool.cpp proves the
mechanism in ONE PROCESS — HyperDht kept 16 handshake slots, only a decrypt
FAILURE ever freed one, so the seventeenth handshake a node took part in was
refused with a bare `return`. A unit test is not evidence that the daemon works.
This drives real peers at a real long-lived daemon over the PUBLIC DHT, past
that limit.
⛔ THE CLIENT MUST SPEAK THE DAEMON'S PROTOCOL. The first version of this file
used tools/swarm-probe, which opens `hyperswarm-peer/probe/v1` while the daemon
opens `blender-bridge-session/v1`. They mesh on the topic and never pair a
channel, so no hello could EVER cross and every probe failed for a reason that
had nothing to do with the daemon. Peers here are real session-daemons.
⛔ AND IT REFUSES TO RENDER A VERDICT WITHOUT A BASELINE. That first version
reported "the responder has gone silent, which is the wall's exact symptom"
from a run in which NOT ONE peer had ever connected — a conclusion its data
could not distinguish from "nothing worked". If the early peers do not connect,
this exits INCONCLUSIVE, because the late ones then say nothing at all.
⚠ REQUIRES INTERNET and takes several minutes: every peer is a real DHT
lookup and holepunch.
"""
import os
import re
import subprocess
import sys
import tempfile
import time
HERE = os.path.dirname(os.path.realpath(__file__))
DAEMON = os.path.join(HERE, "session-daemon")
# ⛔ THE JS CONTROL. Our own pass rate means nothing on its own: the public DHT
# has a failure rate of its own, and without measuring it we cannot tell an
# ordinary flaky lookup from a defect of ours. `--js` runs the IDENTICAL driver
# and the IDENTICAL accounting against hyperswarm 4.17.1 instead of
# session-daemon, so the two numbers are comparable by construction.
USE_JS = "--js" in sys.argv
JS_PEER = os.path.join(HERE, "js_peer.js")
NODE_MODULES = os.path.normpath(os.path.join(HERE, "..", "test", "node_modules"))
PEERS = 20 # must exceed the 16-slot pool
BASELINE = 5 # of the first 8, at least this many must connect
CONNECT_TIMEOUT = 45.0
PEERS_RE = re.compile(rb"peers=(\d+)")
EXIT_PASS, EXIT_FAIL, EXIT_INCONCLUSIVE = 0, 1, 2
def spawn(topic, tmp, tag):
logpath = os.path.join(tmp, "%s.log" % tag)
log = open(logpath, "wb")
if USE_JS:
env = dict(os.environ, NODE_PATH=NODE_MODULES)
cmd = ["node", JS_PEER, "--topic", topic]
p = subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT, env=env)
else:
sock = os.path.join(tmp, "%s.sock" % tag)
cmd = [DAEMON, "--topic", topic, "--socket", sock]
p = subprocess.Popen(cmd, stdout=log, stderr=subprocess.STDOUT)
return p, log, logpath
def wait_for(logpath, needle, timeout, proc):
end = time.time() + timeout
while time.time() < end:
if proc.poll() is not None:
return False
try:
with open(logpath, "rb") as fh:
if needle(fh.read()):
return True
except OSError:
pass
time.sleep(0.4)
return False
def saw_a_peer(blob):
return any(int(n) >= 1 for n in PEERS_RE.findall(blob))
def stop(proc, log):
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
log.close()
def main():
target = JS_PEER if USE_JS else DAEMON
if not os.path.isfile(target):
print("missing %s%s" % (target, "" if USE_JS else " — run daemon/build.sh"))
return EXIT_INCONCLUSIVE
topic = "pool-drain-%d" % os.getpid()
tmp = tempfile.mkdtemp(prefix="longlived-")
print("=" * 78)
print("a long-lived %s on the PUBLIC DHT, %d sequential peers"
% ("hyperswarm 4.17.1 (JS CONTROL)" if USE_JS else "session-daemon", PEERS))
print("=" * 78)
print("topic: %s" % topic)
print("logs: %s" % tmp)
host, hostlog, hostlogpath = spawn(topic, tmp, "host")
try:
if not wait_for(hostlogpath, lambda b: b"dht=up" in b, 60, host):
print("\nINCONCLUSIVE: the host daemon never reached dht=up — this "
"machine may have no route to the DHT.")
return EXIT_INCONCLUSIVE
print(" host is on the public DHT\n")
results = []
for i in range(1, PEERS + 1):
p, log, logpath = spawn(topic, tmp, "peer%02d" % i)
ok = wait_for(logpath, saw_a_peer, CONNECT_TIMEOUT, p)
stop(p, log)
results.append(ok)
print(" peer %2d/%d %s" % (i, PEERS, "connected" if ok else "silent"))
sys.stdout.flush()
with open(hostlogpath, "rb") as fh:
hostblob = fh.read()
pool_full = b"handshake pool FULL" in hostblob
early = results[:8]
late = results[16:]
print("")
print(" peers 1-8 connected : %d/%d (baseline)" % (sum(early), len(early)))
print(" peers 17+ connected : %d/%d <- past the old 16 limit" % (sum(late), len(late)))
print(" host logged pool exhaustion: %s" % ("YES" if pool_full else "no"))
# ⛔ BASELINE FIRST. Without it, "the late ones failed" is indistinguishable
# from "nothing ever connected", and reporting the wall from that is how
# the previous version of this file lied.
if sum(early) < BASELINE:
print("\nINCONCLUSIVE: only %d of the first %d peers connected, so the "
"later results carry no information. Fix connectivity or "
"discovery before reading anything into this run."
% (sum(early), len(early)))
return EXIT_INCONCLUSIVE
if USE_JS:
# The control measures the network, it does not gate our code.
print("\nCONTROL: this is hyperswarm's own rate on the public DHT "
"today. Compare it with the session-daemon run before reading "
"anything into ours.")
return EXIT_PASS
if pool_full:
print("\nFAIL: the host exhausted its handshake pool — #72's wall, "
"unfixed.")
return EXIT_FAIL
if not late or sum(late) == 0:
print("\nFAIL: the baseline connected but nothing past the 16th did. "
"The responder goes silent once it has served enough peers.")
return EXIT_FAIL
print("\nPASS: the host still answers past the old 16-handshake limit "
"(%d/%d late peers connected)." % (sum(late), len(late)))
return EXIT_PASS
finally:
stop(host, hostlog)
if __name__ == "__main__":
sys.exit(main())