Files
soft_quay/core/installer/recovery.go
T

198 lines
5.5 KiB
Go

package installer
import (
"errors"
"fmt"
)
var ErrRecoveryInconsistent = errors.New("install recovery state is inconsistent")
type RecoveryAction string
const (
RecoveryNone RecoveryAction = "none"
RecoveryAborted RecoveryAction = "aborted"
RecoveryRolledBack RecoveryAction = "rolled_back"
RecoveryCommitted RecoveryAction = "committed"
)
type RecoveryResult struct {
Action RecoveryAction
Phase string
}
// Recover resolves an interrupted transaction from journal and directory state.
func Recover(root string) (RecoveryResult, error) {
return recoverWithFence(root, defaultDurability())
}
func recoverWithFence(root string, fence durabilityFence) (RecoveryResult, error) {
fence = effectiveDurability(fence)
layout, err := inspectAppLayout(root)
if err != nil {
return RecoveryResult{}, err
}
record, exists, err := loadTransaction(layout)
if err != nil {
return RecoveryResult{}, err
}
state, err := inspectDirectories(layout)
if err != nil {
return RecoveryResult{}, err
}
if !exists {
if state.backup {
return RecoveryResult{}, fmt.Errorf(
"%w: backup exists without transaction",
ErrRecoveryInconsistent,
)
}
if state.staging {
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
return RecoveryResult{}, err
}
return RecoveryResult{Action: RecoveryAborted}, nil
}
return RecoveryResult{Action: RecoveryNone}, nil
}
result := RecoveryResult{Phase: string(record.Phase)}
switch record.Phase {
case phaseCommitted:
if !state.current || state.staging {
return RecoveryResult{}, fmt.Errorf(
"%w: committed current=%t staging=%t",
ErrRecoveryInconsistent,
state.current,
state.staging,
)
}
if err := removeManagedDirectoryWithFence(layout, layout.backup, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeTransactionWithFence(layout, fence); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryCommitted
return result, nil
case phaseRollbackRequired:
return recoverRollbackRequired(layout, record, state, result, fence)
case phasePrepared, phaseCurrentBackedUp, phaseStagingActivated:
return recoverUncommitted(layout, record, state, result, fence)
default:
return RecoveryResult{}, fmt.Errorf("%w: phase=%q", ErrTransactionCorrupt, record.Phase)
}
}
func recoverUncommitted(
layout appLayout,
record transactionRecord,
state directoryState,
result RecoveryResult,
fence durabilityFence,
) (RecoveryResult, error) {
if record.HadCurrent {
if state.backup {
if state.current && state.staging {
return RecoveryResult{}, fmt.Errorf(
"%w: current, staging and backup all exist",
ErrRecoveryInconsistent,
)
}
if state.current {
if err := renameManagedDirectory(
layout,
layout.current,
layout.staging,
fence,
"app root after recovery staging rename",
); err != nil {
return RecoveryResult{}, fmt.Errorf("move unverified current aside: %w", err)
}
}
if err := renameManagedDirectory(
layout,
layout.backup,
layout.current,
fence,
"app root after recovery current restore",
); err != nil {
return RecoveryResult{}, fmt.Errorf("restore backup during recovery: %w", err)
}
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeTransactionWithFence(layout, fence); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryRolledBack
return result, nil
}
if record.Phase == phasePrepared && state.current && state.staging {
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeTransactionWithFence(layout, fence); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryAborted
return result, nil
}
return RecoveryResult{}, fmt.Errorf(
"%w: old current has no recoverable backup",
ErrRecoveryInconsistent,
)
}
if state.backup || (state.current && state.staging) {
return RecoveryResult{}, fmt.Errorf(
"%w: initial install has conflicting directories",
ErrRecoveryInconsistent,
)
}
if err := removeManagedDirectoryWithFence(layout, layout.current, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeTransactionWithFence(layout, fence); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryAborted
return result, nil
}
func recoverRollbackRequired(
layout appLayout,
record transactionRecord,
state directoryState,
result RecoveryResult,
fence durabilityFence,
) (RecoveryResult, error) {
if record.HadCurrent && !state.backup {
if !state.current {
return RecoveryResult{}, fmt.Errorf(
"%w: rollback lost current and backup",
ErrRecoveryInconsistent,
)
}
if err := removeManagedDirectoryWithFence(layout, layout.staging, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeTransactionWithFence(layout, fence); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryRolledBack
return result, nil
}
if err := rollbackActivated(layout, record.HadCurrent, fence); err != nil {
return RecoveryResult{}, err
}
if err := removeTransactionWithFence(layout, fence); err != nil {
return RecoveryResult{}, err
}
result.Action = RecoveryRolledBack
return result, nil
}