storagereplacement

package
v0.1.0-beta.1 Latest Latest
Warning

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

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

Documentation

Overview

Package storagereplacement owns the operator-approved provider replacement record: its state machine, wait reasons, stable API codes, and task payload contract. It holds no database or provider dependencies.

Index

Constants

View Source
const (
	CodeActive              = "replacement_active"
	CodeSuperseded          = "replacement_superseded"
	CodeNotRetryable        = "replacement_not_retryable"
	CodeTaskRunning         = "replacement_task_running"
	CodeTargetInvalid       = "replacement_target_invalid"
	CodeTargetInUse         = "replacement_target_in_use"
	CodeNoEligibleProvider  = "replacement_no_eligible_provider"
	CodeTargetUnavailable   = "replacement_target_unavailable"
	CodeIdempotencyConflict = "replacement_idempotency_conflict"
	CodeSourceNotCurrent    = "replacement_source_not_current"

	// CodeTaskRetryUnsupported is returned by the generic exhausted-task retry
	// endpoint when the task belongs to a replacement.
	CodeTaskRetryUnsupported = "replacement_task_retry_unsupported"
)

Stable machine-readable codes returned alongside API errors. Clients branch on these, so treat them as part of the public contract.

View Source
const (
	// StageMigrate advances the replacement control plane through the uploader;
	// item transfers run on the dedicated replacement worker.
	StageMigrate = "replace_provider"
	// StageRetire runs the retirement safety gate through the storage cleanup
	// worker.
	StageRetire = "retire_data_set"
	// StageRetireAbandonedTarget ends the service of a target that a later
	// confirmation replaced before it ever took over the slot.
	StageRetireAbandonedTarget = "retire_abandoned_target"
)

Variables

View Source
var (
	// ErrActiveReplacement means the source data set already has a replacement
	// that has not reached a terminal state.
	ErrActiveReplacement = errors.New("data set already has an active replacement")

	// ErrSuperseded means a later confirmation took ownership of this work.
	ErrSuperseded = errors.New("replacement has been superseded")

	// ErrNotRetryable means the replacement is progressing or finished, so the
	// dedicated retry action does not apply.
	ErrNotRetryable = errors.New("replacement is not in a retryable state")

	// ErrTaskRunning means a coordinator task still holds this replacement.
	ErrTaskRunning = errors.New("replacement task is still running")

	// ErrInvalidTarget means the requested provider cannot serve as the target,
	// for example because it is the source itself.
	ErrInvalidTarget = errors.New("requested provider cannot replace this data set")

	// ErrTargetInUse means the requested provider already owns another current
	// data set in the same bucket.
	ErrTargetInUse = errors.New("requested provider already serves this bucket")

	// ErrNoEligibleProvider means automatic selection found no provider the
	// bucket has not already used.
	ErrNoEligibleProvider = errors.New("no eligible replacement provider is available")

	// ErrTargetUnavailable means the requested provider is not currently an
	// active PDP-capable replacement candidate.
	ErrTargetUnavailable = errors.New("requested provider is not available for replacement")

	// ErrIdempotencyConflict means a client request id was reused with different
	// replacement parameters.
	ErrIdempotencyConflict = errors.New("replacement idempotency key conflicts with an earlier request")

	// ErrSourceNotCurrent means the data set no longer owns its replica slot,
	// so replacing it would not change where writes go.
	ErrSourceNotCurrent = errors.New("data set is not the current replica")

	// ErrItemCancelled means this migration item no longer has executable work.
	ErrItemCancelled = errors.New("replacement item is no longer executable")

	// ErrItemDeferred means the item cannot run yet but must be revisited. The
	// coordinator moves on to other content rather than blocking on it.
	ErrItemDeferred = errors.New("replacement item is waiting for its source")

	// ErrPrematureComplete means a retirement safety gate still reports a
	// blocker. Repositories return it even when called outside the worker.
	ErrPrematureComplete = errors.New("replacement cannot complete while a safety gate blocks it")

	// ErrIllegalTransition means the requested state change is not in the
	// state machine.
	ErrIllegalTransition = errors.New("illegal replacement state transition")
)

