myxce/src/tools/deploy-ha.sh
Rongsong Shen 9821ec4f3c fix(ha): election-safety hardening — vote gates, restored-vote term, freshness source
Election protocol fixes validated against raft.tla invariants:

- ha_init_raft(): one-time raft state init at startup (re-init would lose
  durable election progress); n_peers = full cluster size including self
- handle_request_vote(): higher-term step-down moved BEFORE the role gate
  (split-brain fix); standby vote-deny now requires a live primary control
  channel (primary_ctrl_fd >= 0) AND a fresh heartbeat
- ha_heartbeat_is_fresh(): derive freshness from ha->last_activity (the
  field production actually writes); the orphaned last_heartbeat timespec
  had no production writer, leaving the vote-deny gate inert (bug-337);
  field removed from ha_state_t
- handle_request_vote(): restore same-candidate idempotent re-grant —
  deny only a DIFFERENT candidate in the same term, matching raft.c and
  Raft §5.2 votedFor ∈ {Nil, candidateId} (bug-338)
- raft_init(): restore voted_for_term alongside a persisted vote; leaving
  it 0 allowed a double vote in the same term across a restart (bug-339)
- myapp_run(): leader_known check treats a vote recorded under this node's
  own name as a self-vote — production persists the real candidate name,
  the "self" placeholder only appears in test mode
- raft_on_vote_request(): leader steps down on higher-term vote request
  instead of denying it

Tests: test_proto_H6_restored_vote_blocks_second_vote added; H1 restore
assertion strengthened; test_ha_vote_gate.c driven through last_activity +
primary_ctrl_fd (the real production fields). Full /xce-build all green
(lib + controller + scheduler + Go suites, 0 failures, ASAN clean).

deploy-ha.sh: start.sh now forwards extra args to the controller binary.
2026-06-10 01:25:39 +00:00

79 lines
2 KiB
Bash
Executable file

#!/bin/bash
set -e
BINARY=$1
DATADIR=$2
TOP=$3
PEERS=$4
if [ -z "$BINARY" ] || [ -z "$DATADIR" ] || [ -z "$TOP" ] || [ -z "$PEERS" ]; then
echo "Usage: $0 <binary> <data-dir> <top-dir> <peers>"
echo " peers: comma-separated host:port list (e.g. host1:port1,host2:port2)"
exit 1
fi
BINARY=$(realpath "$BINARY")
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
# Copy certificates
cp "$DATADIR/xce.crt" "$TOP/certs/"
cp "$DATADIR/xce.key" "$TOP/certs/"
# Tag and copy cluster.bin
TAG=$(sha256sum "$DATADIR/cluster.bin" | cut -c1-12)
cp "$DATADIR/cluster.bin" "$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)"