Repository navigation
template-manager: reverse the next release's node_dirty_ref_revisions migration so its rollback stays image-only - #1277
Merged
Conversation
|
Thanks for opening this pull request! A SPACE maintainer will take a look. CONTRIBUTING.md explains how we review outside contributions. |
ac-pplx
force-pushed
the
ac/template-mgr-reverse-dirty-ref-revisions
branch
2 times, most recently
from
November 1, 2026 09:36
1aff563 to
6530fd8
Compare
zibi-pplx
previously approved these changes
Nov 1, 2026
zibi-pplx
dismissed
their stale review
November 1, 2026 22:30
Withdrawing this approval for now. Any feedback above still stands.
ac-pplx
marked this pull request as ready for review
November 2, 2026 18:39
ac-pplx
force-pushed
the
ac/template-mgr-reverse-dirty-ref-revisions
branch
from
November 4, 2026 14:55
6530fd8 to
3beb564
Compare
… migration so its rollback stays image-only
The next release adds template-manager migration
20261031050126_node_dirty_ref_revisions (a revision column on
node_dirty_refs, a singleton counter and two triggers). An image rollback
past it would leave a database that records a migration this runner
doesn't bundle, which it refuses ("unbundled migration"), so
template-manager would crash-loop on every rolled-back node.
Generalize the runner's single retired-migration reversal into a list of
unbundled migrations, each with its version, description, original up SQL
(for the recorded checksum), down SQL and a check that refuses when
reversing would drop data worth keeping. The runner checks every recorded
one (checksum, then that check) before any write, then applies their down
SQL newest first, whatever their order in the list, and removes their
history rows in one transaction, so a failure partway leaves the database
as it was. Other recorded versions it doesn't bundle are still refused by
history validation, which runs after the reversal commits.
20261026220935 keeps its checks, refusal messages and log line; only the
context strings on SQLite failures during the down SQL, history delete
and commit are now generic.
Add 20261031050126 to the list, with its up and down SQL copied
byte-for-byte into db/template-manager/history/ so the checksum matches
what the next release's runner records. Reversing it keeps every outbox
row, so its check never refuses. It gives up the revision itself: a
release without the migration acknowledges rows by key, so a re-enqueue
made while a send is in flight can be lost, as before the migration, and
re-applying restarts revisions at 0. A catalog that bundles the version
applies it normally.
A release that lists a migration compares the recorded checksum only with
its own copy, so the release that bundles the migration must ship the
same up SQL byte for byte. A new test fails once a catalog bundles a
listed version with different up SQL, and db/README.md states the rule.
ac-pplx
force-pushed
the
ac/template-mgr-reverse-dirty-ref-revisions
branch
from
November 4, 2026 17:34
3beb564 to
2c64bdf
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Template-manager's node-local SQLite database can be migrated by a surge pod while the previous pod still serves. The rule in
crates/libs/sqlite/src/template_manager.rsis that a rollback must need nothing beyond the image. A runner refuses a database that records a migration it doesn't bundle (unbundled migration). So rolling the image back past #1235's20261031050126_node_dirty_ref_revisionswould crash-loop template-manager on every rolled-back node.This PR ships one release before #1235, using the two-release pattern from #1070, so the image a rollback returns to can reverse that migration.
20261026220935becomes a list (UNBUNDLED). Each entry has a version, description, original up SQL (for the recorded checksum), down SQL, and a check that refuses when reversing would drop data worth keeping.20261031050126is added to the list. Its up and down SQL are copied byte-for-byte from template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235 at1265e6ebintodb/template-manager/history/. Its checksumc92d5187…matches what template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235's migrator records. Reversing it keeps every outbox row, so its check never refuses: it drops only therevisioncolumn, thenode_dirty_ref_revisioncounter and the two triggers. It does give up the revision guarantee: a release without the migration acknowledges rows by key, as before template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235, and re-applying restarts revisions at 0.db/accepted-checksums.toml. So if template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235 changed its up SQL, even a comment, this release would refuse template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235's databases on rollback. A new test,every_listed_migration_matches_the_catalog_that_bundles_it, fails once a catalog bundles a listed version with different up SQL.db/README.mdstates the rule.20261026220935behaves as before. Same checks, same refusal messages, same log line, and its tests pass unchanged. Only the context strings on SQLite I/O failures during down SQL, history delete and commit are now generic: "failed to reverse retired template-manager migration 20261026220935" instead of "failed to reverse retired template cache partitions".db/README.mdgains an "Unbundled template-manager migrations" section: the list, the check-then-reverse transaction, the rule that a new migration joins the list one release before it is bundled and must then ship unchanged, and the in-flight behaviour of20261031050126. The existing "Retired template-manager migration" section is unchanged. The SQLite history paragraph now points to the new section.reverse_retired_partitionsis renamedreverse_unbundled; the only caller is incrates/libs/sqlite/src/lib.rs.Validation
Everything below ran at
1aff5639. The current head differs from it only in doc comments intemplate_manager.rsand prose indb/README.md.Unit tests. With
space-nextest-postgres-setupbuilt first,cargo nextest run -p pplx-space-sqlite -p pplx-space-template-mgr -p pplx-space-db-migrate -p pplx-space-db-migratorran 378 tests: 378 passed, 0 skipped.cargo clippyon the same packages with--all-targets -- -D warningsis clean,cargo fmt --checkis clean, andcargo archreports 0 required findings.template_manager/tests.rsare unchanged and pass. That file only has additions.a_database_with_dirty_ref_revisions_reverses_and_reapplies: next catalog → next catalog (untouched) → this runner (reversed) → next catalog (re-applied) → this runner. Each step checks rows, outbox rows, history, and that the schema equals a fresh database exactly.a_failed_or_modified_dirty_ref_revisions_refuses_reversal: a modified checksum or failed row is refused and the schema is left alone.every_unbundled_migration_reverses_together_or_not_at_all: with both migrations recorded, they reverse together; a non-legacy partition row refuses in the checks, before any down SQL.a_failure_after_the_first_reversal_rolls_back_every_reversal: with both migrations recorded and every check passing, a trigger aborts the older migration's history delete, after the newer migration's down SQL and history delete and the older down SQL have run. Migration fails with the injected error, and rows, outbox, schema and history are unchanged.dirty_ref_revisions_is_the_migration_the_next_release_records: pins the checksum.every_listed_migration_matches_the_catalog_that_bundles_it: has nothing to check here, since this catalog doesn't bundle20261031050126. It fires in template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235's CI once template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235 rebases. Checked locally by adding the version todb/template-manager/(not committed): with one comment line added to the up SQL it fails on20261031050126, and with a byte-identical copy it passes.a_bundled_schema_is_left_untouched._sqlx_migrations has failed, modified, or unbundled migration 20261031050126.reverse_unbundledcommit after each migration's reversal.a_failure_after_the_first_reversal_rolls_back_every_reversalthen fails:20261031050126was reversed and removed from history while20261026220935stayed. The other 11 template-manager tests, includingevery_unbundled_migration_reverses_together_or_not_at_all, still pass. Without the mutation all 12 pass.Real-binary rollback. I built
space-db-migratorfrom #1235 (1265e6eb), frommain(d397b69d) and from this branch, each in its own worktree and its ownCARGO_TARGET_DIR. A target dir shared across worktrees can produce a stale binary.strings <bin> | grep -c 'reversed newer node dirty ref revision migration'gives 0, 0 and 1. I then ran them in order on one database file:upon a fresh file, then two outbox rows inserted and one re-enqueued. This recorded checksumC92D5187….main's migratorupon a copy fails:unbundled migration 20261031050126.uplogsreversed newer node dirty ref revision migrationand exits 0. History is baseline only, the outbox rows are kept, and the schema matches a freshmaindatabase exactly.upagain changes nothing.upre-applies cleanly, with the same checksum and the same schema as in step 1. Existing rows get revision 0, and a new insert gets revision 1.Rollback on Kind with a real pod. I brought up a fresh cluster from this branch with
SPACE_KIND_TELEMETRY_MODE=otel bash infra/space/kind/scripts/up.sh(Kubernetes substrate; about 5.5 minutes); the pods report this head'sgit_sha. I built #1235's migrator in the repo's bookworm builder image, in its own target dir, to match the node's glibc. I ran it asspace-tmplmgron workerspace-local-worker's live database, beside the serving pod, and it recorded20261031050126. I then deleted the pod. This branch's template-manager reversed the migration at startup (WARN log) and was Ready 6s after the delete, with 0 restarts. Running #1235's migrator again re-applied it, and another restart reversed it again (Ready in 6s).Kind smoke, on the same cluster.
rg -l template space_smoke/scenarios, 23 IDs).smoke-kind-typed.shonly takes categories, so I ran each ID withspace_smoke run <id>after the script's own token and org setup. 21 passed in 16m42s.native.aws_secret_network_credential_injectionwas inconclusive because it needsSPACE_SMOKE_SECRET_ARN.node.windows_sandbox_lifecyclerefuses Kind; it needs a deployed backend.all:SPACE_SMOKE_KIND_COLD_RECEIPTS=<dir> bash scripts/tests/smoke-kind-typed.sh alltook 61m56s, with the run exiting 0.ecw.cold_create_environmentandecw.warm_pool_environment, which need the receipts directory.Severity and risk
Severity: high once #1235 ships. Without this, an automatic canary rollback across it crash-loops template-manager on every rolled-back node.
Risk: the change is runner-only plus history SQL; this release has no schema change. Before #1235 ships, no database records
20261031050126, so the new entry is never used. To roll back, revert this commit. That only matters after #1235 ships: without this commit, a rollback past #1235 crash-loops again.Rollout
During a rollback from #1235's release, the serving #1235 pod still reads
revisionafter this release's migrator reverses it. Its outbox reads and acknowledgements fail until it exits or a migrator re-applies the migration, and its rows stay queued. A send it started before the reversal and acknowledges after a re-apply can match a restarted revision and delete a re-enqueue made during that send. This is reasoned from #1235's code; Kind has no surge migrator to observe it. In the forward direction, this release writes the outbox without namingrevision, so it keeps serving while #1235's migrator applies it.Stack
DIRTY_REF_REVISIONS,dirty_ref_revisions_is_the_migration_the_next_release_recordsandevery_listed_migration_matches_the_catalog_that_bundles_it. Once template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235's catalog bundles the version, the entry is never used at runtime, but those tests are what keep template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235's file equal to the copy this release compares against.20261031050126's up SQL, comments included. This release would refuse a database that recorded a different checksum. A changed migration needs a new version.REVISIONEDandPARTITIONED_REVISIONED. template-manager: ack node dirty refs by revision so a re-enqueue during a send survives #1235'sBUNDLEDalready contains20261031050126, so both catalogs would list it twice, andBUNDLEDis no longer the release that reverses it. Give those tests a catalog without20261031050126as the rolling-back release; withBUNDLEDin that role,a_failure_after_the_first_reversal_rolls_back_every_reversalstill passes but reverses only one migration. The runner itself already skips any version the catalog bundles.20261031050126"needs the newer release'sspace-db-migrator template-manager down") is no longer true and should be dropped.20261026220935and deletes this module and itslib.rscall. Whichever lands second adapts:RETIRED_PARTITIONSleaves the list; the module, the call andDIRTY_REF_REVISIONSstay.Not covered
native.aws_secret_network_credential_injection: needs a real AWS secret.node.windows_sandbox_lifecycle: needs a deployed backend.AI assistance: Claude Code (an AI agent) wrote the code, tests, docs and this description, and ran the validation.
Summary by CodeRabbit