Functions

func AbandonedTargetTaskKey

func AbandonedTargetTaskKey(replacementID int64) string

AbandonedTargetTaskKey identifies the cleanup of a target a later confirmation abandoned. It is a separate coordinator because it retires the opposite generation and answers a different safety question.

func Allowed

func Allowed(from, to Status) bool

Allowed reports whether the state machine permits this transition. A status never transitions to itself; callers update fields in place instead.

func Code

func Code(err error) string

Code maps a replacement error to its stable API code. It returns an empty string for errors that carry no client-facing code.

func IsCoordinatorTask

func IsCoordinatorTask(taskType model.TaskType, stage *string) bool

IsCoordinatorTask reports whether a task belongs to a replacement. Generic exhausted-task retry uses it to refuse work that must resume through the dedicated replacement action.

func MigrateTaskKey

func MigrateTaskKey(replacementID int64) string

MigrateTaskKey identifies the single migration coordinator for one replacement. Item concurrency is owned by the durable replacement queue, not by this control-plane task.

func NewAbandonedTargetTask

func NewAbandonedTargetTask(replacementID, bucketID int64, maxRetries int, scheduledAt time.Time) *model.Task

NewAbandonedTargetTask builds the coordinator that ends the paid service of a target no confirmation uses any more. Without it the abandoned service keeps costing money after a later confirmation takes over.

func NewMigratePayload

func NewMigratePayload(replacementID int64) map[string]any

NewMigratePayload builds the coordinator payload.

func NewMigrateTask

func NewMigrateTask(replacementID, bucketID int64, versionID string, maxRetries int, scheduledAt time.Time) *model.Task

NewMigrateTask builds the singleton migration coordinator for one replacement. It prepares the target, seeds durable items, observes their bounded execution state, and hands completed migration to retirement.

func NewRetirePayload

func NewRetirePayload(replacementID int64) map[string]any

NewRetirePayload builds the retirement coordinator payload.

func NewRetireTask

func NewRetireTask(replacementID, bucketID int64, maxRetries int, scheduledAt time.Time) *model.Task

NewRetireTask builds the singleton retirement coordinator for one replacement. It runs on the storage cleanup worker because terminating a service is a destructive lifecycle action, not upload work.

func ParseRetirePayload

func ParseRetirePayload(task *model.Task) (int64, error)

ParseRetirePayload decodes a retirement coordinator payload.

func RetireTaskKey

func RetireTaskKey(replacementID int64) string

RetireTaskKey identifies the single retirement coordinator for one replacement. It retires the generation being replaced.

Types

type ClaimToken

type ClaimToken struct {
	ItemID    int64
	ClaimedAt time.Time
}

ClaimToken fences lifecycle updates made by one item worker lease.

type ExecutionSnapshot

type ExecutionSnapshot struct {
	ReplacementID    int64 `bun:"replacement_id"`
	SeedingComplete  bool  `bun:"seeding_complete"`
	ItemsTotal       int   `bun:"items_total"`
	ItemsCopied      int   `bun:"items_copied"`
	HasPending       bool  `bun:"has_pending"`
	HasActive        bool  `bun:"has_active"`
	HasRetrying      bool  `bun:"has_retrying"`
	HasWaitingSource bool  `bun:"has_waiting_source"`
	HasFailed        bool  `bun:"has_failed"`
}

ExecutionSnapshot is the bounded coordinator view of one replacement. It intentionally reports presence rather than item counts; full aggregates are reserved for operator-facing progress reads.

type FailureReason

type FailureReason string

FailureReason identifies a permanent failure whose recovery action differs from retrying the same approved target.

const (
	FailureReasonTargetInUse FailureReason = "target_in_use"
)

func (FailureReason) Valid

func (r FailureReason) Valid() bool

Valid reports whether the value is a known permanent failure reason.

type Item

