In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with checkpointed rollback and post-migration validation. Design stage; see ARCHITECTURE.md.
89 lines
2.6 KiB
Go
89 lines
2.6 KiB
Go
package checkpoint
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
func (rs *RunState) stepIndex(phase Phase, name string) int {
|
|
for i := range rs.Steps {
|
|
if rs.Steps[i].Phase == phase && rs.Steps[i].Name == name {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Status returns the current status of a step, or StepPending if it has
|
|
// never been started.
|
|
func (rs *RunState) Status(phase Phase, name string) StepStatus {
|
|
if i := rs.stepIndex(phase, name); i >= 0 {
|
|
return rs.Steps[i].Status
|
|
}
|
|
return StepPending
|
|
}
|
|
|
|
// Done reports whether a step already completed successfully. Callers use
|
|
// this to decide whether to skip work on resume.
|
|
func (rs *RunState) Done(phase Phase, name string) bool {
|
|
return rs.Status(phase, name) == StepDone
|
|
}
|
|
|
|
// Outcome returns the StepOutcome recorded for a step, zero-valued if none.
|
|
// This is what lets a resumed run reconstruct a skipped step's result
|
|
// without re-executing it.
|
|
func (rs *RunState) Outcome(phase Phase, name string) StepOutcome {
|
|
if i := rs.stepIndex(phase, name); i >= 0 {
|
|
return rs.Steps[i].StepOutcome
|
|
}
|
|
return StepOutcome{}
|
|
}
|
|
|
|
// Begin marks a step as running, creating its record on first attempt or
|
|
// resetting it on a retry after a prior failure.
|
|
func (rs *RunState) Begin(phase Phase, name string) {
|
|
now := time.Now().UTC()
|
|
if i := rs.stepIndex(phase, name); i >= 0 {
|
|
rs.Steps[i].Status = StepRunning
|
|
rs.Steps[i].StartedAt = &now
|
|
rs.Steps[i].CompletedAt = nil
|
|
rs.Steps[i].StepOutcome = StepOutcome{}
|
|
rs.Steps[i].Error = ""
|
|
} else {
|
|
rs.Steps = append(rs.Steps, StepRecord{
|
|
Phase: phase, Name: name, Status: StepRunning, StartedAt: &now,
|
|
})
|
|
}
|
|
rs.UpdatedAt = now
|
|
}
|
|
|
|
// Complete marks a step done with its outcome. It panics if Begin was never
|
|
// called for this step - that's a bug in the calling phase, not a runtime
|
|
// condition callers should need to handle.
|
|
func (rs *RunState) Complete(phase Phase, name string, outcome StepOutcome) {
|
|
i := rs.stepIndex(phase, name)
|
|
if i < 0 {
|
|
panic(fmt.Sprintf("checkpoint: Complete(%s/%s) called without Begin", phase, name))
|
|
}
|
|
now := time.Now().UTC()
|
|
rs.Steps[i].Status = StepDone
|
|
rs.Steps[i].CompletedAt = &now
|
|
rs.Steps[i].StepOutcome = outcome
|
|
rs.Steps[i].Error = ""
|
|
rs.UpdatedAt = now
|
|
}
|
|
|
|
// Fail marks a step failed, so a later Begin for the same (phase, name)
|
|
// knows to retry it rather than treat it as done.
|
|
func (rs *RunState) Fail(phase Phase, name string, stepErr error) {
|
|
i := rs.stepIndex(phase, name)
|
|
if i < 0 {
|
|
panic(fmt.Sprintf("checkpoint: Fail(%s/%s) called without Begin", phase, name))
|
|
}
|
|
now := time.Now().UTC()
|
|
rs.Steps[i].Status = StepFailed
|
|
rs.Steps[i].CompletedAt = &now
|
|
rs.Steps[i].Error = stepErr.Error()
|
|
rs.UpdatedAt = now
|
|
}
|