Files
soft_quay/core/updater/recovery.go

130 lines
4.2 KiB
Go

package updater
import (
"fmt"
"path/filepath"
)
func recoverLayout(layout updateLayout, syncer DirectorySyncer) error {
record, exists, err := loadTransaction(layout)
if err != nil || !exists {
return err
}
layout.requestID = record.RequestID
layout.staging = filepath.Join(layout.root, stagingDirectoryName, record.RequestID)
layout.backup = filepath.Join(layout.root, backupsDirectoryName, record.RequestID)
switch record.Phase {
case phasePrepared:
return recoverPrepared(layout, syncer)
case phaseTargetBackedUp:
if err := restoreBackup(layout, syncer); err != nil {
return err
}
return removeTransaction(layout, syncer)
case phaseStagingActivated, phaseLaunched:
if healthMatches(layout, record.RequestID) {
if err := removeManagedTree(layout.backup, layout, syncer); err != nil {
return fmt.Errorf("finalize recovered self-update: %w", err)
}
return removeTransaction(layout, syncer)
}
return rollbackLayout(layout, syncer)
case phaseCommitted:
if err := removeManagedTree(layout.backup, layout, syncer); err != nil {
return fmt.Errorf("finalize committed self-update: %w", err)
}
return removeTransaction(layout, syncer)
default:
return ErrTransactionCorrupt
}
}
// 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 {
return fmt.Errorf("%w: old app backup is unavailable", ErrRecoveryRequired)
}
targetExists, err := realDirectoryState(layout.target, "target")
if err != nil {
return err
}
if targetExists {
stagingExists, err := realDirectoryState(layout.staging, "staging")
if err != nil {
return err
}
if stagingExists {
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: %w", ErrRecoveryRequired, err)
}
}
if err := renameDirectory(layout.backup, layout.target, layout, syncer, "restore previous app"); err != nil {
return fmt.Errorf("%w: %w", ErrRecoveryRequired, err)
}
if err := removeTransaction(layout, syncer); err != nil {
return err
}
if err := removeHealth(layout, syncer); err != nil {
return err
}
return nil
}
func restoreBackup(layout updateLayout, syncer DirectorySyncer) error {
targetExists, err := realDirectoryState(layout.target, "target")
if err != nil {
return err
}
if targetExists {
return fmt.Errorf("%w: target exists before backup restoration", ErrRecoveryRequired)
}
if exists, err := realDirectoryState(layout.backup, "backup"); err != nil || !exists {
if err != nil {
return err
}
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: %w", ErrRecoveryRequired, err)
}
return nil
}
func healthMatches(layout updateLayout, requestID string) bool {
record, err := readHealth(layout.health)
return err == nil && record.RequestID == requestID
}
func removeHealth(layout updateLayout, syncer DirectorySyncer) error {
return removeRegularFile(layout.health, layout.root, syncer)
}