type Item struct {
	bun.BaseModel `bun:"table:storage_replacement_items,alias:storage_replacement_item"`

	ID            int64 `bun:",pk,autoincrement"`
	ReplacementID int64 `bun:",notnull"`
	UploadID      int64 `bun:",notnull"`
	// TargetCopyID is the concrete copy row on the target generation. Tasks
	// address it directly so they can never write the wrong generation.
	TargetCopyID *int64     `bun:",nullzero"`
	Status       ItemStatus `bun:",notnull"`
	Attempts     int        `bun:",notnull,default:0"`
	ScheduledAt  time.Time  `bun:",nullzero,notnull,default:current_timestamp"`
	RetryCount   int        `bun:",notnull,default:0"`
	// MaxRetries is nullable only for rows created before the durable item
	// queue migration. Startup recovery initializes it exactly once.
	MaxRetries *int       `bun:",nullzero"`
	ClaimedAt  *time.Time `bun:",nullzero"`
	LeaseUntil *time.Time `bun:",nullzero"`
	LastError  *string    `bun:",nullzero"`
	CreatedAt  time.Time  `bun:",nullzero,notnull,default:current_timestamp"`
	UpdatedAt  time.Time  `bun:",nullzero,notnull,default:current_timestamp"`
}

Item is one unit of migration work. Items are keyed by storage upload, not by object version, so content shared by many versions is copied once.

type ItemStatus

type ItemStatus string

ItemStatus is the lifecycle of one unit of migration work. An item is keyed by stored content, not by object version, so shared content migrates once.

const (
	// ItemStatusPending is seeded work not yet attempted.
	ItemStatusPending ItemStatus = "pending"
	// ItemStatusRunning is an item leased by a replacement worker.
	ItemStatusRunning ItemStatus = "running"
	// ItemStatusRetrying is temporarily failed work waiting for its persisted
	// retry schedule. It remains retirement-blocking.
	ItemStatusRetrying ItemStatus = "retrying"
	// ItemStatusWaitingSource means no readable copy and no cached content is
	// available yet. The coordinator moves on and revisits it later.
	ItemStatusWaitingSource ItemStatus = "waiting_source"
	// ItemStatusCopied means the target holds a committed readable copy.
	ItemStatusCopied ItemStatus = "copied"
	// ItemStatusCancelled means the content no longer needs migrating.
	ItemStatusCancelled ItemStatus = "cancelled"
	// ItemStatusFailed exhausted its item-level retry budget. Other items may
	// continue, but the source cannot retire until an operator retries it.
	ItemStatusFailed ItemStatus = "failed"
)

func (ItemStatus) Blocking

func (s ItemStatus) Blocking() bool

Blocking reports whether the item prevents the source from being retired.

func (ItemStatus) Executable

func (s ItemStatus) Executable() bool

Executable reports whether an item is still active or recoverable.

func (ItemStatus) Valid

func (s ItemStatus) Valid() bool

Valid reports whether the value is a known item status.

type MigratePayload

type MigratePayload struct {
	ReplacementID int64
}

MigratePayload is the persisted state of the migration coordinator. The durable item queue owns all transfer state.

func ParseMigratePayload

func ParseMigratePayload(task *model.Task) (MigratePayload, error)

ParseMigratePayload decodes a migration coordinator payload. These tasks only ever exist after the upgrade that introduced them, so the replacement ID is required rather than inferred.

type Phase

type Phase string

Phase is the worker responsibility a status maps to. It keeps the coordinator dispatch and the state machine from drifting apart.

const (
	// PhasePrepare creates the approved target service and activates it.
	PhasePrepare Phase = "prepare"
	// PhaseMigrate copies stored content to the target.
	PhaseMigrate Phase = "migrate"
	// PhaseRetire runs the safety gate and terminates the old service.
	PhaseRetire Phase = "retire"
	// PhaseNone means no coordinator should act.
	PhaseNone Phase = "none"
)

func PhaseFor

func PhaseFor(status Status) Phase

PhaseFor maps a status to the work a coordinator should perform.

type ProgressSnapshot

