Bundles the fixes validated this session across ha.c/persist.c/worker.c: - raft-apply-decoupling Task 7.4: apply-pump O(N^2) throughput regression fixed via a segment-read cache. - bug-1416: boot-time TOCTOU race in persist_verify_apply_correctness() closed with a recovery-complete gate that closes at process-lifecycle start, not function entry. - bug-451: HOOK_DETAIL_JOB_RUSAGE_UPDATE now has its own persist hook arm instead of silently falling through the default job-state case. - bug-1426: trim_wal_segments() also caps its trim floor at the local apply pump's own progress, not just remote standby-acked positions. - bug-1427: persist_verify_apply_correctness() extended with a worker-count check mirroring the existing job-count check (count_jobmgr_entries and count_workermgr_entries now share one count_manager_entries(var) helper). - bug-1428: persist_register_pending_apply's critical section widened to close the actual race instead of patching a symptom. - bug-1472 (remainder): WORKER_DEAD replay in worker_from_capnp() now applies mark_worker_unavail()'s mutations via a shared apply_worker_dead_mutation() helper instead of duplicating them. - bug-1474 (angle-a): on_standby_connected's stale-duplicate detection fixed for promoted vote endpoints via a shared votes_idx_for_peer() index-scale converter. Each fix independently verified via TDD (RED before fix, GREEN after). Full ASAN rebuild + controller_unit_test: 1101 tests, 0 failures. Live single-node HA deployment validated Task 7.4 and bug-1416 across 5 restart/recovery cycles (45/45 job submissions, zero APPLY_DIVERGENT fences). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
158 lines
6.4 KiB
Bash
Executable file
158 lines
6.4 KiB
Bash
Executable file
#!/bin/bash
|
|
set -e
|
|
|
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
|
BINARY=$1
|
|
TOP=$2
|
|
PEERS=$3
|
|
|
|
if [ -z "$BINARY" ] || [ -z "$TOP" ] || [ -z "$PEERS" ]; then
|
|
echo "Usage: $0 <binary> <top-dir> <peers>"
|
|
echo " peers: comma-separated host:port list (e.g. host1:port1,host2:port2)"
|
|
echo " TLS cert/key and cluster.bin are generated fresh into <top-dir> every"
|
|
echo " run (openssl self-signed cert; cluster.bin via xceadmin from"
|
|
echo " src/configs/*.yaml) -- no pre-existing data directory needed."
|
|
exit 1
|
|
fi
|
|
|
|
BINARY=$(realpath "$BINARY")
|
|
|
|
# xceadmin is meson-built now (src/xceadmin/meson.build), not a plain
|
|
# `pip install -e .` venv checked out under src/xceadmin/ -- its build
|
|
# output lives under whichever meson build dir xce-build.sh's profile
|
|
# picked, or under a nix-installed prefix's bin/, and each candidate is a
|
|
# STATIC SNAPSHOT (meson-build.sh copies src/xceadmin out-of-tree and does
|
|
# a plain, non-editable `pip install`) taken whenever that profile's
|
|
# `xceadmin` target was last built -- editing src/xceadmin/ source does
|
|
# NOT retroactively update an already-built snapshot.
|
|
#
|
|
# Correctness, not just freshness, matters here: xceadmin builds the
|
|
# cluster.bin that $BINARY (the controller daemon being deployed) will
|
|
# decode, so the two must come from the SAME build generation -- e.g. if
|
|
# the capnp wire schema changed between builds, an xceadmin from one
|
|
# generation and a controller from another can silently mis-encode/
|
|
# mis-decode a field (this is exactly how a stale-xceadmin deploy bug
|
|
# went undiagnosed: a fresher-but-DIFFERENT-build xceadmin could just as
|
|
# easily mismatch $BINARY as a staler one). Prefer the xceadmin living in
|
|
# $BINARY's OWN build dir (<builddir>/controller/xce_controller ->
|
|
# <builddir>/xceadmin/.venv/bin/xceadmin) -- guaranteed same-generation.
|
|
# Only if that specific one doesn't exist (e.g. xceadmin was never built
|
|
# in that profile) fall back to whichever other candidate is newest, with
|
|
# a loud warning that same-generation consistency isn't guaranteed.
|
|
BINARY_BUILD_DIR=$(dirname "$(dirname "$BINARY")")
|
|
BINARY_DIR=$(dirname "$BINARY")
|
|
XCEADMIN=""
|
|
if [ -x "$BINARY_BUILD_DIR/xceadmin/.venv/bin/xceadmin" ]; then
|
|
# meson layout: <builddir>/controller/xce_controller + <builddir>/xceadmin/.venv/...
|
|
XCEADMIN="$BINARY_BUILD_DIR/xceadmin/.venv/bin/xceadmin"
|
|
echo "Using xceadmin: $XCEADMIN (same build dir as $BINARY)"
|
|
elif [ -x "$BINARY_DIR/xceadmin" ]; then
|
|
# nix result layout: result/bin/xce_controller + result/bin/xceadmin (flat, no
|
|
# per-component subdir) -- BINARY_BUILD_DIR's two-levels-up guess doesn't apply.
|
|
XCEADMIN="$BINARY_DIR/xceadmin"
|
|
echo "Using xceadmin: $XCEADMIN (same directory as $BINARY)"
|
|
else
|
|
echo "Warning: no xceadmin found alongside $BINARY (checked $BINARY_BUILD_DIR/xceadmin/.venv/bin/xceadmin and $BINARY_DIR/xceadmin) -- falling back to the newest xceadmin found elsewhere, which is NOT guaranteed to be the same build generation as $BINARY."
|
|
XCEADMIN_MTIME=0
|
|
for candidate in \
|
|
"$SCRIPT_DIR/../xce.build/xceadmin/.venv/bin/xceadmin" \
|
|
"$SCRIPT_DIR/../build.release/xceadmin/.venv/bin/xceadmin" \
|
|
"$SCRIPT_DIR/../build.dbg/xceadmin/.venv/bin/xceadmin" \
|
|
"$SCRIPT_DIR/../result/bin/xceadmin" \
|
|
"$(command -v xceadmin 2>/dev/null || true)"
|
|
do
|
|
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
|
candidate_mtime=$(stat -c %Y "$candidate" 2>/dev/null || stat -f %m "$candidate" 2>/dev/null || echo 0)
|
|
if [ "$candidate_mtime" -gt "$XCEADMIN_MTIME" ]; then
|
|
XCEADMIN="$candidate"
|
|
XCEADMIN_MTIME="$candidate_mtime"
|
|
fi
|
|
fi
|
|
done
|
|
if [ -n "$XCEADMIN" ]; then
|
|
echo "Using xceadmin: $XCEADMIN (built $(date -d "@$XCEADMIN_MTIME" 2>/dev/null || date -r "$XCEADMIN_MTIME" 2>/dev/null))"
|
|
fi
|
|
fi
|
|
if [ -z "$XCEADMIN" ]; then
|
|
echo "Error: xceadmin binary not found -- run 'xce-build.sh xceadmin' or 'nix build .#xcecore' first"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v openssl >/dev/null 2>&1; then
|
|
echo "Error: openssl not found -- run under 'nix develop' (provides openssl on PATH)"
|
|
exit 1
|
|
fi
|
|
|
|
IFS=',' read -ra PEER_ARRAY <<< "$PEERS"
|
|
N=${#PEER_ARRAY[@]}
|
|
|
|
# Create directory structure
|
|
mkdir -p "$TOP/certs"
|
|
mkdir -p "$TOP/logs"
|
|
mkdir -p "$TOP/scripts"
|
|
for i in $(seq 1 $N); do
|
|
mkdir -p "$TOP/controller/ctrl-$i/conf"
|
|
mkdir -p "$TOP/controller/ctrl-$i/data"
|
|
done
|
|
|
|
# Self-signed dev/test TLS cert+key, generated fresh directly into the
|
|
# deployment dir every run -- nothing checked into git, nothing shared
|
|
# across deploys. Long-lived (10y) since it's regenerated per deploy
|
|
# anyway; TLS verification is SSL_VERIFY_NONE everywhere in this codebase
|
|
# (no hostname/identity check), so a fresh self-signed pair per run loses
|
|
# no real security property versus a long-lived shared one.
|
|
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
|
-keyout "$TOP/certs/xce.key" -out "$TOP/certs/xce.crt" \
|
|
-subj "/CN=xce-dev-test" 2>/dev/null
|
|
|
|
# Build cluster.bin from src/configs/*.yaml via xceadmin, straight into
|
|
# this deploy's own ctrl-1/conf/ (no intermediate data-dir copy).
|
|
CLUSTER_BIN_TMP="$TOP/.cluster.bin.tmp"
|
|
"$XCEADMIN" config build "$SCRIPT_DIR/../configs" -o "$CLUSTER_BIN_TMP"
|
|
TAG=$(sha256sum "$CLUSTER_BIN_TMP" | cut -c1-12)
|
|
mv "$CLUSTER_BIN_TMP" "$TOP/controller/ctrl-1/conf/cluster.bin.$TAG"
|
|
echo "cluster.bin.$TAG" > "$TOP/controller/ctrl-1/conf/current"
|
|
|
|
# Build peers string: ctrl-1=host1:port1,ctrl-2=host2:port2,...
|
|
PEERS_STR=""
|
|
for i in "${!PEER_ARRAY[@]}"; do
|
|
n=$((i+1))
|
|
[ -n "$PEERS_STR" ] && PEERS_STR+=","
|
|
PEERS_STR+="ctrl-$n=${PEER_ARRAY[$i]}"
|
|
done
|
|
|
|
# Generate start script
|
|
cat > "$TOP/scripts/start.sh" << EOF
|
|
#!/bin/bash
|
|
set -e
|
|
NAME=\$1
|
|
if [ -z "\$NAME" ]; then
|
|
echo "Usage: \$0 <ctrl-name>"
|
|
exit 1
|
|
fi
|
|
shift
|
|
CTRL_DIR=$TOP/controller/\$NAME
|
|
if [ ! -d "\$CTRL_DIR/conf" ] || [ ! -d "\$CTRL_DIR/data" ]; then
|
|
echo "Error: \$CTRL_DIR/conf or \$CTRL_DIR/data does not exist"
|
|
exit 1
|
|
fi
|
|
$BINARY --myname \$NAME --peers "$PEERS_STR" --workdir $TOP/controller --certdir $TOP/certs --logfile $TOP/logs/\$NAME.log --vcpus 4 \$*
|
|
EOF
|
|
chmod +x "$TOP/scripts/start.sh"
|
|
|
|
# Generate cleanup script
|
|
cat > "$TOP/scripts/cleanup.sh" << EOF
|
|
#!/bin/bash
|
|
set -e
|
|
rm -f $TOP/logs/*.log
|
|
for i in \$(seq 1 $N); do
|
|
rm -rf $TOP/controller/ctrl-\$i/data/*
|
|
done
|
|
for i in \$(seq 2 $N); do
|
|
rm -rf $TOP/controller/ctrl-\$i/conf/*
|
|
done
|
|
EOF
|
|
chmod +x "$TOP/scripts/cleanup.sh"
|
|
|
|
echo "Deployed $N-node cluster to $TOP (tag: $TAG)"
|