Help us shape the future of Go! Take the 2026 Go Developer Survey

runtime

package
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

README

Runtime

The runtime package exists to reduce duplication between the three runners: migration, move and datasync. Each runner has a runner.go that drives the same lifecycle. They began as copy-paste forks, so they kept separate copies of the same plumbing. Those copies drift apart. A fix made in one runner is easy to miss in the other two, and nothing fails to compile when it is missed. (See "Keeping the runner triplet in sync" in AGENTS.md.)

Code moves here when it meets both conditions:

  • It is shared. At least two of the runners have a copy, and the copies differ only in which of the runner's own fields they read.
  • It is runner plumbing. It is not a subsystem. The copier, applier, checksum, change feed, checkpoint table and sentinel each have their own package. runtime only wires them into a run, so it imports them and they never import it.

A copy whose difference between runners is real is not forced into one shape. The difference is passed in instead: a callback, a field of a config struct, or an optional value. runtime never imports a runner package.

The name matches the standard library's runtime package. A file that needs both must import one of them under an alias.

What lives here

Type or function What it replaced Used by
Snapshot, Source Two copies of the periodic Status() block, Progress() and its throttle status migration, move
FatalGate Two copies of the fatal change-feed handler (change.ClientConfig.CancelFunc) migration, move
Lifecycle Three copies of the cancel function a Run invocation publishes, the opening lines of Run, and the evidence behind Result() migration, move, datasync
SharedThrottler Two copies of the mutex-guarded throttler that setup resolves migration, move
RecordCopyCompleted Three identical copies of the copy aggregate reported when the copy ends migration, move, datasync
Snapshot and Source

Migration and move walk the same states with the same subsystems. Their status block and Progress report are therefore built once, from a Snapshot. The runner builds a new Snapshot on every call. The Snapshot reads the state once, so every field of one report describes the same state.

Source is a struct of functions that read the runner's subsystems: copier, applier, checker, feeds and throttler. They are functions because setup assigns the subsystems while an API caller may already be polling. A Snapshot calls only the functions that the current state reports on. By the time the run is in that state, setup has assigned those subsystems. Each runner fills in Source inline in its snapshot method, so no runner declares an adapter type. SentinelSchema is optional. A move leaves it nil, because its sources can span several schemas.

FatalGate

This is the handler a runner wires to every change feed it starts. On the first fatal condition, Trip does the following, once:

  1. Moves the run to status.ErrCleanup.
  2. Logs the advice for the operator.
  3. Drops the checkpoint, unless the reason leaves the checkpoint resumable (change.FatalReason.PreservesCheckpoint).
  4. Cancels the run with a status.FatalAbort cause.

Once the run has reached cutover, Trip does nothing. The runner supplies its noun, its checkpoint drop and its cancel function in a FatalTarget.

Datasync does not use FatalGate. It has no cutover, it always keeps its checkpoint, and it records the cause itself (recordFatal).

Lifecycle

Lifecycle holds what a Run invocation shares with the methods other goroutines call while it runs:

  • the function that cancels the run, used by Cancel, Abort, Close and the fatal handler;
  • the correctness evidence the run leaves behind (status.WorkflowResult).

Run starts with:

ctx, end := r.lifecycle.Begin(ctx)
defer end(&retErr)

end does three things, in this order:

  1. It replaces a context.Canceled result with the fatal-abort cause (status.AbortCause).
  2. It records the evidence that the error carries.
  3. It cancels the context.

The evidence is recorded after the substitution, so it is the evidence of the error Run actually returns: a fatal-abort cause can carry status.ErrDurableMutation or status.ErrOwnershipAmbiguous, and context.Canceled never does. Cancelling last does not change the outcome. A context keeps the first cause it was cancelled with, so cancel(nil) cannot hide a fatal cause that is already set.

Lifecycle is a named field and is not embedded. Each runner forwards its own Cancel, Abort and Result to it. As a result, the runner's public API does not gain MarkDurableMutation, SetTerminalOwnership or RecordError. Datasync uses only the cancel side; it reports no Result. Tests that drive phases without calling Run use SetCancel.

SharedThrottler

This holds the throttler that setup resolves partway through. Progress and the change feed's UnderLoad callback may already be reading it at that point. Datasync reads its own load signal under progMu, so it does not use this.

RecordCopyCompleted

