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

sentinel

package
v0.19.0 Latest Latest
Warning

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

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

Documentation

Overview

Package sentinel implements the "sentinel table" cutover gate shared by the migration and move runners.

When a migration/move is run with deferred cutover, spirit creates a small marker table (the sentinel) and then blocks the final cutover until an operator drops it. While blocked, a continuous checksum re-verifies the copied data so a long human-paced wait does not let the shadow copy drift.

Both runners previously carried near-identical copies of this logic; the only differences were which connection/schema the sentinel lives on and the two runner-specific callbacks (RunChecksum, InvalidateWatermark), which are injected into Wait rather than reimplemented here.

Index

Constants

View Source
const TableName = "_spirit_sentinel"

TableName is the fixed name of the sentinel table. It is intentionally a constant (not derived from the migrated table) so an operator always knows which table to drop to release a deferred cutover.

Variables

View Source
var (
	WaitLimit     = 48 * time.Hour
	CheckInterval = 1 * time.Second
)

WaitLimit bounds how long Wait blocks for the sentinel to be dropped before giving up; CheckInterval is the existence-probe period. They are package vars (not consts) only so tests can shorten them; production never overrides them. Keeping the timing here makes the cutover wait identical across every caller (migrate, move).

Functions

func Create

func Create(ctx context.Context, db *sql.DB) error

Create creates the sentinel table in db's currently-selected schema if it does not already exist. The schema is taken from the connection (the table name is unqualified) rather than passed in, so this works under Vitess where a fixed schema name isn't meaningful — point db at the right keyspace/schema. Creation must be idempotent: the name is a constant, so a fresh run can find it left over from an earlier run or shared with a concurrent one, and TestSentinelCreateNeverObservedAbsent relies on CREATE IF NOT EXISTS so a concurrent existence probe never sees it missing. Only the fresh path calls Create (migration's newMigration, move's newCopy), so a sentinel dropped before a successful resume stays dropped. A run whose resume is impossible (e.g. a too-old checkpoint, or move's --force) falls back to the fresh path and, with --defer-cutover, creates it again.

func Exists

func Exists(ctx context.Context, db *sql.DB) (bool, error)

Exists reports whether the sentinel table is present in db's currently-selected schema (DATABASE()), so it tracks Create's unqualified target rather than a passed-in schema name.

func Wait

func Wait(ctx context.Context, cfg WaitConfig) (retErr error)

Wait blocks until the sentinel table is dropped (proceed with cutover), WaitLimit elapses (error), or the continuous checksum exits on its own (error). It returns nil only when it is safe to proceed with cutover. If the sentinel is already absent on entry, it returns nil immediately — no checksum goroutine is spawned and InvalidateWatermark is not called.

Otherwise it runs RunChecksum in the background for the lifetime of the wait, and on every subsequent return path (sentinel dropped, timeout, checksum failure, or parent-context cancellation) it stops that goroutine and then calls InvalidateWatermark to reconcile persisted resume evidence according to the caller's policy. Checker ownership and checkpoint reuse are caller-specific.

Exists, RunChecksum and InvalidateWatermark are required (Wait returns an error if any is nil); a nil Logger defaults to slog.Default().

func WarnIfIgnored added in v0.18.0

func WarnIfIgnored(ctx context.Context, exists func(context.Context) (bool, error), logger *slog.Logger)

WarnIfIgnored is called instead of Wait by a run that did not defer its cutover, and so cuts over without waiting on the sentinel. If the sentinel table exists it logs a warning, so an operator who created the table expecting it to hold this run's cutover can see why it did not. It only reports: a failed probe is logged rather than returned, because whether the table exists does not change what the run does next.

Types

type WaitConfig

type WaitConfig struct {
	// Exists probes whether the sentinel table still exists. It is called once
	// up front (an absent sentinel means "proceed immediately") and then once
	// per CheckInterval.
	Exists func(ctx context.Context) (bool, error)

	// RunChecksum runs the continuous checksum for the duration of the wait.
	// It is spawned in its own goroutine with a child context that Wait
	// cancels on return, and is expected to filter a benign cancellation of
	// that context to a nil error; any non-nil return is treated as a real
	// verification failure and aborts the wait.
	RunChecksum func(ctx context.Context) error

	// InvalidateWatermark is invoked exactly once, after the continuous
	// checksum goroutine has fully stopped, to apply the caller's resume policy.
	// Migration and move both clear checksum evidence unconditionally. It runs
	// even when the parent context was cancelled. Callers needing crash safety
	// before background work must also invalidate evidence before starting
	// that work in RunChecksum.
	InvalidateWatermark func(ctx context.Context) error

	Logger *slog.Logger
}

WaitConfig holds the dependencies of Wait. Exists, RunChecksum and InvalidateWatermark are required; Logger is optional (a nil Logger defaults to slog.Default()). The poll/timeout timing comes from the package-level WaitLimit / CheckInterval so it is identical across callers.

Jump to

Keyboard shortcuts

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