aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments
diff options
context:
space:
mode:
Diffstat (limited to 'research/experiments')
-rw-r--r--research/experiments/019-idle-links/links.c42
-rw-r--r--research/experiments/019-idle-links/run.py57
2 files changed, 88 insertions, 11 deletions
diff --git a/research/experiments/019-idle-links/links.c b/research/experiments/019-idle-links/links.c
index d6cc28b..7c726c2 100644
--- a/research/experiments/019-idle-links/links.c
+++ b/research/experiments/019-idle-links/links.c
@@ -177,17 +177,22 @@ static PureLinkSender *sender_of(size_t device) {
return s;
}
-/* What a sender has to send, to its socket. */
-static void send_packets(PureLinkSender *s, int fd, uint64_t now) {
+/* What a sender has to send, to its socket: false when the system said
+ * there is no way there (no route, nobody at the port). */
+static bool send_packets(PureLinkSender *s, int fd, uint64_t now) {
unsigned char packet[PURE_LINK_DATAGRAM_MOST];
uint64_t number;
+ bool way = true;
for (size_t size; (size = pure_link_sender_packet(s, packet, false, now, &number));) {
- if (send(fd, packet, size, 0) < 0 && (errno == EAGAIN || errno == ENOBUFS)) {
+ ssize_t sent = send(fd, packet, size, 0);
+ if (sent < 0 && (errno == EAGAIN || errno == ENOBUFS)) {
pure_link_sender_refused(s, number);
break;
}
+ if (sent < 0 && (errno == ECONNREFUSED || errno == EHOSTUNREACH || errno == ENETUNREACH)) way = false;
++sends;
}
+ return way;
}
/* Answers waiting on fd, to the sender. */
@@ -293,9 +298,32 @@ typedef struct {
uint64_t made, stored; /* records made and stored: numbers 1..made, 1..stored */
uint64_t next_make; /* when the next record is made */
uint64_t due; /* its sender's next timer (0: none), as it was when last touched */
+ unsigned looked; /* its probes unanswered when it last looked for the node */
+ bool nowhere; /* a send failed: the system saw no way there */
uint64_t made_at[OUTBOX]; /* when each record not stored was made (ns), by number % OUTBOX */
} Device;
+/* A device whose probes go unanswered three times in a row, or whose send
+ * the system said had no way there, looks for the node again, as
+ * link/link.c does: its name looked up, and a new socket connected there in
+ * place of the one before (before the next packet goes). */
+static void look(Device *d, int poller, size_t k, const char *host, const char *port) {
+ struct addrinfo hints = {.ai_family = AF_UNSPEC, .ai_socktype = SOCK_DGRAM}, *found;
+ if (getaddrinfo(host, port, &hints, &found)) return;
+ int fd = socket(found->ai_family, SOCK_DGRAM | SOCK_NONBLOCK, 0);
+ bool ok = fd >= 0 && !connect(fd, found->ai_addr, found->ai_addrlen);
+ freeaddrinfo(found);
+ if (!ok) {
+ if (fd >= 0) close(fd);
+ return;
+ }
+ epoll_ctl(poller, EPOLL_CTL_DEL, d->fd, NULL);
+ close(d->fd);
+ d->fd = fd;
+ struct epoll_event e = {.events = EPOLLIN, .data.u64 = k};
+ epoll_ctl(poller, EPOLL_CTL_ADD, d->fd, &e);
+}
+
static int run_all(char **argv) {
size_t first = strtoull(argv[8], NULL, 10), count = strtoull(argv[9], NULL, 10);
uint64_t period = strtoull(argv[10], NULL, 10) * 1000000u, seconds = strtoull(argv[11], NULL, 10);
@@ -342,8 +370,9 @@ static int run_all(char **argv) {
++stored;
}
if (heard > d->stored) d->stored = heard;
- send_packets(d->sender, d->fd, now); /* what the answer let go */
+ d->nowhere |= !send_packets(d->sender, d->fd, now); /* what the answer let go */
d->due = pure_link_sender_due(d->sender);
+ if (!pure_link_sender_probes(d->sender)) d->looked = 0;
}
if (now - last_scan < 5000000u) continue; /* the devices' clocks, every 5 ms */
last_scan = now;
@@ -361,7 +390,10 @@ static int run_all(char **argv) {
* sender is touched (so ten thousand at rest cost a glance each). */
if (d->due && d->due <= now) pure_link_sender_tick(d->sender, now), more = true;
if (more) {
- send_packets(d->sender, d->fd, now);
+ unsigned probes = pure_link_sender_probes(d->sender);
+ if ((probes >= 3 && probes != d->looked) || d->nowhere) look(d, poller, k, argv[5], argv[6]);
+ d->looked = probes;
+ d->nowhere = !send_packets(d->sender, d->fd, now);
d->due = pure_link_sender_due(d->sender);
}
}
diff --git a/research/experiments/019-idle-links/run.py b/research/experiments/019-idle-links/run.py
index 15ae2de..7f2647d 100644
--- a/research/experiments/019-idle-links/run.py
+++ b/research/experiments/019-idle-links/run.py
@@ -22,7 +22,11 @@ send it until it is stored; at --kill-at seconds the receiver is killed
both sides before and after, the X25519 made (none when the node
remembers its secrets; --no-memo forgets them), and that every order made
was applied once. With --quic too, the same for quinn clients, which
-connect again when their connection is lost.
+connect again when their connection is lost. With --move too (M3), in
+namespaces of its own (unshare -rmn), the devices send to receiver.test
+(127.0.0.2), and the node is started again elsewhere: its store, key and
+memo copied, 127.0.0.2 made unreachable, the name pointed at 127.0.0.3;
+the devices look the name up again when their probes go unanswered.
With --first-contact DELAYS (M4), for each one-way delay in ms, in a
network namespace of its own (unshare -rn) whose loopback delays UDP:
@@ -236,9 +240,10 @@ def recovery(timeline, killed_ms):
return {'steady_per_100ms': round(steady, 1), 'silent_100ms_steps': silent, 'caught_up_ms': None}
-def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo):
+def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo, host='127.0.0.1', move=None):
"""M2 for Pure: n devices send a record every period_ms; the node is
- killed (-9) at kill_at seconds and started again at once."""
+ killed (-9) at kill_at seconds and started again at once (M3, with move:
+ elsewhere, move(args) giving its arguments there)."""
senders = work / f'senders-{n}'
with open(f'{keys}.senders') as source, open(senders, 'w') as out:
for _, line in zip(range(n), source):
@@ -249,7 +254,7 @@ def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo):
args = [ROOT / 'build/pure-serve', '--module', module, '--store', store, '--port', port, '--key', work / 'node.key',
'--link-port', link_port, '--senders', senders]
node, info = start(args)
- generator = subprocess.Popen([str(a) for a in [ROOT / 'build/links', 'run', keys, module, 'order', '127.0.0.1',
+ generator = subprocess.Popen([str(a) for a in [ROOT / 'build/links', 'run', keys, module, 'order', host,
link_port, info['public_key'], 0, n, period_ms, seconds]],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for line in generator.stderr: # progress, a line a second
@@ -267,8 +272,11 @@ def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo):
killed_ms = int((t + killed - seen) * 1000) # on the generator's clock
if not memo:
(work / 'node.key.links').unlink()
+ if move:
+ args = move(args)
node, _ = start(args)
- row = {'devices': n, 'period_ms': period_ms, 'memo': memo, 'restart_ms': round((time.monotonic() - killed) * 1000, 1),
+ row = {'devices': n, 'period_ms': period_ms, 'memo': memo, 'moved': bool(move),
+ 'restart_ms': round((time.monotonic() - killed) * 1000, 1),
'steady_cpu_ms_per_s': round(steady_cpu, 1), 'senders_steady_cpu_ms_per_s': round(senders_steady, 1)}
cpu_start, senders_start = cpu_ms(node.pid), cpu_ms(generator.pid)
time.sleep(10)
@@ -290,6 +298,28 @@ def restart_pure(n, work, keys, module, period_ms, seconds, kill_at, memo):
return row
+def mover(hosts, ip):
+ """M3: the node's store, key and memo carried to another machine (copied,
+ in the namespace of --move), its old address (127.0.0.2) made
+ unreachable, and the name the devices send to (receiver.test) pointed at
+ the new one (127.0.0.3)."""
+ import shutil
+
+ def move(args):
+ args = list(args)
+ store, key = Path(args[args.index('--store') + 1]), Path(args[args.index('--key') + 1])
+ moved_store, moved_key = store.with_name(store.name + '-moved'), key.with_name('moved-' + key.name)
+ shutil.copytree(store, moved_store)
+ shutil.copy(key, moved_key)
+ if Path(f'{key}.links').exists():
+ shutil.copy(f'{key}.links', f'{moved_key}.links')
+ subprocess.run([ip, 'route', 'add', 'unreachable', '127.0.0.2/32', 'table', 'local'], check=True)
+ hosts.write_text('127.0.0.1 localhost\n127.0.0.3 receiver.test\n')
+ args[args.index('--store') + 1], args[args.index('--key') + 1] = moved_store, moved_key
+ return args
+ return move
+
+
def restart_quic(n, work, period_ms, seconds, kill_at):
"""M2 for quinn: the same load; the service is killed (-9) and started
again on its port, with the same key (so it resets the old connections)."""
@@ -539,6 +569,9 @@ def main():
parser.add_argument('--quic', action='store_true', help='measure the quinn service instead (quic/)')
parser.add_argument('--restart', action='store_true', help='M2: kill -9 the receiver under load (with --quic, quinn)')
parser.add_argument('--no-memo', action='store_true', help='M2: the node forgets the secrets it remembered')
+ parser.add_argument('--move', action='store_true',
+ help='M3 (with --restart): started again on another machine, the store carried there (a '
+ 'namespace of its own: unshare -rmn)')
parser.add_argument('--period-ms', type=int, default=5000)
parser.add_argument('--seconds', type=int, default=40)
parser.add_argument('--kill-at', type=int, default=20)
@@ -551,6 +584,17 @@ def main():
parser.add_argument('--rounds', type=int, default=1)
args = parser.parse_args()
counts = [int(c) for c in args.counts.split(',')]
+ if args.move and not os.environ.get('PURE_MOVE_HOSTS'): # in namespaces of its own: its network, its /etc/hosts
+ with tempfile.TemporaryDirectory(prefix='pure-m3-', dir=args.store) as directory:
+ hosts = Path(directory) / 'hosts'
+ hosts.write_text('127.0.0.1 localhost\n127.0.0.2 receiver.test\n')
+ done = subprocess.run(['unshare', '-rmn', sys.executable] + sys.argv, env={**os.environ, 'PURE_MOVE_HOSTS': str(hosts)})
+ sys.exit(done.returncode)
+ if args.move:
+ import shutil
+ ip = shutil.which('ip') or '/usr/sbin/ip'
+ subprocess.run([ip, 'link', 'set', 'lo', 'up'], check=True)
+ subprocess.run(['mount', '--bind', os.environ['PURE_MOVE_HOSTS'], '/etc/hosts'], check=True)
if args.transport:
subprocess.run(['make', '-s', '-C', str(ROOT), 'build/libpure.a', 'build/libpure-link.a', 'build/pure-crypto.so'],
check=True)
@@ -614,7 +658,8 @@ def main():
subprocess.run([str(ROOT / 'build/links'), 'keys', str(max(counts)), str(keys)], check=True)
for n in counts:
row = restart_quic(n, work, args.period_ms, args.seconds, args.kill_at) if args.quic else \
- restart_pure(n, work, keys, module, args.period_ms, args.seconds, args.kill_at, not args.no_memo)
+ restart_pure(n, work, keys, module, args.period_ms, args.seconds, args.kill_at, not args.no_memo,
+ *(('receiver.test', mover(Path(os.environ['PURE_MOVE_HOSTS']), ip)) if args.move else ()))
line = json.dumps(row)
print(line, flush=True)
if args.out: