diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b7d35..07c06ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` + diff --git a/scripts/upgrade-test.py b/scripts/upgrade-test.py index 7a5a53e..4d7de50 100755 --- a/scripts/upgrade-test.py +++ b/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))