Files
llmgw/internal/control/routing.go
T
Ilker ManapandClaude Opus 5.5 540b518fde
CI / test (push) Successful in 9m30s
Rate limits counted across the control planes with router.active_active
Each router's per-counter request and token counts, this minute's and the
last, ride the standby's two-second exchange with the leader. The leader
answers each standby with everyone else's sum, its own included, and puts
the standbys' sum in force on its own router; each limiter adds the
others' counts to its sliding window.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-11-01 17:30:08 +01:00

187 lines
5.9 KiB
Go

package control
import (
"strconv"
"sync"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/accounting"
)
// RoutedReplica is a Ready replica as a router needs it: where it is, what it
// serves and how much it may take. Not its key — every control plane derives
// that from the replica's id and the cluster's secret, so none travels.
type RoutedReplica struct {
ID string `json:"id"`
Model string `json:"model"`
Profile string `json:"profile"`
Pool string `json:"pool"`
Endpoint string `json:"endpoint"`
Weight float64 `json:"weight"`
}
// Routing is what a standby that answers requests itself reads from the
// leader: the replicas it may send to, and which catalog they belong to.
type Routing struct {
Replicas []RoutedReplica `json:"replicas"`
// Catalog changes whenever the leader puts a catalog in force. Only
// compared with the last one a standby saw, never with its own: a model
// added, scaled or given a canary is in the store, and the standby reads
// it again from there.
Catalog string `json:"catalog"`
// Rates are what every other control plane's router has counted against
// each limit, the leader's among them: added to the asker's own, they
// make a key's limits the cluster's.
Rates *accounting.Rates `json:"rates,omitempty"`
}
// Activity is what a standby's router reports back: when it last sent each
// replica a request, and the on-demand models it was asked for. The leader
// evicts and loads by these, and without them a replica busy behind a
// standby looks idle to the one machine that decides.
type Activity struct {
// From names the standby, so its counts replace its own last ones and
// are not given back to it.
From string `json:"from,omitempty"`
LastUsed map[string]time.Time `json:"last_used,omitempty"`
Want []string `json:"want,omitempty"`
// Rates are its router's own counts against each limit.
Rates *accounting.Rates `json:"rates,omitempty"`
}
// RateSharer is a router whose limits can be counted with other routers'.
type RateSharer interface {
Rates() accounting.Rates
SetOtherRates(accounting.Rates)
}
// ratesFresh is how long a standby's counts are believed after it last
// reported: a standby gone quiet stops counting against anyone's limits
// shortly after it stops taking requests.
const ratesFresh = 10 * time.Second
// sharedRates are the standbys' last counts, by name.
type sharedRates struct {
mu sync.Mutex
by map[string]rateReport
seen bool // whether any standby has ever reported
}
type rateReport struct {
rates accounting.Rates
at time.Time
}
// put keeps a standby's latest counts.
func (s *sharedRates) put(from string, r accounting.Rates, now time.Time) {
s.mu.Lock()
defer s.mu.Unlock()
if s.by == nil {
s.by = map[string]rateReport{}
}
s.by[from] = rateReport{rates: r, at: now}
s.seen = true
}
// sum adds every fresh standby's counts but except's; stale ones are
// forgotten. ok is false when no standby has ever reported, and there is
// nothing to put in force.
func (s *sharedRates) sum(except string, now time.Time) (accounting.Rates, bool) {
out := accounting.NewRates(now)
s.mu.Lock()
defer s.mu.Unlock()
for name, r := range s.by {
if now.Sub(r.at) > ratesFresh {
delete(s.by, name)
continue
}
if name != except {
out.Add(r.rates)
}
}
return out, s.seen
}
// ReplicaFeed is a standby's copy of the leader's Ready replicas. ok is false
// when it has none worth using — standbys do not answer requests, or the
// leader has not been heard from lately — and the standby routes nothing.
type ReplicaFeed interface {
Ready() (reps []RoutedReplica, ok bool)
}
// SetReplicaFeed lets a standby route to the leader's replicas.
func (c *Controller) SetReplicaFeed(f ReplicaFeed) { c.feed = f }
// Routing is this control plane's Ready replicas, for a standby to route to.
func (c *Controller) Routing() Routing { return c.RoutingFor("") }
// RoutingFor is Routing for the standby named from, with what every other
// router has counted against each limit.
func (c *Controller) RoutingFor(from string) Routing {
out := Routing{Replicas: c.ready(), Catalog: c.CatalogRev()}
if rs, ok := c.sink.(RateSharer); ok {
others, _ := c.rates.sum(from, time.Now())
others.Add(rs.Rates())
out.Rates = &others
}
return out
}
// ready is the reconciler's Ready replicas, as a router needs them.
func (c *Controller) ready() []RoutedReplica {
reps := c.rec.ReadyReplicas()
out := make([]RoutedReplica, 0, len(reps))
for _, r := range reps {
out = append(out, RoutedReplica{
ID: r.ID, Model: r.Model, Profile: r.Profile, Pool: r.Pool, Endpoint: r.Endpoint, Weight: r.Weight,
})
}
return out
}
// shareRates puts the standbys' counts in force on this control plane's own
// router, while it leads. Once a tick, beside publishing.
func (c *Controller) shareRates() {
rs, ok := c.sink.(RateSharer)
if !ok || (c.lead != nil && !c.lead.IsLeader()) {
return
}
if others, seen := c.rates.sum("", time.Now()); seen {
rs.SetOtherRates(others)
}
}
// CatalogRev names the catalog in force here. The process's owner id with a
// counter: a new leader, or a restarted one, is a different catalog as far as
// any standby knows.
func (c *Controller) CatalogRev() string {
owner := ""
if c.lead != nil {
owner = c.lead.Owner()
}
return owner + "/" + strconv.FormatUint(c.catRev.Load(), 10)
}
// RecordActivity takes in what a standby's router did since its last report.
func (c *Controller) RecordActivity(a Activity) {
if a.From != "" && a.Rates != nil {
c.rates.put(a.From, *a.Rates, time.Now())
}
c.rec.RecordUsage(a.LastUsed)
for _, m := range a.Want {
c.rec.Want(m)
}
}
// routable is what this process's router may send to: the replicas the
// reconciler holds Ready, or, on a standby following the leader, the leader's.
func (c *Controller) routable() []RoutedReplica {
if c.feed != nil && c.lead != nil && !c.lead.IsLeader() {
if reps, ok := c.feed.Ready(); ok {
return reps
}
return nil
}
return c.ready()
}