type ProgressSnapshot struct {
	ReplacementID       int64
	Phase               Phase
	SeedingComplete     bool
	ItemsTotal          int
	ItemsProcessed      int
	ItemsCopied         int
	ItemsNoLongerNeeded int
	ItemsPending        int
	ItemsActive         int
	ItemsAttention      int
	ItemsRetrying       int
	ItemsWaitingSource  int
	ItemsFailed         int
	Percent             *int
	NextRetryAt         *time.Time
}

ProgressSnapshot is the UI-neutral aggregate for one replacement. Total is provisional until SeedingComplete is true.

type Replacement

type Replacement struct {
	bun.BaseModel `bun:"table:storage_replacements,alias:storage_replacement"`

	ID       int64 `bun:",pk,autoincrement"`
	BucketID int64 `bun:",notnull"`
	// CopyIndex is the logical replica slot both generations belong to.
	CopyIndex           int              `bun:",notnull"`
	SourceDataSetID     int64            `bun:",notnull"`
	TargetDataSetID     int64            `bun:",notnull"`
	SelectionMode       SelectionMode    `bun:",notnull"`
	RequestedProviderID *types.OnChainID `bun:"type:text"`
	ClientRequestID     string           `bun:",notnull"`
	Status              Status           `bun:",notnull"`
	WaitReason          *WaitReason      `bun:",nullzero"`
	FailureReason       *FailureReason   `bun:",nullzero"`
	LastError           *string          `bun:",nullzero"`
	// ItemsTotal and ItemsCopied are maintained inside the transactions that
	// seed and complete items, so progress never needs a history-sized count.
	ItemsTotal  int `bun:",notnull,default:0"`
	ItemsCopied int `bun:",notnull,default:0"`
	// SeedCursorUploadID advances through storage uploads in bounded batches so
	// no single transaction scales with retained bucket history.
	SeedCursorUploadID int64 `bun:",notnull,default:0"`
	SeedingComplete    bool  `bun:",notnull,default:false"`
	// StateVersion fences item-worker pause requests from a later coordinator
	// recovery. LastDispatchedAt is the durable fairness cursor used by the
	// global item queue.
	StateVersion     int64      `bun:",notnull,default:1"`
	LastDispatchedAt *time.Time `bun:",nullzero"`
	// TerminationEpoch is recorded before the old service is treated as
	// terminated, so a crash between termination and observation re-reads it
	// instead of terminating twice.
	TerminationTxHash     *string    `bun:",nullzero"`
	TerminationEpoch      *int64     `bun:",nullzero"`
	TerminationObservedAt *time.Time `bun:",nullzero"`
	// AbandonedTerminationEpoch records termination of a superseded target.
	// It is separate from the source termination fields above because the two
	// services belong to opposite generations.
	AbandonedTerminationTxHash     *string    `bun:",nullzero"`
	AbandonedTerminationEpoch      *int64     `bun:",nullzero"`
	AbandonedTerminationObservedAt *time.Time `bun:",nullzero"`
	SupersededByID                 *int64     `bun:",nullzero"`
	ConfirmedAt                    time.Time  `bun:",nullzero,notnull,default:current_timestamp"`
	CreatedAt                      time.Time  `bun:",nullzero,notnull,default:current_timestamp"`
	UpdatedAt                      time.Time  `bun:",nullzero,notnull,default:current_timestamp"`
}

Replacement is one operator-approved provider replacement for a single bucket replica slot. It is created only by an explicit confirmation and authorizes a new paid service, the topology switch, migration, and termination of the old service once the safety gate passes.

type SelectionMode

type SelectionMode string

SelectionMode records how the operator chose the target provider.

const (
	// SelectionModeAutomatic picks an eligible provider the bucket has not used.
	SelectionModeAutomatic SelectionMode = "automatic"
	// SelectionModeManual uses exactly the provider the operator supplied.
	SelectionModeManual SelectionMode = "manual"
)

func (SelectionMode) Valid

func (m SelectionMode) Valid() bool

Valid reports whether the value is a known selection mode.

type Status

type Status string

