Fence installation transaction durability (T-613)

This commit is contained in:
ila
2026-07-18 16:28:49 +08:00
parent 84befee70f
commit 20596a7de4
18 changed files with 697 additions and 64 deletions
+61 -23
View File
@@ -42,18 +42,20 @@ func (err *RollbackError) Unwrap() error {
// Switcher activates a verified staging directory and runs an injected check.
type Switcher struct {
health HealthCheck
afterStep func(switchStep) error
health HealthCheck
afterStep func(switchStep) error
durability durabilityFence
}
func NewSwitcher(health HealthCheck) *Switcher {
return &Switcher{health: health}
return &Switcher{health: health, durability: defaultDurability()}
}
func (switcher *Switcher) Switch(root string) error {
if switcher.health == nil {
return ErrHealthCheckRequired
}
fence := effectiveDurability(switcher.durability)
layout, err := inspectAppLayout(root)
if err != nil {
return err
@@ -75,7 +77,7 @@ func (switcher *Switcher) Switch(root string) error {
}
record := newTransaction(phasePrepared, state.current)
if err := writeTransaction(layout, record); err != nil {
if err := writeTransactionWithFence(layout, record, fence); err != nil {
return err
}
if err := switcher.runStep(stepPrepared); err != nil {
@@ -83,7 +85,13 @@ func (switcher *Switcher) Switch(root string) error {
}
if state.current {
if err := os.Rename(layout.current, layout.backup); err != nil {
if err := renameManagedDirectory(
layout,
layout.current,
layout.backup,
fence,
"app root after current backup rename",
); err != nil {
return fmt.Errorf("backup current directory: %w", err)
}
if err := switcher.runStep(stepCurrentRenamed); err != nil {
@@ -91,21 +99,27 @@ func (switcher *Switcher) Switch(root string) error {
}
}
record.Phase = phaseCurrentBackedUp
if err := writeTransaction(layout, record); err != nil {
if err := writeTransactionWithFence(layout, record, fence); err != nil {
return err
}
if err := switcher.runStep(stepCurrentBackedUp); err != nil {
return err
}
if err := os.Rename(layout.staging, layout.current); err != nil {
if err := renameManagedDirectory(
layout,
layout.staging,
layout.current,
fence,
"app root after staging activation rename",
); err != nil {
return fmt.Errorf("activate staging directory: %w", err)
}
if err := switcher.runStep(stepStagingRenamed); err != nil {
return err
}
record.Phase = phaseStagingActivated
if err := writeTransaction(layout, record); err != nil {
if err := writeTransactionWithFence(layout, record, fence); err != nil {
return err
}
if err := switcher.runStep(stepStagingActivated); err != nil {
@@ -115,35 +129,35 @@ func (switcher *Switcher) Switch(root string) error {
healthErr := switcher.health(layout.current)
if healthErr != nil {
record.Phase = phaseRollbackRequired
if err := writeTransaction(layout, record); err != nil {
if err := writeTransactionWithFence(layout, record, fence); err != nil {
return &RollbackError{Health: healthErr, Rollback: err}
}
if err := switcher.runStep(stepRollbackRequired); err != nil {
return err
}
if err := rollbackActivated(layout, record.HadCurrent); err != nil {
if err := rollbackActivated(layout, record.HadCurrent, fence); err != nil {
return &RollbackError{Health: healthErr, Rollback: err}
}
if err := removeTransaction(layout); err != nil {
if err := removeTransactionWithFence(layout, fence); err != nil {
return &RollbackError{Health: healthErr, Rollback: err}
}
return fmt.Errorf("%w: %v", ErrHealthCheckFailed, healthErr)
}
record.Phase = phaseCommitted
if err := writeTransaction(layout, record); err != nil {
if err := writeTransactionWithFence(layout, record, fence); err != nil {
return err
}
if err := switcher.runStep(stepCommitted); err != nil {
return err
}
if record.HadCurrent {
if err := removeManagedDirectory(layout, layout.backup); err != nil {
return fmt.Errorf("%w: cleanup committed backup: %v", ErrRecoveryRequired, err)
if err := removeManagedDirectoryWithFence(layout, layout.backup, fence); err != nil {
return fmt.Errorf("%w: cleanup committed backup: %w", ErrRecoveryRequired, err)
}
}
if err := removeTransaction(layout); err != nil {
return fmt.Errorf("%w: %v", ErrRecoveryRequired, err)
if err := removeTransactionWithFence(layout, fence); err != nil {
return fmt.Errorf("%w: %w", ErrRecoveryRequired, err)
}
return nil
}
@@ -155,7 +169,7 @@ func (switcher *Switcher) runStep(step switchStep) error {
return switcher.afterStep(step)
}
func rollbackActivated(layout appLayout, hadCurrent bool) error {
func rollbackActivated(layout appLayout, hadCurrent bool, fence durabilityFence) error {
state, err := inspectDirectories(layout)
if err != nil {
return err
@@ -168,17 +182,35 @@ func rollbackActivated(layout appLayout, hadCurrent bool) error {
if state.staging {
return fmt.Errorf("%w: current and staging both exist", ErrRollbackFailed)
}
if err := os.Rename(layout.current, layout.staging); err != nil {
if err := renameManagedDirectory(
layout,
layout.current,
layout.staging,
fence,
"app root after rollback staging rename",
); err != nil {
return fmt.Errorf("move failed current aside: %w", err)
}
}
if err := os.Rename(layout.backup, layout.current); err != nil {
if err := renameManagedDirectory(
layout,
layout.backup,
layout.current,
fence,
"app root after rollback current restore",
); err != nil {
if _, statErr := os.Stat(layout.staging); statErr == nil {
_ = os.Rename(layout.staging, layout.current)
_ = renameManagedDirectory(
layout,
layout.staging,
layout.current,
fence,
"app root after rollback restore",
)
}
return fmt.Errorf("restore previous current: %w", err)
}
return removeManagedDirectory(layout, layout.staging)
return removeManagedDirectoryWithFence(layout, layout.staging, fence)
}
if state.backup {
@@ -188,9 +220,15 @@ func rollbackActivated(layout appLayout, hadCurrent bool) error {
if state.staging {
return fmt.Errorf("%w: current and staging both exist", ErrRollbackFailed)
}
if err := os.Rename(layout.current, layout.staging); err != nil {
if err := renameManagedDirectory(
layout,
layout.current,
layout.staging,
fence,
"app root after initial rollback rename",
); err != nil {
return fmt.Errorf("move failed initial install aside: %w", err)
}
}
return removeManagedDirectory(layout, layout.staging)
return removeManagedDirectoryWithFence(layout, layout.staging, fence)
}