fix: unblock the demo reset rollback after an interrupted restore #171

Merged
Corentin merged 1 commit from fix/demo-reset-rollback-escape into dev 2026-09-26 16:41:03 +02:00
Owner

Le piege

scripts/demo-reset.sh rejoue un dump complet a l'etape [4/6] : DROP, puis
CREATE, puis INSERT, table par table. Si elle meurt entre le DROP et l'INSERT de
schema_migrations (coupure, disque plein, base qui tombe), le schema reste a
moitie change et la table de suivi a disparu.

Le retour arriere vers la sauvegarde de securite que l'outil venait de prendre a
l'etape [3/6] repassait alors par la verification de compatibilite de l'etape
[1/6], qui voyait la divergence et refusait en code 3. Le retour arriere etait
bloque exactement au moment ou il sert, et la documentation renvoyait vers le
dump nocturne du cron : tout ce qui s'est passe depuis la nuit etait perdu.

La sortie de secours

Une sauvegarde de securite est, par construction, l'image de CETTE base-la prise
quelques secondes plus tot. Chaque sauvegarde porte desormais un marqueur
rollback.tag ecrit par l'outil, qui contient :

  • une empreinte de la base dont elle vient (nom du schema, instant de creation
    des tables systeme du schema mysql, projet compose et conteneur wakdo-db,
    condenses en sha256) — une restauration applicative ne la change pas, donc
    elle survit telle quelle a une restauration interrompue ;
  • l'empreinte sha256 du db.sql.gz pose a cote, qui lie le marqueur a ce
    contenu precis.

L'etape [1/6] leve la comparaison de schema seulement si les quatre conditions
tiennent ensemble : meta.txt annonce kind=pre-reset, le marqueur est present,
son empreinte de base correspond a la base visee maintenant, et son empreinte de
dump correspond au dump pose a cote.

La levee est aussi directionnelle : elle ne joue que quand la base a PERDU
des migrations que la sauvegarde possede, la signature d'une restauration
interrompue. Une migration appliquee DEPUIS (vrai deploiement entre-temps) reste
refusee avec le meme code de sortie, marqueur ou pas. Il n'y a pas d'option
--force
: le garde-fou n'est pas desarme, seul le chemin sur par construction
est ouvert. Chaque levee est annoncee en clair et tracee dans
demo-backups/rollback-escape.log ; --dry-run l'annonce sans rien ecrire.

Les controles d'integrite du dump ajoutes par #167 tournent avant et
independamment : une sauvegarde marquee dont le db.sql.gz est vide, tronque,
non-gzip, ou sans -- Dump completed reste refusee.

Tests

tests/shell/demo-snapshot-lib.test.sh (job shell-tests, sans Docker) passe de
18 a 37 assertions. Les nouvelles couvrent : une sauvegarde marquee acceptee
alors que schema_migrations diverge ; un instantane ordinaire avec la meme
divergence toujours refuse ; un marqueur absent, recopie sur un autre instantane,
retouche a la main, ou venu d'une autre base qui n'accorde rien ; et le maintien
des controles d'integrite sur le retour arriere.

Verification en conditions reelles

Pile jetable wakdorbk (detruite depuis, aucun reste), vraie remise a zero
coupee en plein [4/6] en tuant le client de restauration pendant la remise en
place de schema_migrations :

Moment schema_migrations tables product product_ingredient categorie 1
avant 15 24 48 164 Menus (abime par le jury)
apres l'interruption (code 5) absente 23 58 179 Menus
apres le retour arriere (code 0) 15 24 48 164 Menus (abime par le jury)

Sur ce meme etat hybride : le code de dev refuse le retour arriere en code 3 ;
un instantane ordinaire (--snapshot reference) reste refuse en code 3 avec le
correctif ; l'empreinte de base est identique avant et apres l'interruption, ce
qui est la propriete sur laquelle repose le marqueur.

Ce que la sortie de secours ne couvre pas

