Documentation
¶
Index ¶
- Variables
- func WatchTask(ctx context.Context, task Task, logger *slog.Logger) (wait func())
- type Block
- type ChecksumProgress
- type CopyProgress
- type ETA
- type ETAState
- type LastCheckpoint
- type Progress
- type State
- type TableProgress
- type Task
- type ThrottleStatus
- type Tracker
- func (t *Tracker) Begin()
- func (t *Tracker) Do(state State, fn func() error) (err error)
- func (t *Tracker) Duration(state State) time.Duration
- func (t *Tracker) Elapsed() time.Duration
- func (t *Tracker) Get() State
- func (t *Tracker) RecordCopyCompleted(rows, chunks uint64)
- func (t *Tracker) Set(state State)
- func (t *Tracker) SetMetricsSink(sink metrics.Sink, logger *slog.Logger)
- func (t *Tracker) StartTime() time.Time
- func (t *Tracker) TotalElapsed() time.Duration
- type WorkflowMetricsSink
- type WorkflowPhaseOutcome
- type WorkflowResult
- type WorkflowTerminalOwnership
Constants ¶
This section is empty.
Variables ¶
var ( ErrMismatchedAlter = errors.New("alter statement in checkpoint table does not match the alter statement specified here") ErrBinlogNotFound = errors.New("checkpoint binlog file not found on server") ErrCheckpointTooOld = errors.New("checkpoint is too old to safely resume") ErrCheckpointCollision = errors.New("checkpoint belongs to a different table (truncation collision)") ErrCouldNotWriteCheckpoint = errors.New("could not write checkpoint") ErrWatermarkNotReady = errors.New("watermark not ready") // ErrOwnershipAmbiguous marks a failure after which spirit cannot tell // which side owns the table(s): a DDL or RENAME that the server may have // committed before the client lost its acknowledgement, or a caller-owned // traffic switch whose outcome is unknown. Spirit never retries past one // of these, because a retry that guesses wrong can move ownership a // second time. Callers should test for it with errors.Is and escalate to // a human rather than re-running. ErrOwnershipAmbiguous = errors.New("ownership ambiguous; verify table ownership manually before retrying") // ErrDurableMutation marks an error returned after the current invocation // authoritatively completed a durable write. It is orthogonal to // ErrOwnershipAmbiguous: callers may know a write happened without knowing // which side owns traffic, or may know ownership despite later cleanup // failing. ErrDurableMutation = errors.New("durable mutation completed before failure") )
var ( CheckpointDumpInterval = 50 * time.Second StatusInterval = 30 * time.Second )
Functions ¶
func WatchTask ¶
WatchTask periodically does the status reporting for a task. This includes writing to the logger the current state, and dumping checkpoints.
It returns a wait function the caller can invoke during shutdown to block until the spawned goroutines have exited. This avoids races where a still-running checkpoint goroutine writes a fresh row after the caller has closed/torn down the surrounding state — a pattern that has produced flakes in tests that mutate the checkpoint table after Run() returns (see #773).
Types ¶
type Block ¶ added in v0.17.0
type Block struct {
// contains filtered or unexported fields
}
Block is the multi-line report a runner returns from Status(): a header line followed by one indented row per subsystem (copier, applier, change feed, checkpoint).
The single-line form it replaces had grown to twenty-odd space-separated fields, which is dense but not readable: a value that changes width shifts every field after it, so the eye cannot follow one number down a scrollback. Grouping by subsystem and padding the labels to a common width is what makes a spike in one field visible across ticks. See github.com/block/spirit/issues/329.
Note that the whole block is one log record with newlines in it. Under the default slog handler (what the CLI uses) that prints as written. A handler that quotes the message — slog's TextHandler, or a JSON handler — will render the newlines escaped instead.
The zero value is not useful; construct with NewBlock.
func (*Block) Row ¶ added in v0.17.0
Row appends a labelled row of fields. A row whose text is empty is dropped, so a caller can pass a helper that reports nothing — a nil applier, a change feed that does not publish stats — without having to test for it first. Trailing spaces are trimmed for the same reason: they are what is left when such a helper contributes nothing to the end of a row.
type ChecksumProgress ¶ added in v0.16.0
type ChecksumProgress struct {
RowsChecked uint64 // rows verified so far
RowsTotal uint64 // total rows to verify
}
ChecksumProgress tracks progress of the checksum phase, where Spirit verifies the copied data against the source before cutover. RowsChecked and RowsTotal are 0 outside the checksum phase.
func (ChecksumProgress) Fraction ¶ added in v0.17.0
func (c ChecksumProgress) Fraction() float64
Fraction returns progress in 0..1, for callers that need the ratio rather than the rendered percentage. 0 before the row estimate is known.
func (ChecksumProgress) String ¶ added in v0.16.0
func (c ChecksumProgress) String() string
String renders the checksum progress for the human-readable summary line, e.g. "71436/221193 32.30%".
type CopyProgress ¶ added in v0.17.0
type CopyProgress struct {
RowsCopied uint64 // rows copied so far
RowsTotal uint64 // estimated total rows to copy
}
CopyProgress tracks progress of the row copy. It is the numeric form of what the copier used to report only as a preformatted string, so the status block can lay the percentage and the counts out as separate fields.
func (CopyProgress) Fraction ¶ added in v0.17.0
func (c CopyProgress) Fraction() float64
Fraction returns progress in 0..1, for callers that need the ratio rather than the rendered percentage. 0 before the row estimate is known.
func (CopyProgress) String ¶ added in v0.17.0
func (c CopyProgress) String() string
String renders the copy progress, e.g. "1031251/16370180 6.30%".
type ETA ¶ added in v0.16.0
ETA is the structured form of the ETA embedded in Summary. State reports whether Duration is available yet — e.g. ETAMeasuring during the initial window before a copy rate is known — so callers can show "calculating" rather than a misleading 0. Duration is the estimated remaining row-copy time, valid only when State is ETAReady and 0 otherwise.
type ETAState ¶ added in v0.16.0
type ETAState string
Progress is returned as a struct because we may add more to it later. It is designed for wrappers (like a GUI) to be able to summarize the current status without parsing log output. ETAState describes the availability of the row-copy ETA estimate, so callers can distinguish "still measuring" from a real estimate without parsing the Summary string. It mirrors the cases GetETA renders as text.
const ( // ETANone means there is no copy ETA because the migration is not in the // row-copy phase. Duration is 0. ETANone ETAState = "" // ETAMeasuring means a copy is in progress but no copy rate has been measured // yet, so no estimate is available (Summary shows "ETA TBD"). Duration is 0. ETAMeasuring ETAState = "measuring" // ETAReady means Duration holds a current remaining-time estimate. ETAReady ETAState = "ready" // ETADue means the copy is essentially complete (Summary shows "ETA DUE"). // Duration is 0. ETADue ETAState = "due" )
type LastCheckpoint ¶ added in v0.17.0
type LastCheckpoint struct {
// contains filtered or unexported fields
}
LastCheckpoint records what the last successful checkpoint write saved, so a runner can report it as the ckpt row of its periodic status block ("ckpt 12s ago binlog.000123:4567" — see Row) instead of the checkpoint dumper logging a line of its own every time it runs. See github.com/block/spirit/issues/329.
The age and the position belong together because together they answer the only question worth asking about a checkpoint mid-run: if this process died now, where would the next one resume, and is that point still available on the source? A position the source has since purged is not resumable, and the age is what tells you how fast the run is drifting toward that.
The zero value is ready to use and reports "never" / "none". Safe for concurrent use: the recorder and the status goroutine are always different goroutines. The timestamp and the position are stored separately, so a reader can in principle pair an age from one write with the position from the previous one — harmless at status-line cadence, where both are approximate by construction.
Contains sync/atomic values, so it must not be copied after first use.
func (*LastCheckpoint) Age ¶ added in v0.17.0
func (c *LastCheckpoint) Age() string
Age renders how long ago the last checkpoint was written, rounded to the second, or "never" if none has been.
func (*LastCheckpoint) At ¶ added in v0.17.0
func (c *LastCheckpoint) At() time.Time
At returns when the last checkpoint was written, or the zero time if none has been.
func (*LastCheckpoint) Position ¶ added in v0.17.0
func (c *LastCheckpoint) Position() string
Position returns the resume coordinate the last checkpoint saved, or "none" if no checkpoint has been written yet (or the source reported no position). Never returns the empty string: the status block needs something to render.
func (*LastCheckpoint) Record ¶ added in v0.17.0
func (c *LastCheckpoint) Record(position string)
Record marks a checkpoint as having just been persisted at position, which is the opaque resume coordinate the change feed reported: a binlog file:offset, a GTID set, or whatever an alternative source encodes.
func (*LastCheckpoint) Row ¶ added in v0.17.0
func (c *LastCheckpoint) Row() string
Row renders the checkpoint row of a status block: how long ago the last checkpoint was written and the position it saved, or "never" before the first one.
type Progress ¶
type Progress struct {
CurrentState State // current state, i.e. CopyRows
Summary string // text based representation, i.e. "12.5% copyRows ETA 1h 30m"
// Resume is true when this run resumed from a checkpoint left by an earlier
// run, rather than starting the copy from scratch.
//
// It exists because a resumed run walks the whole state machine again
// (CopyRows, Checksum, ...) even when those phases are near-instant, so a
// wrapper watching CurrentState sees what looks like a migration starting
// over. CurrentState is deliberately left alone — callers parse it for phase
// display — and this reports the fact alongside it: pair it with
// Tables/Checksum progress to decide whether to render the run as
// "recovering" rather than "starting".
Resume bool
// Throttle reports whether the current phase is paused by a throttler, and
// why. Which signals count depends on the phase: the copy honours all of
// them, while a checksum only honours load signals (a read-only snapshot
// pass cannot cause replica lag, so pausing it on lag would only hold the
// snapshot open for longer).
Throttle ThrottleStatus
// ETA is the structured remaining row-copy estimate and its availability.
ETA ETA
// Checksum is the structured progress of the post-copy checksum phase,
// populated while CurrentState is Checksum and zero otherwise. It is the
// structured form of the checksum progress embedded in Summary.
Checksum ChecksumProgress
// Tables contains per-table progress for multi-table migrations.
// For single-table migrations, this will have one entry.
Tables []TableProgress
}
type State ¶
type State int32
const ( Initial State = iota CopyRows ApplyChangeset // first mass apply RestoreSecondaryIndexes AnalyzeTable Checksum PostChecksum // second mass apply // WaitingOnSentinelTable comes after the initial checksum so that // `state >= Checksum` is true while the sentinel-wait blocks the cutover. // During this state Spirit also runs the "continuous checksum" loop // described in docs/migrate.md. WaitingOnSentinelTable CutOver // ReverseWindow is the post-cutover reverse window, entered only when // --reverse-window > 0: traffic is on the target and spirit keeps the source // current in change-only mode while watching for a revert request. It sorts // after CutOver (so `state >= Checksum` stays true) and lets orchestration // surface that a revert is still possible. ReverseWindow Close ErrCleanup )
type TableProgress ¶ added in v0.11.0
type TableProgress struct {
TableName string // name of the table being migrated
RowsCopied uint64 // actual rows settled; may exclude earlier work on resume
RowsTotal uint64 // estimated table cardinality, not an upper bound
IsComplete bool // true if this table's copy is complete
}
TableProgress tracks progress for a single table in the migration.
func TablesFromChunker ¶ added in v0.17.0
func TablesFromChunker(chunker table.Chunker) []TableProgress
TablesFromChunker returns a stable, structured snapshot for runner progress. Multi-source chunkers preserve their source-qualified table identifiers.
type ThrottleStatus ¶ added in v0.17.0
type ThrottleStatus struct {
// Throttled is true while a throttler is telling the current phase to
// pause. This is the field to branch on.
//
// It is false in phases that do not pace themselves against a throttler at
// all — which is every phase except the row copy and the checksum. A loaded
// server is not reported as pausing a cutover or a sentinel wait, because
// nothing there is reading that signal.
Throttled bool
// Reason names the signal and the comparison that tripped it, in the form
// "<signal> <observed> <op> <threshold>" — e.g. "commit-latency 128ms >= 100ms"
// or "redo-aware 24 > 17". When several signals throttle at once they are
// joined with "; ", because clearing only one of them will not resume the
// copy.
//
// It is intended for display, not for branching: it is "" when Throttled is
// false, and may also be "" when the configured throttler cannot explain
// itself (see throttler.ReasonedThrottler).
//
// It is also sampled independently of Throttled rather than atomically with
// it, so on a signal that is changing underneath the poll the two can
// disagree: a throttler that clears in between yields Throttled with an
// empty Reason, and a reason can quote a comparison that has just stopped
// holding. Both are display-level staleness on a value that is a snapshot
// anyway — not a bug to report.
Reason string
// Utilization is load relative to the point at which throttling begins:
// 1.0 is exactly at that point, >1.0 is over it, and lower values are
// further below it. It lets a wrapper show "running at 40% of the load
// limit" rather than only a long ETA.
//
// 0 is ambiguous and must not be rendered as idle. It is also what this
// field reports when no continuous load signal exists at all — notably when
// throttling is replica-lag-only, which is a budget rather than a load gauge
// (see throttler.GradualThrottler). A copy paused on replica lag therefore
// reports Throttled with Utilization 0, so a wrapper drawing a load gauge
// should treat 0 as "unknown" and hide it rather than show an idle server.
Utilization float64
}
ThrottleStatus reports whether the current phase is paused by a throttler, and why. Before this, throttling was only visible in the logs, so a wrapper polling status saw a migration that had gone quiet with no way to say why (issue #844).
type Tracker ¶ added in v0.17.0
type Tracker struct {
// contains filtered or unexported fields
}
func (*Tracker) Begin ¶ added in v0.17.0
func (t *Tracker) Begin()
Begin marks the start of a run: it resets all timing (start time, per-state durations) and enters Initial, so setup work before the first phase is attributed to Initial and TotalElapsed measures from here. Runners call it at the top of Run, where they previously recorded a startTime field. Calling Begin again starts a fresh run rather than extending the previous one.
func (*Tracker) Do ¶ added in v0.17.0
Do runs fn as the given state: it transitions to state, runs fn, and attributes fn's wall-clock time (panic and runtime.Goexit inclusive) to state. The state remains current after Do returns — as with Set, the next state begins only when it is entered.
The typed sink outcome describes this one bounded attempt, not the eventual workflow result. A non-cancellation error or abnormal unwind is failed; cancellation/deadline errors are cancelled; a nil error is succeeded.
func (*Tracker) Duration ¶ added in v0.17.0
Duration returns the total time attributed to state so far, including the still-running interval when state is current. States visited more than once accumulate. Note that once a bracket completes its interval is closed: Duration(state) freezes while Elapsed keeps growing until the next transition — they answer different questions.
func (*Tracker) Elapsed ¶ added in v0.17.0
Elapsed returns how long the current state has been current. It reports 0 before the first transition. This is the value to render on the status block's header ("copier-time", "sentinel-wait-time", ...).
func (*Tracker) RecordCopyCompleted ¶ added in v0.17.0
RecordCopyCompleted reports the copy aggregate settled during this Runner.Run invocation. It is emitted even when the copy phase later returns an error; rows and chunks count completed work, not phase success.
func (*Tracker) Set ¶ added in v0.17.0
Set transitions to state without a bracket: it closes the previous state's still-open interval (if a completed Do already closed it, the gap since is left unattributed) and state begins accruing now. Prefer Do wherever the phase has a clear extent.
func (*Tracker) SetMetricsSink ¶ added in v0.17.0
SetMetricsSink installs the sink that phase transitions are reported to, and the logger used when a send fails. A nil or metrics.NoopSink disables reporting without adding work to state transitions.
Reporting is synchronous, so a slow sink slows transitions — bounded by metrics.SinkTimeout per send, and there are only a dozen transitions in a run. It is deliberately the same trade-off the copier already makes for its per-chunk metrics, which send far more often.
func (*Tracker) StartTime ¶ added in v0.17.0
StartTime returns when the run began: Begin, or the first transition if Begin was never called. It is the zero time before either, and stable for the life of a run — migration derives timestamped _old table names from it.
func (*Tracker) TotalElapsed ¶ added in v0.17.0
TotalElapsed returns how long the tracker has been running: the time since Begin (or, if Begin was never called, since the first transition). It reports 0 before either. This is the value to render as "total-time".
type WorkflowMetricsSink ¶ added in v0.17.0
type WorkflowMetricsSink interface {
metrics.Sink
RecordWorkflowPhaseStarted(State)
RecordWorkflowPhaseFinished(State, WorkflowPhaseOutcome)
RecordWorkflowCopyCompleted(rows, chunks uint64)
}
WorkflowMetricsSink is the optional typed workflow capability of the runner's existing metrics.Sink. Generic metrics consumers need only implement metrics.Sink; callers that need exact phase-attempt and aggregate semantics implement these methods on the same sink rather than installing a second observer.
type WorkflowPhaseOutcome ¶ added in v0.17.0
type WorkflowPhaseOutcome uint8
Tracker owns the current State plus per-state wall-clock timing. Runners embed it in place of a bare State field so that state transitions and phase timing cannot drift apart, and so per-phase durations no longer need ad-hoc fields on the runner (copyDuration, sentinelWaitStartTime, ...).
The zero value is ready for use.
Phases with a clear extent run under Do, which times exactly the function it brackets. Set remains the primitive for transitions whose "phase" has no meaningful end from the setter's perspective (Close, ErrCleanup); it closes out the previous state's running interval, matching the historical "one state ends when the next starts" semantics.
Tracker assumes spirit's linear execution model: a single goroutine advances through the phases in order, and the only concurrent transition is a fatal Set (ErrCleanup) racing an open bracket. In that case time accrues to the bracketed state up to the fatal transition and the bracket's own exit becomes a no-op. It is not designed for concurrent or overlapping phases. WorkflowPhaseOutcome is how one bounded Tracker.Do attempt returned. It is deliberately not a workflow/SLO result: a retry or a resumed process may later run the same phase again.
const ( WorkflowPhaseOutcomeInvalid WorkflowPhaseOutcome = iota WorkflowPhaseOutcomeSucceeded WorkflowPhaseOutcomeFailed WorkflowPhaseOutcomeCancelled )
type WorkflowResult ¶ added in v0.17.0
type WorkflowResult struct {
DurableMutation bool
TerminalOwnership WorkflowTerminalOwnership
}
WorkflowResult is the reusable evidence a runner retained when its Run invocation returned. It is separate from phase metrics: durable mutation and physical ownership are correctness facts, not time-series dimensions.
func (WorkflowResult) OwnershipAmbiguous ¶ added in v0.17.0
func (r WorkflowResult) OwnershipAmbiguous() bool
OwnershipAmbiguous reports that the runner could not prove which side owns the table or traffic.
func (WorkflowResult) ReverseFinalized ¶ added in v0.17.0
func (r WorkflowResult) ReverseFinalized() bool
ReverseFinalized reports that reverse cutover definitively restored the source as owner.
type WorkflowTerminalOwnership ¶ added in v0.17.0
type WorkflowTerminalOwnership uint8
WorkflowTerminalOwnership is authoritative terminal physical ownership evidence from a runner. Zero means the runner has no special terminal ownership fact to report.
const ( WorkflowTerminalOwnershipNone WorkflowTerminalOwnership = iota WorkflowTerminalOwnershipReverseFinalized WorkflowTerminalOwnershipAmbiguous )