Harden self-update recovery (T-617)

This commit is contained in:
ila
2026-07-20 00:05:41 +08:00
parent ea1d6c8ee4
commit 24e9862192
12 changed files with 369 additions and 42 deletions
+31 -4
View File
@@ -15,7 +15,7 @@ func recoverLayout(layout updateLayout, syncer DirectorySyncer) error {
layout.backup = filepath.Join(layout.root, backupsDirectoryName, record.RequestID)
switch record.Phase {
case phasePrepared:
return removeTransaction(layout, syncer)
return recoverPrepared(layout, syncer)
case phaseTargetBackedUp:
if err := restoreBackup(layout, syncer); err != nil {
return err
@@ -39,6 +39,33 @@ func recoverLayout(layout updateLayout, syncer DirectorySyncer) error {
}
}
// recoverPrepared reconciles the only phase where the durable journal may be
// present while the first rename was interrupted between Rename and its
// directory fence. The physical managed layout is authoritative here: blindly
// removing the journal would strand the old app in backup after such a fence
// failure.
func recoverPrepared(layout updateLayout, syncer DirectorySyncer) error {
targetExists, err := realDirectoryState(layout.target, "target")
if err != nil {
return err
}
backupExists, err := realDirectoryState(layout.backup, "backup")
if err != nil {
return err
}
switch {
case targetExists && !backupExists:
return removeTransaction(layout, syncer)
case !targetExists && backupExists:
if err := restoreBackup(layout, syncer); err != nil {
return err
}
return removeTransaction(layout, syncer)
default:
return fmt.Errorf("%w: prepared transaction has target=%t backup=%t", ErrRecoveryRequired, targetExists, backupExists)
}
}
func rollbackLayout(layout updateLayout, syncer DirectorySyncer) error {
backupExists, err := realDirectoryState(layout.backup, "backup")
if err != nil || !backupExists {
@@ -57,11 +84,11 @@ func rollbackLayout(layout updateLayout, syncer DirectorySyncer) error {
return fmt.Errorf("%w: staged and active app both exist", ErrRecoveryRequired)
}
if err := renameDirectory(layout.target, layout.staging, layout, syncer, "preserve failed staged app"); err != nil {
return fmt.Errorf("%w: %v", ErrRecoveryRequired, err)
return fmt.Errorf("%w: %w", ErrRecoveryRequired, err)
}
}
if err := renameDirectory(layout.backup, layout.target, layout, syncer, "restore previous app"); err != nil {
return fmt.Errorf("%w: %v", ErrRecoveryRequired, err)
return fmt.Errorf("%w: %w", ErrRecoveryRequired, err)
}
if err := removeTransaction(layout, syncer); err != nil {
return err
@@ -87,7 +114,7 @@ func restoreBackup(layout updateLayout, syncer DirectorySyncer) error {
return fmt.Errorf("%w: backup is unavailable", ErrRecoveryRequired)
}
if err := renameDirectory(layout.backup, layout.target, layout, syncer, "restore previous app"); err != nil {
return fmt.Errorf("%w: %v", ErrRecoveryRequired, err)
return fmt.Errorf("%w: %w", ErrRecoveryRequired, err)
}
return nil
}