Status is the lifecycle of one approved replacement.

const (
	// StatusPreparingTarget means the approved target service is being created.
	// Writes still go to the source.
	StatusPreparingTarget Status = "preparing_target"
	// StatusMigrating means the target owns the slot for new writes and stored
	// content is being copied across.
	StatusMigrating Status = "migrating"
	// StatusWaiting means progress is blocked on a recoverable dependency. It
	// consumes no retry budget and is never a failure.
	StatusWaiting Status = "waiting"
	// StatusRetiring means migration finished and the source is going through
	// the retirement safety gate.
	StatusRetiring Status = "retiring"
	// StatusCleanupAttention means retirement cannot proceed without an
	// operator decision. Automatic task retry is suppressed.
	StatusCleanupAttention Status = "cleanup_attention"
	// StatusFailed means the work exhausted its retries and needs the operator
	// to retry it from the Data Sets surface.
	StatusFailed Status = "failed"
	// StatusCompleted means the source service was terminated and observed as
	// terminated.
	StatusCompleted Status = "completed"
	// StatusSuperseded means a later confirmation replaced this one.
	StatusSuperseded Status = "superseded"
)

func NextStates

func NextStates(from Status) []Status

NextStates lists the statuses reachable from one status.

func OnTaskExhausted

func OnTaskExhausted(status Status) (Status, bool)

OnTaskExhausted maps a status to the state a terminally exhausted coordinator task leaves behind. Retirement goes to operator attention rather than failed because its remaining work is a cleanup decision, not a retryable copy. The second return value reports whether any change is warranted.

func (Status) Active

func (s Status) Active() bool

Active reports whether a coordinator should still advance this replacement.

func (Status) HoldsSource

func (s Status) HoldsSource() bool

HoldsSource reports whether the replacement still owns its source data set, which is what stops a second replacement from starting on the same source.

func (Status) Retryable

func (s Status) Retryable() bool

Retryable reports whether the dedicated retry action accepts this status. Waiting work resumes on its own and is deliberately excluded.

func (Status) Terminal

func (s Status) Terminal() bool

Terminal reports whether the replacement can never change again.

func (Status) Valid

func (s Status) Valid() bool

Valid reports whether the value is a known status.

type WaitReason

type WaitReason string

WaitReason explains a recoverable pause so an operator can tell "nothing is wrong yet" from "something needs me". It is only meaningful while the status is StatusWaiting.

const (
	// WaitReasonReadableSource means no replica and no cached copy can supply
	// the content this item needs.
	WaitReasonReadableSource WaitReason = "readable_source"
	// WaitReasonTarget means the approved target provider is unreachable.
	WaitReasonTarget WaitReason = "target"
	// WaitReasonTargetCreating means the paid target service has not been
	// created yet.
	WaitReasonTargetCreating WaitReason = "target_creating"
	// WaitReasonTargetWritable means the created service is not writable yet.
	WaitReasonTargetWritable WaitReason = "target_writable"
	// WaitReasonFunding means the wallet cannot yet pay for the target service.
	WaitReasonFunding WaitReason = "funding"
	// WaitReasonProvider means a provider call failed in a way that resolves on
	// its own.
	WaitReasonProvider WaitReason = "provider"
	// WaitReasonTerminationEpoch means the chain has not yet reached the epoch
	// at which the old service ends.
	WaitReasonTerminationEpoch WaitReason = "termination_epoch"
	// WaitReasonSourceWrites means a write to the old provider is still in
	// flight, so it cannot be retired.
	WaitReasonSourceWrites WaitReason = "source_writes"
	// WaitReasonCoverage means some retained version is not yet readable on the
	// new provider.
	WaitReasonCoverage WaitReason = "coverage"
)

func (WaitReason) Message

func (r WaitReason) Message() string

Message is the operator-facing explanation of the pause. It says what is being waited on, never an internal state name.

func (WaitReason) Valid

func (r WaitReason) Valid() bool

Valid reports whether the value is a known wait reason.

Jump to

Keyboard shortcuts

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