msgextraseq

package
v1.20.0 Latest Latest
Warning

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

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ModeLegacy        = cutover.ModeInactive
	ModeTransactional = cutover.ModeActive
)

Allocator modes, mirroring octo_message_extra_version_state.mode.

Aliases of the shared constants rather than independent literals: cutover.Flip is what writes this column (`SET mode=cutover.ModeActive`), so declaring 0/1 separately here would let the two drift. If they ever did, a flip would succeed and then readStateForShare — comparing against these — would see a mode matching neither and fail every message_extra write closed.

View Source
const ExpectedModeEnv = "OCTO_MESSAGE_EXTRA_VERSION_EXPECTED_MODE"

ExpectedModeEnv optionally declares the allocator mode a deployment expects. Unset makes no assertion; "legacy"/"transactional" fail closed on mismatch (brief D9 read-side guard). Exported for the `app cutover msgextra status` operator command, which reports the guard alongside the state row.

View Source
const MaxCutoverFloor int64 = maxSafeInteger - MaxReserveCount

MaxCutoverFloor is the largest cutover floor Activate accepts: it leaves at least one full MaxReserveCount batch of headroom below maxSafeInteger, so the first reservations after activation cannot immediately hit ErrOverflow.

View Source
const MaxReserveCount = 1000

MaxReserveCount bounds a single reservation (brief D3). It matches this module's existing chunk magnitude (api_manager.go). Callers that exceed it either reject client input or reserve in chunks.

View Source
const StateTable = "octo_message_extra_version_state"

StateTable is the DB-authoritative allocator-state table (pkg/cutover shape: singleton_id / mode / epoch / cutover_floor).

Variables

View Source
var (
	// ErrInvalidCount is returned when count <= 0 or count > MaxReserveCount.
	ErrInvalidCount = errors.New("msgextraseq: count out of range")
	// ErrOverflow is returned when a reservation would cross maxSafeInteger.
	ErrOverflow = errors.New("msgextraseq: version would exceed 2^53-1")
	// ErrUnknownMode is returned when the state row carries an unrecognized mode.
	ErrUnknownMode = errors.New("msgextraseq: unknown allocator mode")
	// ErrExpectedModeMismatch is returned when the resolved mode does not match the
	// deployment's OCTO_MESSAGE_EXTRA_VERSION_EXPECTED_MODE (or that env is malformed).
	ErrExpectedModeMismatch = errors.New("msgextraseq: allocator mode does not match expected")
	// ErrInvariantViolation is returned when a defensive post-write check fails.
	ErrInvariantViolation = errors.New("msgextraseq: allocator invariant violated")
)

Sentinel errors. Callers map these to their existing localized error contract; the allocator never writes an HTTP response itself (error-handling rule).

View Source
var ErrFloorTooHigh = errors.New("msgextraseq: cutover floor leaves no headroom below 2^53-1")

ErrFloorTooHigh is returned by Activate when the requested cutover floor leaves no headroom below maxSafeInteger, which would make every subsequent reservation fail with ErrOverflow (a write outage). The floor must leave at least one full MaxReserveCount batch of room.

View Source
var ErrFloorTooLow = errors.New("msgextraseq: cutover floor is below the observed max version")

ErrFloorTooLow is returned by Activate when the requested cutover floor is below the maximum version already observed, which would risk reissuing a version at or below an existing one.

View Source
var ErrStateRowMissing = errors.New("msgextraseq: allocator state row missing (run the migration first)")

ErrStateRowMissing is returned when the singleton state row is absent (the migration seeds it; a missing row means the schema is not in place).

View Source
var ErrStateTableMissing = fmt.Errorf("%w: the table itself does not exist", ErrStateRowMissing)

ErrStateTableMissing is the subset of ErrStateRowMissing where the table itself is absent, and it matters here more than in the sibling domain.

This allocator's runtime treats the two OPPOSITELY:

  • missing ROW: readStateForShare maps dbr.ErrNotFound to legacy, so writes keep flowing on the pre-cutover allocator.
  • missing TABLE (MySQL 1146): readStateForShare has no case for it, the error propagates, and EVERY message_extra write fails closed.

So an operator surface must be able to say which one it found. It wraps ErrStateRowMissing, so callers that only care that the authority is absent keep matching on that.

Functions

func ExpectedModeSpellings added in v1.15.0

func ExpectedModeSpellings() map[string]int

ExpectedModeSpellings is the authoritative set of values ExpectedModeEnv accepts, and the mode each one asserts. Anything else is malformed and fails closed.

Exported so the operator command reports the guard using the same table the allocator enforces it with. A second hand-written copy in the CLI could disagree with the running server about whether a value is valid — the guard readout would then contradict the thing it is describing.

Types

type PreflightResult added in v1.14.0

