Watch
0
0
Fork
You've already forked hyperhive
0

swarm-archive: check each agent's restic repository weekly

A weekly swarm-archive-check unit runs restic check --read-data-subset on every repository on the archive host, serialised with the prune by a shared flock. It logs archive check failed per agent, pushes check gauges, and has an alert and dashboard columns.
This commit is contained in:
atlas 2026-11-14 11:51:25 +01:00
commit 5c30985a25
2 changed files with 503 additions and 18 deletions

View file

@ -1,6 +1,6 @@
# `checks.script-test-archive-metrics` — runs the metric functions of the archive's
# backup and prune scripts (../host-modules/lib/archive-backup.nix and
# archive-server.nix) against a stub `curl` and a fake `restic`. The cases are in
# archive-server.nix, including the check script's loop) against a stub `curl` and a fake `restic`. The cases are in
# ./archive-metrics.sh, which cuts each function out of the rendered script.
{
pkgs,
@ -11,6 +11,18 @@ let
# Answers from the files named by FAKE_SNAPSHOTS and FAKE_STATS; an unset
# variable makes that subcommand fail, and so does a call without --no-lock:
# the metrics only read, and must not wait on a lock.
# `check` is the integrity check: it must hold the repository lock, and fails
# for the repositories named in FAKE_CHECK_FAIL (space separated agents).
case " $* " in
*" check "*)
echo "check $*" >>"$FAKE_LOG"
case " $* " in *" --no-lock "*) exit 1 ;; esac
for a in ''${FAKE_CHECK_FAIL:-}; do
case " $* " in *"/$a/ "*) exit 1 ;; esac
done
exit 0
;;
esac
case " $* " in *" --no-lock "*) ;; *) exit 1 ;; esac
case " $* " in
*" snapshots "*) [ -n "''${FAKE_SNAPSHOTS:-}" ] && cat "$FAKE_SNAPSHOTS" ;;
@ -25,6 +37,7 @@ pkgs.runCommand "hyperhive-script-test-archive-metrics"
nativeBuildInputs = [
pkgs.coreutils
pkgs.jq
pkgs.util-linux
];
BACKUP_SCRIPT = pkgs.writeText "archive-backup.sh" (
import ../host-modules/lib/archive-backup.nix { inherit (pkgs) lib; } {
@ -39,6 +52,22 @@ pkgs.runCommand "hyperhive-script-test-archive-metrics"
otlpEndpoint = endpoint;
}
);
CHECK_SCRIPT = pkgs.writeText "archive-check.sh" (
(import ../host-modules/lib/archive-server.nix { inherit (pkgs) lib; } {
rclone = pkgs.rclone;
restic = fakeRestic;
reposDir = "/repos";
addr = "127.0.0.1:1";
authFile = "/auth";
authGroup = "g";
retention = {
keepWithin = null;
keepLast = 1;
};
checkSubset = "7%";
otlpEndpoint = endpoint;
}).checkScript
);
PRUNE_SCRIPT = pkgs.writeText "archive-prune.sh" (
(import ../host-modules/lib/archive-server.nix { inherit (pkgs) lib; } {
rclone = pkgs.rclone;

View file

@ -16,6 +16,8 @@ mkdir -p "$root/bin"
cat >"$root/bin/curl" <<STUB
#!$BASH
cat >"$root/curl.payload"
cat "$root/curl.payload" >>"$root/curl.all"
echo >>"$root/curl.all"
STUB
chmod +x "$root/bin/curl"
@ -185,5 +187,99 @@ else
fail "failed: $(cat "$root/err")"
fi
# --- the check side ---
c=hyperhive.archive.check
{
cut_function "$CHECK_SCRIPT" otlp_gauges
cut_function "$CHECK_SCRIPT" push_check_metrics
cut_function "$CHECK_SCRIPT" check_agents
} >"$root/check-fns.sh"
grep -q 'check_agents()' "$root/check-fns.sh" || { echo "FAILED: check_agents not found"; exit 1; }
grep -q 'push_check_metrics()' "$root/check-fns.sh" || { echo "FAILED: push_check_metrics not found"; exit 1; }
# The gauges pushed for agent $1 in the last run, as `name=value` lines.
agent_gauges() {
jq -rs --arg a "$1" '.[] | select(.resourceMetrics[0].scopeMetrics[0].metrics[0].gauge.dataPoints[0].attributes[0].value.stringValue == $a)
| .resourceMetrics[0].scopeMetrics[0].metrics[] | "\(.name)=\(.gauge.dataPoints[0].asDouble)"' "$root/curl.all"
}
case="the check loop goes on after a failure, exits non-zero and names the agent that failed"
rm -f "$root/curl.all" "$root/fake.log"
: >"$root/fake.log"
FAKE_LOG="$root/fake.log" FAKE_CHECK_FAIL=beta run "$root/check-fns.sh" check_agents alpha beta gamma
rc=$?
[ "$rc" != 0 ] || fail "exit status was 0"
grep -qx 'archive check failed: beta' "$root/err" || fail "no failure line: $(cat "$root/err")"
grep -q 'archive check failed: alpha\|archive check failed: gamma' "$root/err" && fail "a passing agent was reported: $(cat "$root/err")"
[ "$(wc -l <"$root/fake.log")" = 3 ] || fail "want a check of every agent: $(cat "$root/fake.log")"
grep -q '/repos/gamma/ .*check --read-data-subset=7%' "$root/fake.log" || fail "gamma not checked with the subset: $(cat "$root/fake.log")"
grep -qx 'checked alpha' "$root/out" || fail "no success line: $(cat "$root/out")"
case="the check pushes each agent's own result"
[ "$(agent_gauges alpha | grep -c .)" = 3 ] || fail "alpha gauges: $(agent_gauges alpha)"
agent_gauges alpha | grep -qx "$c.last_run_success=1" || fail "alpha result: $(agent_gauges alpha)"
agent_gauges alpha | grep -q "^$c.last_run_timestamp_seconds=[0-9]" || fail "alpha time: $(agent_gauges alpha)"
agent_gauges alpha | grep -q "^$c.duration_seconds=[0-9]" || fail "alpha duration: $(agent_gauges alpha)"
agent_gauges beta | grep -qx "$c.last_run_success=0" || fail "beta result: $(agent_gauges beta)"
agent_gauges gamma | grep -qx "$c.last_run_success=1" || fail "gamma result: $(agent_gauges gamma)"
[ "$(jq -rs '.[0].resourceMetrics[0].resource.attributes[0].value.stringValue' "$root/curl.all")" = swarm-archive-check ] ||
fail "service.name"
case="the check pushes the duration it measured"
if run "$root/check-fns.sh" push_check_metrics alpha 1 100 190; then
want_gauges "$c.last_run_success=1
$c.last_run_timestamp_seconds=190
$c.duration_seconds=90"
else
fail "failed: $(cat "$root/err")"
fi
case="the check exits zero when every repository passes"
if FAKE_LOG="$root/fake.log" run "$root/check-fns.sh" check_agents alpha beta; then
grep -q 'archive check failed' "$root/err" && fail "failure line: $(cat "$root/err")"
else
fail "exit status was non-zero: $(cat "$root/err")"
fi
case="the check refuses a name that is not an agent name and goes on"
: >"$root/fake.log"
FAKE_LOG="$root/fake.log" run "$root/check-fns.sh" check_agents 'bad name;x' alpha
rc=$?
[ "$rc" != 0 ] || fail "exit status was 0"
grep -qx 'archive check failed: bad name;x is not an agent name' "$root/err" || fail "no refusal: $(cat "$root/err")"
[ "$(wc -l <"$root/fake.log")" = 1 ] || fail "alpha not checked: $(cat "$root/fake.log")"
# --- the lock the prune and the check share ---
lock_prologue() {
awk '/^if \[ -n "\$\{ARCHIVE_MAINTENANCE_LOCK:-\}" \]; then$/ {on=1} on {print} on && /^fi$/ {exit}' "$1"
}
for s in CHECK_SCRIPT PRUNE_SCRIPT; do
lock_prologue "${!s}" >"$root/lock-$s.sh"
grep -q flock "$root/lock-$s.sh" || { echo "FAILED: $s has no maintenance lock"; exit 1; }
done
case="the check waits while the prune holds the lock, and runs once it is free"
lockfile="$root/maintenance.lock"
flock "$lockfile" sleep 5 &
holder=$!
sleep 0.5
ARCHIVE_MAINTENANCE_LOCK="$lockfile" timeout 1 bash "$root/lock-CHECK_SCRIPT.sh"
[ "$?" = 124 ] || fail "did not wait for the held lock"
kill "$holder" 2>/dev/null
wait "$holder" 2>/dev/null
ARCHIVE_MAINTENANCE_LOCK="$lockfile" timeout 5 bash "$root/lock-CHECK_SCRIPT.sh" || fail "did not run on a free lock"
case="the prune waits on the same lock"
flock "$lockfile" sleep 5 &
holder=$!
sleep 0.5
ARCHIVE_MAINTENANCE_LOCK="$lockfile" timeout 1 bash "$root/lock-PRUNE_SCRIPT.sh"
[ "$?" = 124 ] || fail "did not wait for the held lock"
kill "$holder" 2>/dev/null
wait "$holder" 2>/dev/null
case="without the lock variable the scripts take no lock"
timeout 5 bash "$root/lock-CHECK_SCRIPT.sh" || fail "failed without the variable"
[ "$failures" -eq 0 ] || exit 1
echo "all cases passed"