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.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user