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

reconcile

package
v0.16.1 Latest Latest
Warning

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

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

Documentation

Overview

Package reconcile retires the durable remnants a crashed control-plane operation leaves behind. It holds no lease, names no epoch, and settles nothing a session's owner is responsible for.

Why this is not internal/admission

The obvious home for a gate sweep is internal/admission: it already owns the due-command reconciler, the service-sweep authorization and the gate vocabulary. It is deliberately not used, and the reason is structural rather than a preference about package size.

This sweep's one hard boundary is that it must never answer, deny, suspend, restore or synthesize a resolution for a gate -- see GateSweeper.Sweep. In internal/admission that boundary would be defended only by the seams the sweeper declared, because Service.AdmitGateResponse and Service.admit are package-local calls: a sweeper there can reach the answer path without a seam to walk and without a store to name. Here it cannot reach it at all, because this package does not import internal/admission and holds no way to express a command. TestTheGateSweepImportsNoPlaneThatCanAnswerAGate and TestTheGateSweepReachesNoGateResolutionCapability hold both halves.

Index

Constants

This section is empty.

Variables

View Source
var ErrInvalidGateSweeperConfig = errors.New("reconcile: invalid gate sweeper configuration")

ErrInvalidGateSweeperConfig reports a gate sweeper configuration refused before it could reach a store.

It is its own sentinel rather than a shared one, so a deployment composing several sweepers can tell which one it misconfigured.

Functions

This section is empty.

Types

type Clock

type Clock interface {
	Now() time.Time
}

Clock is the time seam.

It is Now alone, where admission.Clock also has AfterFunc. A sweep is driven by its caller's cadence and does the work of one pass inside one call; a seam that could schedule would let it continue after the call it was made in returned, which is not a lifetime this package can account for.

type DueGates

type DueGates interface {
	ControlShards() int
	ListDueGates(context.Context, sessionstore.ListDueGatesRequest) (sessionstore.DueGatePage, error)
}

DueGates is the service-plane due-gate query.

It is TWO methods for the reason admission.DueCommands is: a sweeper needs the shard count and one shard's page, and the count is a READ of the decision the backend is committed to rather than a setting. A sweeper configured with its own count would silently never visit some shards.

It is deliberately NOT sessionstore.Store and deliberately not admission.Commands. Both carry admission and settlement methods, and a seam that carried them would put the answer path this sweep may not take within reach of the sweep -- which is the thing step 3 is about.

type GateDisposition

type GateDisposition uint8

GateDisposition is what one pass did about one remnant intent, or why it did nothing.

It is a closed vocabulary with a rendering for every member including an unrecognized one, for the reason admission.Disposition is: a row's fate is operational data and "" is not a fate.

const (
	// GateDispositionRetired means the intent is now a tombstone. It covers
	// the repeat: SessionStore answers a retirement of an already-tombstoned
	// intent with success and writes nothing, because a caller cannot tell a
	// lost reply from a failure. So a crash between the page and the write
	// costs a repeat and nothing else, which is what makes step 2's validation
	// safe to repeat rather than merely usually-repeated.
	GateDispositionRetired GateDisposition = iota

	// GateDispositionTooSoon means the intent is younger than
	// sessionstore.MinGateIntentRemnantAge. Inside that window a remnant and an
	// OpenGate still in flight are the same bytes, so the store refuses. It is
	// an ordinary outcome: the row stays due and a later pass retires it.
	GateDispositionTooSoon

	// GateDispositionReopened means the store refused because the session's own
	// durable record projects the gate as OPEN. The page said remnant and the
	// store's independent re-read said otherwise, which is a gate opened
	// between the two. Retiring it anyway would leave a public gate with no
	// deadline in any view, under an identity that can never be reused.
	GateDispositionReopened

	// GateDispositionRaceLost means the intent row moved between the page and
	// the compare-and-swap, so the retirement was aimed at bytes that no longer
	// exist. Distinct from Reopened because the two want different things from
	// a caller: re-read versus nothing at all.
	GateDispositionRaceLost

	// GateDispositionAbsent means there is no such intent row. SessionStore
	// never erases, so a retired intent has a durable spelling and it is a
	// tombstone; an absent row is one this store has never held.
	GateDispositionAbsent
)

func (GateDisposition) String

func (d GateDisposition) String() string

type GateIntents

type GateIntents interface {
	RetireGateDeadlineIntent(context.Context, sessionstore.RetireGateDeadlineIntentRequest) error
}

GateIntents is the one durable write this sweeper makes.

Exactly one method, and the narrowness is load-bearing rather than tidy: OpenGate and ResolveGate live beside RetireGateDeadlineIntent on the same store value, and both are writes this package must never make. OpenGate would resurrect a deadline; ResolveGate is precisely the synthesized resolution step 3 forbids. A seam offering either is authority this package has no licence for.

type GateSweepResult

