Documentation
¶
Overview ¶
Package planner decides which work a shard may dispatch, given what every shard last reported.
Two scheduling rules are global while the thing that enforces them is per-shard: strict priority says no worse band is served while a better one has due work anywhere, and weighted fairness says the distinct keys at that band split the batch by weight. So each shard reports what it sees and plans against the merged picture, on its own clock - no shard ever waits for another.
plan := p.Plan(shard, capacity) // after this shard's own Tally or Clear
A shard's participation is DECLARED, never inferred. Every cycle a shard either Tally's what it saw or Clear's because it could not look; there is no timeout, and a merely slow shard keeps its last tally and stays counted. A shard that fails to scan MUST Clear, or the band it last claimed goes on outranking every peer that could actually serve work.
The planner holds no steps, touches no database, and does no I/O. It deals in fairness keys and counts; resolving a key to actual steps is the caller's job. Every method is safe for concurrent use - one planner is shared by every shard on a replica.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Plan ¶
type Plan struct {
// GlobalBand is the best (lowest) band any live shard has due work at, or math.MaxInt when the
// fleet has nothing due. Slots is drawn entirely from this band - there is no spill.
GlobalBand int
// Slots is the ordered sequence of fairness keys this shard should dispatch, one entry per step.
// A key appearing three times means three of its steps. The order is the global plan's fairness
// interleave, filtered to this shard's occurrences, so replaying it preserves the interleave.
Slots []string
// Keys is the distinct keys in Slots, in first-appearance order - the caller's fetch predicate.
Keys []string
// PerKeyCap is the largest number of slots any single key won on this shard. A caller fetching
// with a uniform per-key cap uses this: it over-fetches for the lighter keys, but keeps the fetch
// a single query, and both factors are bounded by capacity.
PerKeyCap int
}
Plan is one shard's answer for one cycle.
An empty Slots means this shard dispatches nothing, and it deliberately does not say why - nothing is due anywhere, nothing is due here, this shard's work is above the global band, or the band's slots all went to shards holding more of the planned keys. The caller's action is the same in every case (hold no candidates), so distinguishing them would be surface with no consumer. For a log line, compare GlobalBand against the band just tallied.
type Planner ¶
type Planner struct {
// contains filtered or unexported fields
}
Planner holds the most recent Tally from each shard and turns them into per-shard Plans. Safe for concurrent use: one goroutine per shard is the expected caller shape.
func (*Planner) Clear ¶
Clear drops a shard's tally, excluding it from planning until it reports again. A caller whose scan FAILED calls this: it holds that fact directly, so the planner never has to infer a shard's absence from silence and a clock.
Excluding a shard that could not look is the accurate model, not a safety hack. It cannot dispatch this cycle regardless, and the global band means the best band with due work that someone can actually serve. Nothing is lost either: if the shard recovers still holding that band, its next Tally restores it within a cycle.
Leaving a failed shard's last tally in place is what makes this necessary rather than optional. That tally claims a band only it holds, so every peer computes the same global minimum, finds none of its own keys there, and dispatches nothing - forever, waiting on a shard that will never report again. A shard that is merely SLOW is a different case and must not be cleared: it is alive, its last tally is still the best information anyone has, and it will replace it when its scan returns.
func (*Planner) LastBand ¶
LastBand reports the band and distinct-key count of the most recent plan, for a caller inspecting what this planner last decided - which today is the piston's and this package's own tests. A NEGATIVE band means there is nothing to report: either nothing has been planned yet, or the last plan found the fleet idle. Treat that as "no answer" rather than as a band.
func (*Planner) Plan ¶
Plan returns what one shard should dispatch this cycle, drawn from the global band and capped at capacity slots across the whole fleet.
Call it after this shard's own Tally for the cycle: planning first means planning against your own previous report, which at best wastes a cycle and at worst claims a band you no longer hold.
Every shard rolls its own plan from its own snapshot. The lottery is independent per caller, which changes nothing in expectation and needs no coordination.
func (*Planner) Reset ¶
func (p *Planner) Reset()
Reset drops every tally. Call it when the owning process restarts its shard set; a stale tally from a previous run would otherwise claim a band nobody is serving.
func (*Planner) Tally ¶
Tally records what one shard currently sees: its minimum due band, and one Tally per fairness key at that band. It replaces that shard's previous report.
A shard with nothing due must still report (band math.MaxInt, no tallies), and one that tried to look and FAILED must call Clear. Between them those cover every outcome, which is what lets the planner know a shard's state outright instead of inferring it from silence. Staying quiet is not an option a caller has: the previous tally stands until something replaces or clears it.
The tallies slice is HANDED OVER, not copied: it is retained, and normalized in place (a non-positive weight is rewritten to 1 - see below). So a caller must neither mutate nor reuse a slice it has passed, and in particular must not pass the same backing array twice: the second call's normalization would write into an array a snapshot of the first is still handing out to an unlocked Plan. Every producer today allocates a fresh slice per cycle, which is what the contract asks for.
func (*Planner) TallyAge ¶
TallyAge is how long ago the STALEST shard still in the planner reported, and zero when none is - the one number that says how mixed the freshness of a plan's inputs is.
It measures a real coupling with no other readout. Every shard plans from a merged view of every shard's LAST report, so a piston cycling slowly holds its peers' plans on a picture that old: the global band and the slice rule are both computed from those mixed-freshness tallies, and a shard whose cycle stretched to 400ms while its peers spin at 67ms has them planning against a view six of their own cycles stale. Nothing detects or corrects that today, and nothing here should start to - see Clear on why a timeout would be the wrong fix. This exists so the question "are the inputs diverging" can be ASKED, since a throughput number alone cannot distinguish it from a slow database.
Expect roughly one cycle interval in a healthy fleet. Sustained multiples of that name a shard whose piston has fallen behind its peers.
type Tally ¶
Tally is one fairness key's aggregate on one shard, at that shard's minimum due band: how many due steps the key has there, and the age and weight of its OLDEST one.
It is not a step. A Tally with Count 40 stands for 40 steps, and a shard reports one Tally per distinct key - so a tally set is O(keys), never O(backlog). That is the property the whole three-phase shape exists to preserve, and a caller that reports one Tally per step defeats it.
Count is expected to be capped at the planning capacity by whoever produced it. The cap is lossless here: no key is ever assigned more than the whole batch, so a count above capacity is indistinguishable from capacity.
Weight is the fairness weight of the key's oldest due step, deliberately not of its newest - keying weight off the oldest is what stops a tenant self-promoting by queueing newer high-weight work.