Detaille dans docs/ops/demo-reset.md : elle porte sur la compatibilite de
schema, pas sur l'integrite du fichier ni sur l'identite de cible (code 7, qui
passe avant) ; ce n'est pas une signature (pas de secret dans le depot, donc elle
ecarte l'erreur de manipulation, pas une falsification deliberee et complete) ;
elle ne verifie pas que le code deploye correspond au schema restaure ; elle ne
repare pas une sauvegarde non marquee ; et elle ne couvre pas la perte du volume
de donnees, qui reste du ressort du dump nocturne.

## Le piege `scripts/demo-reset.sh` rejoue un dump complet a l'etape `[4/6]` : DROP, puis CREATE, puis INSERT, table par table. Si elle meurt entre le DROP et l'INSERT de `schema_migrations` (coupure, disque plein, base qui tombe), le schema reste a moitie change et la table de suivi a disparu. Le retour arriere vers la sauvegarde de securite que l'outil venait de prendre a l'etape `[3/6]` repassait alors par la verification de compatibilite de l'etape `[1/6]`, qui voyait la divergence et refusait en code 3. Le retour arriere etait bloque exactement au moment ou il sert, et la documentation renvoyait vers le dump nocturne du cron : tout ce qui s'est passe depuis la nuit etait perdu. ## La sortie de secours Une sauvegarde de securite est, par construction, l'image de CETTE base-la prise quelques secondes plus tot. Chaque sauvegarde porte desormais un marqueur `rollback.tag` ecrit par l'outil, qui contient : - une empreinte de la base dont elle vient (nom du schema, instant de creation des tables systeme du schema `mysql`, projet compose et conteneur `wakdo-db`, condenses en sha256) — une restauration applicative ne la change pas, donc elle survit telle quelle a une restauration interrompue ; - l'empreinte sha256 du `db.sql.gz` pose a cote, qui lie le marqueur a ce contenu precis. L'etape `[1/6]` leve la comparaison de schema seulement si les quatre conditions tiennent ensemble : `meta.txt` annonce `kind=pre-reset`, le marqueur est present, son empreinte de base correspond a la base visee maintenant, et son empreinte de dump correspond au dump pose a cote. La levee est aussi **directionnelle** : elle ne joue que quand la base a PERDU des migrations que la sauvegarde possede, la signature d'une restauration interrompue. Une migration appliquee DEPUIS (vrai deploiement entre-temps) reste refusee avec le meme code de sortie, marqueur ou pas. **Il n'y a pas d'option `--force`** : le garde-fou n'est pas desarme, seul le chemin sur par construction est ouvert. Chaque levee est annoncee en clair et tracee dans `demo-backups/rollback-escape.log` ; `--dry-run` l'annonce sans rien ecrire. Les controles d'integrite du dump ajoutes par #167 tournent avant et independamment : une sauvegarde marquee dont le `db.sql.gz` est vide, tronque, non-gzip, ou sans `-- Dump completed` reste refusee. ## Tests `tests/shell/demo-snapshot-lib.test.sh` (job `shell-tests`, sans Docker) passe de 18 a 37 assertions. Les nouvelles couvrent : une sauvegarde marquee acceptee alors que `schema_migrations` diverge ; un instantane ordinaire avec la meme divergence toujours refuse ; un marqueur absent, recopie sur un autre instantane, retouche a la main, ou venu d'une autre base qui n'accorde rien ; et le maintien des controles d'integrite sur le retour arriere. ## Verification en conditions reelles Pile jetable `wakdorbk` (detruite depuis, aucun reste), vraie remise a zero coupee en plein `[4/6]` en tuant le client de restauration pendant la remise en place de `schema_migrations` : | Moment | `schema_migrations` | tables | `product` | `product_ingredient` | categorie 1 | |---|---|---|---|---|---| | avant | 15 | 24 | 48 | 164 | `Menus (abime par le jury)` | | apres l'interruption (code 5) | absente | 23 | 58 | 179 | `Menus` | | apres le retour arriere (code 0) | 15 | 24 | 48 | 164 | `Menus (abime par le jury)` | Sur ce meme etat hybride : le code de `dev` refuse le retour arriere en code 3 ; un instantane ordinaire (`--snapshot reference`) reste refuse en code 3 avec le correctif ; l'empreinte de base est identique avant et apres l'interruption, ce qui est la propriete sur laquelle repose le marqueur. ## Ce que la sortie de secours ne couvre pas Detaille dans `docs/ops/demo-reset.md` : elle porte sur la compatibilite de schema, pas sur l'integrite du fichier ni sur l'identite de cible (code 7, qui passe avant) ; ce n'est pas une signature (pas de secret dans le depot, donc elle ecarte l'erreur de manipulation, pas une falsification deliberee et complete) ; elle ne verifie pas que le code deploye correspond au schema restaure ; elle ne repare pas une sauvegarde non marquee ; et elle ne couvre pas la perte du volume de donnees, qui reste du ressort du dump nocturne.
fix: unblock the demo reset rollback after an interrupted restore
All checks were successful
CI / secret-scan (push) Successful in 25s
CI / php-lint (push) Successful in 28s
CI / js-tests (push) Successful in 46s
CI / php-lint (pull_request) Successful in 25s
CI / static-tests (pull_request) Successful in 2m39s
CI / shell-tests (pull_request) Successful in 7s
CI / static-tests (push) Successful in 2m31s
CI / shell-tests (push) Successful in 7s
CI / secret-scan (pull_request) Successful in 25s
CI / js-tests (pull_request) Successful in 49s
0bdbd1200e
Step [4/6] of scripts/demo-reset.sh replays a full dump: DROP then CREATE
then INSERT, table by table. When it dies between the DROP and the INSERT of
schema_migrations (power cut, full disk, database going down), the schema is
left half changed and the migration table is gone. Rolling back to the safety
backup the tool had just taken at step [3/6] then went through the schema
compatibility check of step [1/6], which saw the divergence and refused with
exit code 3. The rollback was blocked exactly when it was most needed, and the
documentation sent the operator to the nightly cron dump, losing everything
since the night.

A safety backup is, by construction, the image of that very database taken
seconds earlier, so comparing it to the current schema is meaningless when the
current schema is the one an interrupted restore left half changed. Each
safety backup now carries a rollback.tag written by capture_snapshot in
pre-reset mode, holding a fingerprint of the database it came from (schema
name, creation time of the mysql system tables, compose project and wakdo-db
container, hashed) and the sha256 of the dump next to it. Step [1/6] lifts the
schema comparison only when all four conditions hold together: meta.txt says
kind=pre-reset, the tag is there, its database fingerprint matches the
database targeted now, and its dump fingerprint matches the dump next to it.

The lift is also directional: it only applies when the database LOST
migrations the backup has, which is the signature of an interrupted restore.
A migration applied SINCE (a real deployment in between) is still refused with
the same exit code, tag or no tag. There is no --force option: the guard is
not disarmed, only the path that is safe by construction is opened. Every lift
is announced in the output and recorded in demo-backups/rollback-escape.log;
--dry-run announces it without writing anything.

The dump integrity checks stay untouched and keep running before and
independently of this: a marked backup whose db.sql.gz is empty, truncated,
not gzip, or missing its "-- Dump completed" trailer is still refused.

tests/shell/demo-snapshot-lib.test.sh covers the new pure functions (37
assertions): a marked backup is accepted while schema_migrations diverges, an
ordinary snapshot with the same divergence is still refused, and a tag that is
absent, copied onto another snapshot, hand-edited, or coming from another
database grants nothing.

Verified end to end on a throwaway stack: a real reset cut mid [4/6] left
schema_migrations dropped (24 tables and 15 migrations before, 23 tables and
no migration table after); the rollback was refused with exit code 3 by the
current code and restored the exact pre-reset state with the fix.
Corentin scheduled this pull request to auto merge when all checks succeed 2026-09-26 16:31:10 +02:00
Sign in to join this conversation.
No reviewers
No labels
auto-merge
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
AcadeNice/corentin_wakdo!171
No description provided.