Documentation
¶
Index ¶
- Constants
- Variables
- func ExpectedModeSpellings() map[string]int
- type PreflightResult
- type State
- type Store
- func (s *Store) Activate(ctx context.Context, floor int64) (bool, error)
- func (s *Store) CurrentState(ctx context.Context) (State, error)
- func (s *Store) Mode(tx *dbr.Tx) (int, error)
- func (s *Store) ObserveReserveRetry()
- func (s *Store) Preflight(ctx context.Context) (PreflightResult, error)
- func (s *Store) ReserveTx(tx *dbr.Tx, channelID string, channelType uint8, count int) ([]int64, error)
Constants ¶
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.
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.
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.
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.
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 ¶
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).
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.
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.
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).
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
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 ¶
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 (*Store) Activate ¶ added in v1.14.0
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
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
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).