type GateSweepResult struct {
	// Shard is the control shard this pass visited.
	Shard int

	// Pages, Examined and Unreadable are the store's own cost report,
	// accumulated over the pages this pass read. They are reported rather than
	// folded together: a full page that reported nothing is a different state
	// from nothing being due, and rows nothing could decode are a different
	// state again.
	Pages      int
	Examined   int
	Unreadable int

	// Remnants counts the retireable rows the store reported.
	Remnants int

	// OpenPastDeadline counts the due rows whose gate the session's durable
	// record still projects as OPEN.
	//
	// It is a COUNT AND NOTHING ELSE, and that is step 3. Such a gate is
	// current due work that gate continuation -- a later task, in the Host --
	// owns. This pass leaves it open, leaves it due, and moves past it on the
	// bounded cursor so it cannot starve the records behind it.
	OpenPastDeadline int

	// Retired counts the intents this pass tombstoned, which is the same number
	// as Dispositions[GateDispositionRetired] and is carried separately because
	// it is the one number an operator reads to know the sweep is doing
	// anything.
	Retired int

	// Dispositions counts every recorded outcome by kind.
	Dispositions map[GateDisposition]int

	// Queries counts the store calls this pass made that reach a provider: due
	// pages and retirements. ControlShards is deliberately not counted -- it
	// reads a decision the store already holds and makes no provider request.
	Queries int

	// Resumed reports that this pass continued a cursor a previous pass over
	// this shard persisted, rather than starting at the head of the view.
	Resumed bool

	// Exhausted reports that this pass reached the end of the shard's due view,
	// so the next pass over it re-arms at the head against a fresh wall-clock
	// bound.
	Exhausted bool

	// Truncated reports that MaxPages was reached with a continuation still
	// outstanding. The position is persisted, so the next pass over this shard
	// continues from it.
	Truncated bool
}

GateSweepResult is what one pass over one shard did, and what producing it cost.

type GateSweeper

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

GateSweeper retires stale gate deadline intents, one control shard per pass, round-robin.

It holds two pieces of state and they are different in kind. The rotor is ADVICE: a restarted replica resumes at shard zero and loses nothing. The per-shard cursors are POSITION, and that is what step 3's anti-starvation rests on -- a shard whose head is occupied by gates this sweep may not touch is continued past them on the next pass instead of meeting them again.

There is no reconciliation claim here, unlike admission.Reconciler, and the omission is deliberate. A claim is per SESSION while a due gate page mixes every tenant that hashes into the shard, so a claim would be taken and given back per row; and RetireGateDeadlineIntent is a compare-and-swap onto the revision the page reported, so two replicas racing one intent produce one retirement and one GateDispositionRaceLost rather than a wrong answer. The claim would suppress duplicate cost, not protect a decision. Separately, sessionstore v0.9.0's reconciliationClaimDue returns the zero storage.Due, so a claim taken here would sit in no deadline view at all.

func NewGateSweeper

func NewGateSweeper(cfg GateSweeperConfig) (*GateSweeper, error)

NewGateSweeper validates a configuration before it can reach a store.

func (*GateSweeper) Sweep

func (s *GateSweeper) Sweep(ctx context.Context, principal identity.Principal) (GateSweepResult, error)

Sweep retires the stale gate deadline intents in the next control shard.

STEP ONE'S AUTHORIZATION IS AuthorizeServiceSweep AND NOTHING ELSE. This is a cross-tenant control-plane query -- a shard holds whichever tenants hash into it, so one page can mix them -- and AuthorizeServiceSweep is the decision a tenant principal can never reach. AuthorizeControl is not called, and not because it would be redundant: AuthorizeControl is how a gate ANSWER is authorized, and a sweep that called it would be a sweep holding the licence for the write it may not make.

STEP THREE IS THE HARD BOUNDARY. A due row whose gate is still publicly open is counted in OpenPastDeadline and nothing else happens to it. This sweeper cannot answer it, deny it, suspend the session, restore it, or write a resolution, and that is not a promise made in this comment -- it holds because the only two seams it has are DueGates and GateIntents, and neither can express any of those, which TestTheGateSweepReachesNoGateResolutionCapability pins against the compiled types.

type GateSweeperConfig

type GateSweeperConfig struct {
	// Authorizer decides the sweep. The method called is AuthorizeServiceSweep
	// and there is no other; see GateSweeper.Sweep.
	Authorizer SweepAuthorizer

	Due     DueGates
	Intents GateIntents
	Clock   Clock

	// PageLimit bounds one due page.
	PageLimit int

	// MaxPages bounds ONE pass over ONE shard. It is a bound on the pass and
	// not on the work: a shard with more due rows than MaxPages*PageLimit is
	// reported Truncated and continued by the next pass over that shard, from
	// the cursor this sweeper persisted. Draining instead would let one busy
	// shard hold the rotor, and therefore every other shard's sweep, for as
	// long as its backlog lasts.
	MaxPages int
}

GateSweeperConfig is one replica's gate-deadline sweep configuration.

type SweepAuthorizer

type SweepAuthorizer interface {
	AuthorizeServiceSweep(ctx context.Context, principal identity.Principal) error
}

SweepAuthorizer decides the cross-tenant service sweep.

It declares AuthorizeServiceSweep ALONE, where admission.Authorizer declares AuthorizeControl beside it. That is the difference that matters here: AuthorizeControl is how a gate answer is authorized (admission.CommandGateResponse), so a sweeper holding admission.Authorizer would hold the authorization half of the path it may not take. Factory's authorizer satisfies both interfaces, so nothing is duplicated at the composition; what is removed is the reach.

Jump to

Keyboard shortcuts

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