mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
ea3e01bb20
* fix: reboot a guest by asking it, not by pulling its reset line RebootInstances was QMP system_reset alone. The guest kernel is never told, so it does not sync, does not unmount and loses every dirty page - which is how a reboot issued seconds after a write loses that write, and how a guest rebooted during cloud-init can fail to come back at all. Guests now launch with -action shutdown=pause, so a clean guest poweroff leaves QEMU paused rather than gone. Reboot presses the power button, waits for the guest to reach that state, then resets and resumes the same process: no volume, tap or state churn, and no window where the guest is missing. A guest that will not go quietly is hard-reset, which is what AWS documents. The button is pressed again every 10s while the guest ignores it. One signal is not a shutdown request on a guest whose acpid has not started, which is the same boot race that produced the failure. Two consequences handled here: a guest that powers itself off now leaves QEMU paused, so the heartbeat quits it; and a guest launched before the flag exits on powerdown rather than pausing, so it is reset without being asked. * fix: answer a reboot when it is accepted, not when the guest is back Asking a guest to shut down takes as long as the guest takes. RebootInstances gives the daemon five seconds to reply, so a reboot that waited for the guest reported a reboot that had in fact succeeded as InvalidInstanceID.NotFound. Manager.Reboot now admits the reboot — the instance exists and is running — and runs the powerdown and reset in the background, which is the contract EC2 documents. The two admission failures are still reported; nothing after them can be, so a reset that fails is logged and left to the heartbeat. The gateway also stops calling a NATS timeout NotFound. Only the absence of a subscriber says anything about where the instance is, which is how the sibling start and terminate paths already read it. * test: prove the churn reboot by its boot ID, not by how fast it happened The subtest waited ten seconds and then read /proc/uptime. A reboot is asynchronous, so the guest answering at that point may be the one that has not shut down yet, and its uptime is whatever it was before — which the test reads as "reboot may not have occurred". Waiting for the boot ID to change is positive proof the guest rebooted and does not depend on how long the guest takes to go down. The uptime assertion stays, now measured against a boot that is known to be new. * test: prove the graceful reboot against real guests and a real engine Two tests the reboot design asked for and the unit tests cannot give. Cell 18 launches two guests that differ only in how they answer the power button. One installs a shutdown hook writing an fsynced line a second, so the file that survives measures how much of its shutdown it was allowed to run — a guest reset instead of asked leaves none of it. The other ignores the power key entirely and must be reset once its budget expires, with the daemon saying so. Each variant asserts the evidence only it can produce, and the slow one asserts the absence of the line the wedged one requires. Cell 27 loads a PostgreSQL instance past its shared buffers with pgbench and reboots it dirty. The probe is an UNLOGGED table: PostgreSQL writes no WAL for one, so a clean shutdown checkpoints it and crash recovery truncates it unconditionally. Rows committed normally survive a hard reset too, so the obvious assertion would pass against the bug. The rds suite and cell 27 both gain budget for the load phase. * fix: stop the process-group test misreading a reaped child as alive The test polled until the child was gone, then checked once more. A task being reaped shows state X for an instant, which processRunning counted as running, so that second look failed about 1 run in 50 under -race. * fix: wait for the recovery retry loop before removing a test's data dir restoreInstances starts the retry loop untracked, and it rewrites the local state file when it ends. A write landing during t.TempDir cleanup failed TestRestoreInstances_* with RemoveAll: directory not empty.
128 lines
6.8 KiB
Bash
128 lines
6.8 KiB
Bash
# shellcheck shell=bash
|
|
# shellcheck disable=SC2034 # every variable here is consumed by the sourcing workflow
|
|
# Source from a workflow `run:` block:
|
|
# . "${GITHUB_WORKSPACE}/spinifex/.github/scripts/suite-sets.sh"
|
|
#
|
|
# Which e2e suites each topology can run. Sourced by every workflow that drives
|
|
# suites (e2e.yml, e2e-suites.yml, e2e-nightly.yml) so the lists cannot drift
|
|
# apart — they previously disagreed three ways.
|
|
#
|
|
# Eligibility is a property of the suite, not of the workflow: a suite belongs
|
|
# to a topology when all of its assertions are reachable there. lb is
|
|
# multi-only for exactly that reason — on a single node every internet-facing
|
|
# subtest skips for want of a peer, so a single-node lb run reports green
|
|
# having exercised a fraction of the suite.
|
|
|
|
# Suites runnable against a one-node environment.
|
|
#
|
|
# partialblock gates merges because it earned it, not because it is new: on one
|
|
# node, same binary and workload, it fails against the pre-fix engine (1758 of
|
|
# 8192 halves reverted a generation) and passes against the fixed one (0 of
|
|
# 8192). A suite that has never been shown to go red for the reason it claims
|
|
# does not belong here.
|
|
# rds is single-only for the opposite reason to lb: v1 has no multi-AZ
|
|
# behaviour, so a second node adds no assertion. Its DB VMs are capped at four
|
|
# concurrent by the suite's own semaphore, which is what keeps it inside the
|
|
# job's 50-minute budget alongside the suites above.
|
|
# quota belongs to both topologies because the gate it asserts is gateway code
|
|
# that runs identically on either, and because a quota left switched on would
|
|
# cap every suite sharing the environment — so it must be proved to restore
|
|
# itself wherever it runs, not only on one node.
|
|
# storagefault runs last in both lists and belongs to both topologies, for
|
|
# different reasons. On one node it is RS(1,0), so freezing the only predastore
|
|
# is a total outage and the guest-survival assertion is unambiguous. On three it
|
|
# is RS(2,1) with degraded writes on, so two must be frozen to break the shard
|
|
# floor — and only there is a cross-node restart possible at all, which is the
|
|
# case that takes a volume over from a node holding writes the backend never
|
|
# received. It is last because it SIGSTOPs a cluster-wide
|
|
# service: anything sharing the environment would see the same outage and report
|
|
# a failure that says nothing about itself.
|
|
E2E_SUITES_SINGLE="single iam cert eks ecs storagegrowth partialblock rds quota storagefault"
|
|
|
|
# Suites runnable against a multi-node environment.
|
|
E2E_SUITES_MULTI="multinode lb cert quota storagefault"
|
|
|
|
# grep -xE alternation form, for narrowing a requested set to the eligible one.
|
|
E2E_SUITES_SINGLE_RE="$(printf '%s' "${E2E_SUITES_SINGLE}" | tr ' ' '|')"
|
|
E2E_SUITES_MULTI_RE="$(printf '%s' "${E2E_SUITES_MULTI}" | tr ' ' '|')"
|
|
|
|
# What every e2e-nightly permutation cell must prove, deliberately narrower
|
|
# than the two sets above: those cells have a 35-minute budget and exist to show
|
|
# that every install / network / host-OS combination boots and serves. The
|
|
# service suites are not deferred to the PR gate — eks, ecs and rds each run
|
|
# nightly in their own dedicated cell, because each needs a guest AMI built
|
|
# first and a service failure should not mask whether a host OS boots. These
|
|
# are subsets, and e2e_suites_assert_subset below enforces that.
|
|
#
|
|
# storagefault has a dedicated cell for two reasons. It SIGSTOPs a cluster-wide
|
|
# service, so in a permutation cell every suite sharing that environment would
|
|
# report the outage as its own failure. And a full pass is ~63 minutes, nearly
|
|
# all of it deliberate waiting, which is too long to add to the PR gate — where
|
|
# it has never run, since the gate's default suite list does not name it.
|
|
#
|
|
# Its cell is three-node: the RS(2,1) shard floor and the cross-node takeover
|
|
# after a failed seal do not exist on one node, and they are the assertions
|
|
# worth a nightly.
|
|
E2E_SUITES_NIGHTLY_SINGLE="single cert iam"
|
|
E2E_SUITES_NIGHTLY_MULTI="multinode cert lb"
|
|
|
|
# How long a suite gets, for the same reason the lists above live here: the
|
|
# timeout was written out separately in each workflow and drifted, so rds ran
|
|
# 50m on the PR gate and 30m nightly and panicked there. A suite needing more
|
|
# than the default declares it once, here.
|
|
# e2e_suite_timeout <suite>
|
|
e2e_suite_timeout() {
|
|
case "$1" in
|
|
# rds serialises its DB VMs behind a 3 GiB semaphore, so its wall clock is
|
|
# what the queue costs, not what the tests do: single tests logged 11m, 22m
|
|
# and 26m waiting for budget. 17 tests at that concurrency is over an hour
|
|
# before anything goes wrong, and the first pass to run deep into the suite
|
|
# still had three left at 60m. The loaded graceful-reboot case adds an
|
|
# instance that holds its slot through a pgbench load as well as a create.
|
|
rds) echo "105m" ;;
|
|
# diskperf runs two 16 GiB fio profiles per repetition, each on a volume it
|
|
# creates first. One pass is ~20 minutes on the bare-metal cell and a
|
|
# baseline capture at SPINIFEX_DISKPERF_REPS=3 is three times that.
|
|
diskperf) echo "180m" ;;
|
|
# storagefault is mostly deliberate waiting: one test alone holds predastore
|
|
# SIGSTOPped for seven minutes to prove a guest survives it. A full pass on
|
|
# dev-prod is ~63 minutes, so the 30m default truncated it mid-suite.
|
|
storagefault) echo "90m" ;;
|
|
*) echo "30m" ;;
|
|
esac
|
|
}
|
|
|
|
# One matrix leg per suite, carrying the job budget that suite needs. The outer
|
|
# timeout must always be later than the `go test` one inside it: go test reports
|
|
# where it stopped, a cancelled job reports only "The operation was canceled".
|
|
# e2e_suite_matrix <suite-list> <floor-minutes> [allowance-minutes]
|
|
e2e_suite_matrix() {
|
|
local list="$1" floor="$2" allowance="${3:-15}" suite budget
|
|
for suite in ${list}; do
|
|
budget="$(e2e_suite_timeout "${suite}")"
|
|
budget=$((${budget%m} + allowance))
|
|
if [ "${budget}" -lt "${floor}" ]; then budget="${floor}"; fi
|
|
printf '%s\t%s\n' "${suite}" "${budget}"
|
|
done | jq -cRs 'split("\n")|map(select(length>0)|split("\t")|{suite:.[0],timeout:(.[1]|tonumber)})'
|
|
}
|
|
|
|
# Fail loudly when a narrowed list names a suite its topology cannot run. This
|
|
# is the guard that stops the lists drifting again: a suite added to a nightly
|
|
# subset without also being declared eligible above stops the job here rather
|
|
# than silently skipping half its assertions in CI.
|
|
# e2e_suites_assert_subset <label> <subset> <eligible-set>
|
|
e2e_suites_assert_subset() {
|
|
local label="$1" subset="$2" eligible="$3" suite found eligible_suite
|
|
for suite in ${subset}; do
|
|
found=0
|
|
for eligible_suite in ${eligible}; do
|
|
if [ "${suite}" = "${eligible_suite}" ]; then found=1; break; fi
|
|
done
|
|
if [ "${found}" -ne 1 ]; then
|
|
echo "::error::${label} names suite '${suite}', which is not eligible for that topology (eligible: ${eligible}). Fix the list in .github/scripts/suite-sets.sh." >&2
|
|
return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|