type PreflightResult struct {
	// CurrentMode/CurrentFloor/CurrentEpoch are the live state row values.
	CurrentMode  int
	CurrentFloor int64
	CurrentEpoch uint64
	// MaxMessageExtraVersion is MAX(message_extra.version) across all channels.
	MaxMessageExtraVersion int64
	// MaxLegacySeqBoundary is MAX(seq.min_seq) across the messageExtra GenSeq
	// keys — the upper bound on versions the legacy HiLo allocator has handed out.
	MaxLegacySeqBoundary int64
	// MaxRedisCursor is the largest valid cached messageExtraVersion:* hash value.
	// RedisCursorKeyCount/RedisCursorFieldCount are aggregate visit counts; key
	// and field names are deliberately never surfaced because they contain user,
	// source, and channel identifiers.
	MaxRedisCursor        int64
	RedisCursorKeyCount   int64
	RedisCursorFieldCount int64
	// InvalidRedisCursorFieldCount counts malformed, negative, or above-issued
	// cursors. They cannot be trusted as server-issued and are excluded from floor
	// evidence; upgraded sync handlers repair the per-channel cache when next read.
	InvalidRedisCursorFieldCount int64
	// RecommendedFloor is the max of the three maxima: the smallest floor that
	// cannot reissue an already-used version or sit below a cached sync cursor.
	RecommendedFloor int64
}

PreflightResult reports the evidence behind a recommended cutover floor and the current allocator state. It is produced by a read-only Preflight.

type State

type State struct {
	Mode         int
	Epoch        uint64
	CutoverFloor int64
}

State is the decoded allocator-state row. epoch is surfaced for observability only and is never a write-abort condition (brief C1).

type Store

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

Store is the shared allocator. Construct with New and reuse; it is safe for concurrent use (all mutable state lives in MySQL).

func New

func New(ctx *config.Context) *Store

New builds a Store bound to the given octo-lib context.

func (*Store) Activate added in v1.14.0

func (s *Store) Activate(ctx context.Context, floor int64) (bool, error)

Activate flips the allocator from legacy to transactional under an exclusive lock on the state row. The FOR UPDATE is the drain barrier: it waits for every in-flight writer (each holds the state row FOR SHARE until it commits) to finish under legacy, then no new writer can proceed until this commits — so the maxima it recomputes under the lock are final. floor must be >= that max or the flip is refused (ErrFloorTooLow). Returns flipped=false with a nil error when the allocator is already transactional (idempotent).

func (*Store) CurrentState added in v1.15.0

func (s *Store) CurrentState(ctx context.Context) (State, error)

CurrentState reads the live allocator state row — no locks, no evidence scans, no writes. It is the cheap read behind `app cutover msgextra status`; Preflight is the full-evidence version.

It returns this package's State, not pkg/cutover's: the shared control plane is an implementation detail of the flip, and callers already spell this package's field names (CutoverFloor, not Floor).

func (*Store) Mode added in v1.14.0

func (s *Store) Mode(tx *dbr.Tx) (int, error)

Mode returns the currently effective allocator mode, taking the same shared lock on the state row that ReserveTx does (held until the caller commits). It lets a caller pick a lock order that matches the mode BEFORE it reserves.

This exists for the card write path (#627 D4): all writers must reserve the channel sequence (ReserveTx) before taking any message-row lock so the global lock order is uniform (state → channel-seq → message-row) and cannot deadlock. Card writers deliberately keep message-row-first in legacy mode (PR#548 single-process version monotonicity, where GenSeq has no serializing lock), so they must know the mode up front to choose the order. Non-card writers are already seq-first unconditionally and do not need this.

The double state read (Mode here, then again inside ReserveTx) is harmless: it is the same row under a shared lock the caller already holds.

func (*Store) ObserveReserveRetry added in v1.14.0

func (s *Store) ObserveReserveRetry()

ObserveReserveRetry records that a caller retried its whole transaction after a retriable lock error (MySQL 1213 deadlock / 1205 lock-wait timeout). The allocator itself only emits success/failure; retry is a caller-side signal because only the caller owns the transaction it re-runs (metrics.go reserve_total result=retry, brief D8).

func (*Store) Preflight added in v1.14.0

func (s *Store) Preflight(ctx context.Context) (PreflightResult, error)

Preflight reads (no locks, no writes) the maxima that bound already-issued versions and reports a safe cutover floor plus the current state. It never mutates anything, so it is safe to run against production at any time.

ctx bounds the MySQL reads. It does NOT bound the Redis cursor scan: the client library takes no per-command context, so a scan already in flight runs to completion. That residue is why the operator command's interrupt handling has a second stage.

func (*Store) ReserveTx

func (s *Store) ReserveTx(tx *dbr.Tx, channelID string, channelType uint8, count int) ([]int64, error)

ReserveTx reserves count message_extra versions for the given storage channel within the caller's transaction and returns them in assignment order.

The returned slice always has len == count. In transactional mode the values are a contiguous ascending range; in legacy mode they are count distinct values from GenSeq (contiguity is not guaranteed under concurrency, matching pre-task behavior). Returning explicit per-item values — rather than a single "first" — keeps callers mode-agnostic and behavior-preserving across the Prepare window; see context.yaml.

channelID must be the already-derived storage-channel id (personal chats pass the fake channel id). The allocator does no Space/ownership lookup; those gates stay in the caller (brief D6, space-isolation rule).

Jump to

Keyboard shortcuts

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