Documentation
¶
Index ¶
- Variables
- func AbortCause(ctx context.Context, err error) error
- func FatalAbort(err error) error
- func WatchTask(ctx context.Context, task Task, logger *slog.Logger) (wait func())
- type Aborter
- 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) DoContext(ctx context.Context, state State, fn func() error) 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 )
var ErrFatalAbort = errors.New("fatal abort")
ErrFatalAbort marks the cause a runner cancels its own context with when the change feed (or another out-of-band check) reports a fatal condition. Wrap a cause with FatalAbort before cancelling; AbortCause substitutes only causes that carry it.
Functions ¶
func AbortCause ¶ added in v0.18.0
AbortCause returns the cause ctx was cancelled with in place of err, when err is the context.Canceled that cancellation produced and the cause carries ErrFatalAbort. Runners cancel their context with such a cause (see FatalAbort) when the change feed reports a fatal condition; without this substitution every phase would return context.Canceled, and the abort would be reported, and recorded as a phase outcome, as if an operator had cancelled the run.
err is returned unchanged when it is nil or not a cancellation, when ctx is not cancelled, when the cause does not carry ErrFatalAbort (an operator cancellation: the runner's Cancel, or the caller cancelling its own context, with or without a cause of its own), and when err carries ErrDurableMutation or ErrOwnershipAmbiguous, evidence the caller must still be able to inspect.
func FatalAbort ¶ added in v0.18.0
FatalAbort marks err with ErrFatalAbort. The result has err's message unchanged, and errors.Is and errors.As still match err and everything it wraps.
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 Aborter ¶ added in v0.18.0
type Aborter interface {
Abort(cause error)
}
Aborter is implemented by a Task that can stop itself with a cause. When a checkpoint cannot be written, the checkpoint dumper aborts such a task with the write error marked by FatalAbort, so the task can return that error (see AbortCause) instead of the context.Canceled a plain Cancel produces. A Task that does not implement it is stopped with Cancel.
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 CopyFromTables ¶ added in v0.18.0
func CopyFromTables(tables []TableProgress) CopyProgress
CopyFromTables sums a per-table snapshot into the runner-wide copy progress. Deriving it from the same snapshot is what lets Progress.Copy and Progress.Tables reconcile: both count settled rows against the tables' cardinality estimates, whichever chunker is doing the copying.
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.
func EstimateETA ¶ added in v0.18.0
func EstimateETA(copiedRows, totalRows uint64, pct float64, rowsPerSecond uint64, startTime time.Time) ETA
EstimateETA returns the estimated remaining copy time and the state of that estimate. The duration is meaningful only when the state is ETAReady. No estimate is available before a copy rate has been measured (the first etaInitialWaitTime, or while no rows have been timed; ETAMeasuring) or once the copy is essentially complete (pct > 99.99; ETADue) — the callers present each case (GetETA renders "TBD"/"DUE", GetETAState returns the state and 0 seconds).
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
// Copy is the runner-wide row copy: the sum of Tables, so the two reconcile
// by construction. Both count settled rows against the tables' cardinality
// estimates, never the chunker's keyspace position, which is what the
// copier itself paces on for an auto_increment key and what the ETA is
// derived from. Copy is zero until the copy chunker exists and keeps its
// final reading through the later phases, so a caller can read how much
// the run copied at any point. RowsTotal is an estimate, so RowsCopied can
// exceed it.
//
// Two consequences of that split follow. RowsCopied counts rows the copy
// settled, so a row the binlog applier wrote before the copy reached it
// is not counted (the copy inserts with INSERT IGNORE, which reports it
// as unaffected), and on a busy table the copy finishes short of
// RowsTotal. A resume restores the count from the checkpoint and
// continues it; a move deletes and re-copies the rows at or above the
// resume position, so the rows among them settled before the checkpoint
// are counted again. And the ETA, including its DUE state, is paced on the
// keyspace for an auto_increment key, so over a sparse key range the copy
// can read close to complete here while the ETA is still counting down,
// or the reverse. Summary carries both halves.
Copy CopyProgress
// 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 PostChecksum because that is when it // runs. The checkpoint gates persist checksum resume evidence only in // [Checksum, WaitingOnSentinelTable): the sentinel wait discards it, so a // restart during the wait re-verifies the whole range. 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 >= CutOver` stays true: fatalError is a no-op and // the status/checkpoint loops stop) 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, carried forward across a 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) DoContext ¶ added in v0.18.0
DoContext is Do for a phase that runs under ctx: when ctx was cancelled with a fatal-abort cause, the context.Canceled error fn returns is replaced by that cause (see AbortCause), so the phase is recorded as failed rather than cancelled and the caller receives the cause.
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 )