AtlatestRepositoryapiary
1
#!/usr/bin/env bash2
#3
# Cross-repo keepalive integration smoke. Boots a v0.2.8+4
# enclave-server with TIGHT ping-interval / ping-timeout knobs5
# (5 s / 5 s) so the test finishes in ~30 s, then runs the v0.1.2+6
# apiary binary against it and verifies the two-sided7
# PING/PONG handshake actually composes.8
#9
# Scenarios (each <30 s):10
#11
# 1. Server-PING / apiary-PONG: with tight ping-interval the12
# server PINGs apiary within 5-15 s of connect. apiary's13
# `handle-server-ping` emits a matching PONG. We assert14
# the apiary log shows "Apiary responded to server PING"15
# AND the apiary connection stays alive (no16
# ping-timeout-disconnect on the server side).17
#18
# 2. Force-disconnect detection + reconnect: kill -STOP the19
# enclave to freeze writes. Apiary sees no incoming traffic20
# and (after its 60 s idle threshold + 30 s PONG timeout)21
# would trigger reconnect — but to keep the test under a22
# minute, we instead kill -9 the enclave (drops the socket23
# cleanly), restart it, and assert apiary's reconnect-24
# attempt path runs and re-establishes registration.25
#26
# A separate, longer-running manual procedure (see27
# `procedures/apiary-enclave-keepalive-soak`) covers the full28
# 15-min idle window + iptables-based network-kill scenario.30
set -u31
PORT=${PORT:-49870}32
ENCLAVE_REPO=${ENCLAVE_REPO:-/home/daviwil/Projects/Code/sigil/task-apiary-and-enclave-keepalive-pair-enclave}33
APIARY_REPO=${APIARY_REPO:-$(pwd)}34
ENCLAVE_BIN=${ENCLAVE_BIN:-$ENCLAVE_REPO/build/dev/bin/enclave}35
APIARY_BIN=${APIARY_BIN:-$APIARY_REPO/build/dev/bin/apiary}36
DB=${DB:-/tmp/apiary-keepalive-smoke.db}37
PING_INTERVAL=${PING_INTERVAL:-5}38
PING_TIMEOUT=${PING_TIMEOUT:-5}40
if [ ! -x "$ENCLAVE_BIN" ]; then41
echo "FATAL: enclave binary missing at $ENCLAVE_BIN" >&242
exit 143
fi44
if [ ! -x "$APIARY_BIN" ]; then45
echo "FATAL: apiary binary missing at $APIARY_BIN" >&246
exit 147
fi49
SERVER_LOG=$(mktemp)50
APIARY_LOG=$(mktemp)51
SERVER=52
APIARY_PID=54
cleanup() {55
[ -n "${APIARY_PID:-}" ] && kill "$APIARY_PID" 2>/dev/null56
[ -n "${SERVER:-}" ] && kill "$SERVER" 2>/dev/null57
wait 2>/dev/null58
rm -f "$SERVER_LOG" "$APIARY_LOG"59
rm -f "$DB" "$DB-shm" "$DB-wal"60
}61
trap cleanup EXIT63
# ============================================================64
# Phase 1 — server-PING / apiary-PONG composition.65
# ============================================================66
echo "==> seeding DB at $DB"67
rm -f "$DB" "$DB-shm" "$DB-wal"69
DAVIWIL_PASS=admin70
ENCLAVE_DB_PATH="$DB" "$ENCLAVE_BIN" \71
user add daviwil --password "$DAVIWIL_PASS" --role admin > /dev/null73
LEADER_OUT=$(ENCLAVE_DB_PATH="$DB" "$ENCLAVE_BIN" \74
bot register leader-quinn --owner daviwil --allow-services yes --auto-join "#hive")75
LEADER_TOKEN=$(echo "$LEADER_OUT" | grep -oE '[a-f0-9]{64}' | head -1)76
if [ -z "$LEADER_TOKEN" ]; then77
echo "FATAL: no leader-quinn token" >&2; exit 178
fi79
echo " leader-quinn token: ${LEADER_TOKEN:0:8}…(${#LEADER_TOKEN})"81
start_server() {82
ENCLAVE_LOG_LEVEL=debug \83
ENCLAVE_PORT="$PORT" \84
ENCLAVE_DB_PATH="$DB" \85
ENCLAVE_PING_INTERVAL="$PING_INTERVAL" \86
ENCLAVE_PING_TIMEOUT="$PING_TIMEOUT" \87
"$ENCLAVE_BIN" serve >> "$SERVER_LOG" 2>&1 &88
SERVER=$!89
sleep 290
if ! kill -0 "$SERVER" 2>/dev/null; then91
echo "FAIL: server died at startup" >&292
cat "$SERVER_LOG" >&293
exit 194
fi95
}97
echo "==> starting enclave-server on :$PORT (ping-interval=${PING_INTERVAL}s ping-timeout=${PING_TIMEOUT}s)"98
start_server100
echo "==> launching apiary"101
export APIARY_ENCLAVE_HOST=127.0.0.1102
export APIARY_ENCLAVE_PORT="$PORT"103
export APIARY_ENCLAVE_TLS=no104
export APIARY_USER=leader-quinn105
export APIARY_TOKEN="$LEADER_TOKEN"106
export APIARY_CHANNEL=#hive107
export APIARY_OWNER_NICK=daviwil108
export APIARY_REPORTS_TO=daviwil109
export APIARY_SERVICES_TIMEOUT=8.0110
export APIARY_MODE=leader111
INIT_JSON='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0.0"}}}'112
INITED_JSON='{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}'113
(printf '%s\n%s\n' "$INIT_JSON" "$INITED_JSON"; tail -f /dev/null) \114
| "$APIARY_BIN" --log-level debug > "$APIARY_LOG" 2>&1 &115
APIARY_PID=$!117
# Wait up to 6 s for the bridge to come up.118
for _ in $(seq 1 60); do119
if grep -q "Apiary bridge connected" "$APIARY_LOG"; then120
break121
fi122
sleep 0.1123
done125
if ! grep -q "Apiary bridge connected" "$APIARY_LOG"; then126
echo "FAIL: apiary did not connect"127
echo "==> apiary log:"128
sed 's/^/ /' "$APIARY_LOG"129
exit 1130
fi131
echo " apiary connected"133
# Sleep through one ping-interval+slack so the server has time to134
# send at least one PING and apiary has time to respond.135
echo "==> idling for $((PING_INTERVAL * 3)) s to observe server PING / apiary PONG"136
sleep $((PING_INTERVAL * 3))138
# ============================================================139
# Assertions for Phase 1140
# ============================================================141
fail=0142
check() {143
local desc=$1 pattern=$2 file=$3144
if grep -qE "$pattern" "$file"; then145
echo "PASS: $desc"146
else147
echo "FAIL: $desc"148
echo " expected: $pattern"149
echo " in: $file"150
fail=1151
fi152
}154
check_absent() {155
local desc=$1 pattern=$2 file=$3156
if ! grep -qE "$pattern" "$file"; then157
echo "PASS: $desc"158
else159
echo "FAIL: $desc"160
echo " unexpected: $pattern"161
grep -E "$pattern" "$file" | head -3 | sed 's/^/ /'162
fail=1163
fi164
}166
echo167
echo "==> phase 1 assertions (server-PING → apiary-PONG)"168
check "server emitted at least one PING out to apiary" \169
'ping-out.*nick=leader-quinn' "$SERVER_LOG"170
check "apiary's handle-server-ping responded to the PING" \171
'Apiary responded to server PING' "$APIARY_LOG"172
check_absent "no Ping timeout disconnect of apiary on the server side" \173
'ping-timeout-disconnect.*nick=leader-quinn' "$SERVER_LOG"174
check "apiary's keepalive-tick has run at least once" \175
'Apiary keepalive PING out|Apiary responded to server PING' "$APIARY_LOG"177
# ============================================================178
# Phase 2 — force-disconnect + reconnect.179
# ============================================================180
echo181
echo "==> phase 2: kill enclave, verify apiary detects + reconnects"182
kill -9 "$SERVER"183
wait "$SERVER" 2>/dev/null184
SERVER=186
# Apiary's irc-tick will see EOF on the next read and the event187
# loop's reconnect-trigger path should kick in. Give it a moment188
# to log the reconnect attempt.189
sleep 3191
check "apiary logged reconnect-trigger after server died" \192
'Apiary reconnecting' "$APIARY_LOG"194
echo "==> restarting enclave"195
start_server196
# Apiary's reconnect loop has exponential backoff starting at 1s.197
# Let it cycle a few times to actually reach the now-running server.198
sleep 12200
check "apiary reconnected after server came back" \201
'Apiary reconnected' "$APIARY_LOG"202
check_absent "apiary did not segfault" \203
'segfault|SIGSEGV' "$APIARY_LOG"205
# ============================================================206
# Phase 3 — extended outage spanning multiple reconnect attempts.207
#208
# The brief's failing scenario: enclave restarts (typical209
# downtime ~5-10s during which the listener is down or up but210
# not yet accepting registrations). Apiary's reconnect attempt211
# 1 fires at the 1s backoff window, finds the server unreachable212
# OR partially-reachable, and the registration handshake hangs.213
# Pre-fix, the registration timeout (30s) raised an exception214
# that escaped the goroutine and crashed apiary.215
#216
# Post-fix, do-reconnect! returns 'retry on registration timeout217
# (or any handshake error), the partial irc is cleaned up, and218
# the backoff schedule cycles to attempt 2, 3, etc. With the219
# tightened 10s registration timeout each failed attempt cycles220
# fast enough that recovery happens within ~30s of the server221
# coming back.222
#223
# This phase verifies the no-crash invariant by holding the224
# server down through MULTIPLE reconnect attempts, then bringing225
# it back. Apiary should reach a healthy state without any226
# "Apiary crashed" log line.227
# ============================================================228
echo229
echo "==> phase 3: extended outage — server down for ~12s spanning >=3 reconnect attempts"230
kill -9 "$SERVER"231
wait "$SERVER" 2>/dev/null232
SERVER=234
# Hold the server down for 12s. With backoff 1s/2s/4s/8s, apiary235
# should fire attempts 1, 2, 3 (and possibly 4) within this236
# window — all of which will fail with TCP-refused. Then we237
# bring the server back and let the next attempt succeed.238
sleep 12240
check "apiary logged at least 3 reconnect attempts during outage" \241
'attempt=3' "$APIARY_LOG"242
check_absent "apiary did not crash with registration-timeout error" \243
'Apiary crashed.*registration timed out|Apiary crashed.*do-reconnect!' "$APIARY_LOG"244
check_absent "apiary did not crash at all during the outage" \245
'Apiary crashed mode=' "$APIARY_LOG"247
echo "==> restarting enclave (phase 3)"248
start_server249
# Backoff caps at 60s but should already be at 8-16s by now.250
# Wait long enough for one of the next attempts to land.251
sleep 25253
check "apiary reconnected after extended outage" \254
'Apiary reconnected after-attempts' "$APIARY_LOG"256
# ============================================================257
# Final result258
# ============================================================259
if [ $fail -eq 0 ]; then260
echo261
echo "ALL CHECKS PASSED"262
exit 0263
else264
echo265
echo "SOME CHECKS FAILED"266
echo267
echo "==> server log (last 40 lines):"268
tail -40 "$SERVER_LOG" | sed 's/^/ /'269
echo270
echo "==> apiary log (last 40 lines):"271
tail -40 "$APIARY_LOG" | sed 's/^/ /'272
exit 1273
fi