upgrade-test.py: record the engine's own view of its datadir, beside the verdict
gates / gates (push) Successful in 1s
gates / gates (push) Successful in 1s
R-459. The harness returned proven for E3b while MariaDB was logging that the conversion it requires had been skipped. The verdict was right - the app's data survived, which is what it asked - but the harness watched the app and the migration log, and neither looks at engine state. engine_state_after now carries each database service's own answer: MariaDB's datadir version plus 'mariadb-upgrade --check-if-upgrade-is-needed', and PostgreSQL's PG_VERSION. It sits BESIDE the verdict and is never folded into it. An unconverted datadir is not known to be a failure - 5 of 5 restarts showed no degradation - so a verdict that called it failed would encode an unproven judgement, which is worse than reporting a fact and letting a person read both. No template changed. Nothing with MARIADB_ in it is committed by this work: that is a fleet-wide decision the operator owns, and it affects four apps.
This commit is contained in:
@@ -1,3 +1,30 @@
|
||||
## upgrade-test.py records the ENGINE's own view of itself (2026-09-06, R-459) — NOT A RELEASE
|
||||
|
||||
**Scripts only. No template changed, and deliberately so** — nothing with `MARIADB_` in it is
|
||||
committed by this work; that is a fleet-wide decision the operator owns.
|
||||
|
||||
The harness returned **`proven`** for edge E3b while MariaDB was logging that the datadir conversion it
|
||||
requires had been **skipped**. The verdict was not wrong — the app's data did survive, which is what it
|
||||
asked — but nothing here could see that the engine had been left in a state the engine itself calls
|
||||
incomplete. It watched the app and the migration log; **neither looks at engine state.**
|
||||
|
||||
`engine_state_after` now carries, per database service, the engine's own answer. On the E3b re-run:
|
||||
|
||||
```
|
||||
verdict: proven
|
||||
engine_state_after.bookstack-db.answer:
|
||||
"11.6.2-MariaDB| Major version upgrade detected from 11.6.2-MariaDB to 12.3.3-MariaDB. Check required! [exit=0]"
|
||||
```
|
||||
|
||||
**It is reported BESIDE the verdict and never folded into it.** An unconverted datadir is not known to
|
||||
be a failure — `SPIKE-r459-mariadb-upgrade-2026-09-06.md` measured 5 of 5 restarts with no degradation
|
||||
— so a verdict that called it `failed` would encode an unproven judgement, which is worse than
|
||||
reporting a fact and letting a person read both.
|
||||
|
||||
**Both engines are covered and they fail differently:** MariaDB starts anyway and skips the conversion
|
||||
quietly; PostgreSQL refuses to start on a datadir from an older major. "The engine would not start" is
|
||||
as much an engine-state observation as "the engine says it needs a check".
|
||||
|
||||
## upgrade-test.py — a harness that measures whether an upgrade keeps the data (2026-09-06, R-449) — NOT A RELEASE
|
||||
|
||||
**No template changed. No image moved. Scripts only.** `scripts/upgrade-test.py` +
|
||||
|
||||
@@ -156,6 +156,53 @@ def settle(project: str, workdir: Path, wait=420):
|
||||
return False, round(time.time() - t0, 1), states
|
||||
|
||||
|
||||
# --- the ENGINE's own view of itself (R-459) ------------------------------------------------------
|
||||
#
|
||||
# WHY THIS EXISTS. On edge E3b this harness returned `proven` — correctly: the app's data survived,
|
||||
# which is what it asked. But MariaDB was at that moment logging that the datadir conversion it
|
||||
# requires had been SKIPPED, and nothing here could see it. The harness watched the app and the
|
||||
# migration log; neither looks at engine state.
|
||||
#
|
||||
# IT IS REPORTED BESIDE THE VERDICT, NEVER FOLDED INTO IT. An unconverted datadir is not known to be
|
||||
# a failure — SPIKE-r459-mariadb-upgrade-2026-09-06 measured 5 of 5 restarts with no degradation — so
|
||||
# a verdict that called it `failed` would encode an unproven judgement, which is worse than reporting
|
||||
# a fact and letting a person read both.
|
||||
#
|
||||
# The two engines fail differently and the field carries both: MariaDB starts anyway and skips the
|
||||
# conversion quietly; PostgreSQL REFUSES to start on a datadir from an older major. So "the engine
|
||||
# would not start" is as much an engine-state observation as "the engine says it needs a check".
|
||||
ENGINE_PROBES = {
|
||||
# image-name fragment -> (probe command inside the container, what the answer means)
|
||||
"mariadb": (
|
||||
"cat /var/lib/mysql/mariadb_upgrade_info 2>&1; echo '|'; "
|
||||
"mariadb-upgrade --check-if-upgrade-is-needed --user=root "
|
||||
"--password=$MYSQL_ROOT_PASSWORD 2>&1; echo \"[exit=$?]\"",
|
||||
"datadir version | the engine's own upgrade verdict",
|
||||
),
|
||||
"postgres": (
|
||||
"cat /var/lib/postgresql/data/PG_VERSION 2>&1", "datadir major version",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def engine_state(images: dict):
|
||||
"""Ask every database engine in this stack what it thinks of its own datadir.
|
||||
|
||||
Best-effort and never fatal: a probe that cannot run records why, because "we could not ask" and
|
||||
"the engine is content" are different facts and only one of them is about the engine.
|
||||
"""
|
||||
out = {}
|
||||
for svc, ref in images.items():
|
||||
for frag, (cmd, meaning) in ENGINE_PROBES.items():
|
||||
if frag not in ref:
|
||||
continue
|
||||
r = cvp._sh(["docker", "exec", svc, "sh", "-c", cmd], timeout=120)
|
||||
out[svc] = {"image": ref, "probe": meaning,
|
||||
"answer": " ".join(((r.stdout or "") + (r.stderr or "")).split())[:600],
|
||||
"probe_rc": r.returncode}
|
||||
return out
|
||||
|
||||
|
||||
MIGRATION_RE = re.compile(
|
||||
r"migrat|upgrad|schema|alter table|CREATE TABLE|InnoDB: Upgrad|mysql_upgrade|"
|
||||
r"mariadb-upgrade|Running .* migration|Applying|db:migrate",
|
||||
@@ -189,6 +236,8 @@ def run_edge(edge_id: str) -> dict:
|
||||
"from": e["frm"], "to": e["to"], "verdict": "inconclusive",
|
||||
"seed_read_before": False, "seed_read_after": False, "healthy_after": False,
|
||||
"migration_observed": None, "abort": "not-attempted", "abort_detail": None,
|
||||
# engine_state is a REPORT, not a judgement — see ENGINE_PROBES.
|
||||
"engine_state_after": None,
|
||||
"duration_s": 0, "measured_at": None, "evidence": f"evidence/{edge_id}"}
|
||||
t0 = time.time()
|
||||
log = []
|
||||
@@ -254,6 +303,12 @@ def run_edge(edge_id: str) -> dict:
|
||||
full_to = compose(workdir, project, "logs", "--no-color", timeout=180)
|
||||
(ev / "to-full.log").write_text((full_to.stdout + full_to.stderr)[-400000:])
|
||||
|
||||
rec["engine_state_after"] = engine_state(e["to"]) or None
|
||||
if rec["engine_state_after"]:
|
||||
for svc, st in rec["engine_state_after"].items():
|
||||
say(f"engine state {svc}: {st['answer'][:180]}")
|
||||
(ev / "engine-state.json").write_text(json.dumps(rec["engine_state_after"], indent=2))
|
||||
|
||||
mig = migration_lines(project, workdir, swap_at)
|
||||
rec["migration_observed"] = mig[0] if mig else None
|
||||
(ev / "migration-lines.txt").write_text("\n".join(mig))
|
||||
|
||||
Reference in New Issue
Block a user