Files
stalwart-migrator/internal/checkpoint/step.go
T
jcoffey-dev 719a945d64 Initial commit: stalwart-migrator design and scaffolding
In-place upgrade tool for Stalwart Mail Server (0.15.5 -> latest) with
checkpointed rollback and post-migration validation. Design stage; see
ARCHITECTURE.md.
2026-08-22 18:17:17 -07:00

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
}