This reports the copy aggregate (rows and chunks) when the copy ends. A resumed run's chunker restores its row count from the checkpoint but starts its chunk count at zero. RecordCopyCompleted subtracts the restored rows so that the two counts cover the same invocation.

What does not live here (yet)

These parts of the runners are still duplicated or still differ. The last two are tracked in AGENTS.md ("Not yet unified").

  • DumpCheckpoint in migration and move. The two share a skeleton, including the rule for which states may persist checksum evidence. They differ only in the position and in the record fields.
  • invalidateChecksumWatermark. It runs raw SQL that belongs in pkg/checkpoint.
  • Close teardown. Each runner owns different resources. The order of the steps is shared, but the code is not.
  • Aurora autoscaling details that pkg/concurrency does not cover.

When you find another copy, extract it here, or into the subsystem package it belongs to. Then add a row to the table above and to the shared-table section of AGENTS.md.

Documentation

Overview

Package runtime holds the runner pieces that pkg/migration, pkg/move and pkg/datasync would otherwise each keep a copy of, and let drift apart.

The finite runners (migration and move) walk the same states with the same subsystems, so their periodic status block and Progress report are built here from a Snapshot rather than from two copies of the same switch. FatalGate is the state transition both make when their change feed fails, and SharedThrottler the throttler both resolve while already being polled. All three runners use Lifecycle, the cancel function and correctness evidence of a Run invocation, and RecordCopyCompleted.

The name matches the standard library's runtime package. A file that needs both must import one under an alias.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func RecordCopyCompleted

func RecordCopyCompleted(tracker *status.Tracker, c copier.Copier, rowsAtResume uint64)

RecordCopyCompleted reports to tracker the copy aggregate settled during this Run invocation. The chunker restores its settled row count from the checkpoint while its chunk count starts afresh, so rowsAtResume, the row count it restored, is subtracted to keep the two figures on the same invocation. It does nothing until c has a chunker.

Used by all three runners, including datasync.

Types

type FatalGate

type FatalGate struct {
	// contains filtered or unexported fields
}

FatalGate is the change feed's fatal-condition handler (change.ClientConfig.CancelFunc) shared by the finite runners. The zero value is ready to use. A runner owns one gate for its lifetime and wires every change feed it starts to it, so a burst of fatal events from several feed goroutines is realistic: the gate acts on the first and ignores the rest.

Datasync does not use it. It has no cutover to guard and always keeps its checkpoint (see change.FatalReason.PreservesCheckpoint).

func (*FatalGate) Trip

func (g *FatalGate) Trip(reason change.FatalReason, t FatalTarget) bool

Trip aborts the run for reason. It returns false, and does nothing, once the run has reached cutover: the tables have been swapped, so there is nothing left to abort. Otherwise it returns true, and on the first call only:

  • sets the state to status.ErrCleanup,
  • logs reason's operator advice (change.FatalReason.Advice),
  • drops the checkpoint unless reason leaves it resumable (change.FatalReason.PreservesCheckpoint). A schema change, for example, would otherwise block the run from proceeding permanently; starting again is the better choice. A dead stream preserves it: that is exactly the failure checkpoint resume exists to recover from.
  • cancels the run with a status.FatalAbort cause.

Doing these once keeps the side effects from racing each other and the runner's Close.

type FatalTarget

type FatalTarget struct {
	// Noun names the run in log lines and the abort error ("migration", "move").
	Noun    string
	Tracker *status.Tracker
	Logger  *slog.Logger
	// DropCheckpoint invalidates the run's checkpoint. It must return nil
	// without doing anything when setup has not created the checkpoint table
	// yet: a feed can fail during early setup.
	DropCheckpoint func(context.Context) error
	// Cancel cancels the run with cause. Run returns the cause (see
	// status.AbortCause), so the abort is reported as the failure it is and
	// not as an operator cancellation.
	Cancel func(cause error)
}

FatalTarget is the run a FatalGate aborts.

type Lifecycle

type Lifecycle struct {
	// contains filtered or unexported fields
}

Lifecycle is the state a runner's Run invocation shares with the methods an API caller or a background goroutine calls while it runs: the function that cancels it, and the correctness evidence (status.WorkflowResult) it leaves behind. The zero value is ready to use. A runner owns one as a named field and forwards its own Cancel, Abort and Result to it, so the runner's public API does not grow the methods that record evidence.

func (*Lifecycle) Begin

func (l *Lifecycle) Begin(ctx context.Context) (context.Context, func(*error))

