R-379 fix: the rollback must re-discover the DB container
gates / gates (push) Successful in 12s

Found by v0.220.0's own live walk on its first real run. writeSafetyDump
captures its DiscoveredDB before the stop; the DB-only start then re-creates the
container with a new id, so the rollback's docker exec hit a dead container and
sat in waitDBReady for 30s. The app was held for an infrastructure reason while
its data was recoverable.

Re-discover and match by {stack, engine} - what reimportDBDumpsFrom already did.
Fail closed when the container cannot be found.

No unit test caught it because they all inject the import seam and never look at
container identity. The new test asserts the identity handed to the import.
This commit is contained in:
2026-08-22 18:14:31 +02:00
parent 2c724c9283
commit 5b52a5964d
3 changed files with 154 additions and 2 deletions
@@ -202,6 +202,14 @@ func (m *Manager) writeSafetyDump(ctx context.Context, stackName, nsRoot string)
return set, nil
}
// shortID trims a docker id for logs. Never used for identity — only for reading.
func shortID(id string) string {
if len(id) > 12 {
return id[:12]
}
return id
}
// maxUndoCopiesPerApp is how many `pre-restore-` copies an app keeps.
//
// THREE, and the reasoning rather than a number pulled from the air. One is not enough: the case
@@ -342,13 +350,53 @@ func (m *Manager) rollbackSafetyDump(ctx context.Context, stack string, set safe
return ImportDump(ctx, db, path, m.logger, m.isDebug())
}
}
// RE-DISCOVER THE CONTAINERS. The undo FILE is stable; the container it must be poured into is
// NOT. `writeSafetyDump` captured its DiscoveredDB before the stop, and by the time the rollback
// runs the stack has been stopped and the DB service re-created — a NEW container id.
//
// MEASURED LIVE ON demo-hp 2026-08-22, which is the only reason this is here: the first live run
// of this code captured `docmost-postgres id=9adbc14f9af6` at 16:05:44, the DB-only start
// re-created it as `309795897b82` at 16:05:47, and the rollback's `docker exec` against the dead
// id sat in `waitDBReady` until it timed out 30 s later — so the app was HELD for an
// infrastructure reason when its data was recoverable. The unit tests could not see it: they
// inject the import seam and never touch container identity. `reimportDBDumpsFrom` already
// re-discovers for exactly this reason.
discover := m.discoverDBs
if discover == nil {
discover = func(ctx context.Context) ([]DiscoveredDB, error) {
return DiscoverDatabases(ctx, m.logger, m.isDebug(), m.knownStackNames())
}
}
live, dErr := discover(ctx)
if dErr != nil {
return fmt.Errorf("a visszavonás előtt nem sikerült felderíteni az adatbázisokat: %w", dErr)
}
liveFor := func(want DiscoveredDB) (DiscoveredDB, bool) {
for _, db := range live {
if db.StackName == want.StackName && db.DBType == want.DBType {
return db, true
}
}
return DiscoveredDB{}, false
}
for _, f := range set.Files {
if _, sErr := os.Stat(f.Path); sErr != nil {
return fmt.Errorf("a visszavonáshoz szükséges mentés nem található (%s): %w", filepath.Base(f.Path), sErr)
}
target, ok := liveFor(f.DB)
if !ok {
// Fail closed: pouring an undo into a container we cannot identify is worse than saying
// we could not do it.
return fmt.Errorf("a(z) %s adatbázis-tárolója nem található a visszavonáshoz", f.DB.ContainerName)
}
if target.ContainerID != f.DB.ContainerID {
m.logger.Printf("[DEBUG] [offbox] %s: %s was re-created during the restore (%s → %s) — rolling back into the live container",
stack, f.DB.ContainerName, shortID(f.DB.ContainerID), shortID(target.ContainerID))
}
m.logger.Printf("[INFO] [offbox] %s: rolling back to the pre-restore state from %s", stack, filepath.Base(f.Path))
if err := imp(ctx, f.DB, f.Path); err != nil {
return fmt.Errorf("a korábbi állapot visszaállítása sikertelen (%s): %w", f.DB.ContainerName, err)
if err := imp(ctx, target, f.Path); err != nil {
return fmt.Errorf("a korábbi állapot visszaállítása sikertelen (%s): %w", target.ContainerName, err)
}
}
m.logger.Printf("[INFO] [offbox] %s: rollback complete — %d database(s) returned to the pre-restore state", stack, len(set.Files))