Begin starts a Run invocation. It derives a context that Cancel cancels, publishes its cancel function, and clears the evidence the previous invocation left. Run must defer the returned function with the address of its named error result:

ctx, end := r.lifecycle.Begin(ctx)
defer end(&retErr)

end replaces a context.Canceled result with a fatal-abort cause (see status.AbortCause), then records the evidence the result carries (see RecordError), then cancels the context. Recording after the substitution records the evidence of the error Run returns, which a fatal-abort cause can carry and context.Canceled cannot. Cancelling last changes nothing: a context keeps the first cause it was cancelled with.

func (*Lifecycle) Cancel

func (l *Lifecycle) Cancel(cause error)

Cancel cancels the running invocation with cause. A nil cause is an operator cancellation: Run returns context.Canceled. A cause marked with status.FatalAbort is returned by Run instead. It does nothing before Begin (early setup, or test paths that bypass Run).

func (*Lifecycle) MarkDurableMutation

func (l *Lifecycle) MarkDurableMutation()

MarkDurableMutation records that this invocation changed something that a retry cannot undo.

func (*Lifecycle) RecordError

func (l *Lifecycle) RecordError(err error)

RecordError records the evidence err carries: status.ErrDurableMutation and status.ErrOwnershipAmbiguous.

func (*Lifecycle) Result

func (l *Lifecycle) Result() status.WorkflowResult

Result returns the evidence retained from the most recent invocation.

func (*Lifecycle) SetCancel

func (l *Lifecycle) SetCancel(cancel context.CancelCauseFunc)

SetCancel replaces the cancel function. Begin calls it; tests that drive a runner's phases without Run call it to observe or supply cancellation.

func (*Lifecycle) SetTerminalOwnership

func (l *Lifecycle) SetTerminalOwnership(o status.WorkflowTerminalOwnership)

SetTerminalOwnership records which side owns the table or traffic when the invocation ends.

type SharedThrottler

type SharedThrottler struct {
	// contains filtered or unexported fields
}

SharedThrottler holds the throttler a runner resolves during setup. Setup assigns it partway through, while an API caller may already be polling Progress, which reports whether the run is paused, and a change feed's UnderLoad callback may already be reading it. The zero value holds nil.

func (*SharedThrottler) Get

Get returns the resolved throttler, or nil if setup has not resolved one.

func (*SharedThrottler) Set

Set publishes the resolved throttler.

type Snapshot

type Snapshot struct {
	// Noun names the workflow in the status header ("migration", "move").
	Noun    string
	State   status.State
	Tracker *status.Tracker
	// Tables is the copy chunker's per-table progress. Progress and Status
	// both derive their copy figures from it, so the API and the log block
	// report one measure: settled rows against the tables' cardinality
	// estimates, kept past the end of the copy.
	Tables     []status.TableProgress
	Checkpoint *status.LastCheckpoint
	Resumed    bool
	Source     Source
}

Snapshot is what a runner reports on. The runner builds one per call, and the state is read once when it does, so every field of a report describes the same state rather than whichever state each field happened to observe.

func (*Snapshot) Progress

func (s *Snapshot) Progress() status.Progress

Progress returns the structured report for API callers.

func (*Snapshot) Status

func (s *Snapshot) Status() string

Status returns the periodic report on the whole run: a header line plus one indented row per subsystem (see status.Block). It absorbs what used to be separate periodic lines from the change feeds (flushes, rotations) and the checkpoint dumper, which each ran on their own interval — see github.com/block/spirit/issues/329.

func (*Snapshot) ThrottleStatus

func (s *Snapshot) ThrottleStatus() status.ThrottleStatus

ThrottleStatus reports only signals honored by the work currently running. Checksum passes honor load signals; interval waits and cutover are unpaced. The sentinel wait only reports pacing while a continuous checksum runs.

type Source

type Source struct {
	Copier    func() copier.Copier
	Applier   func() applier.Applier
	Checker   func() checksum.Checker
	Feeds     func() []change.Source
	Throttler func() throttler.Throttler
	// SentinelSchema, when set and not empty, adds the sentinel table's name
	// to the sentinel row. A move's sources can span several schemas, so it
	// leaves it nil.
	SentinelSchema func() string
}

Source gives a Snapshot the runner's subsystems. Setup assigns them while an API caller may already be polling, so they are read through functions, and a Snapshot only calls the ones the current state reports on, which are the ones setup has finished assigning. Every function but SentinelSchema must be set.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL