CI / test (push) Successful in 9m30s
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>
111 lines
3.1 KiB
Go
111 lines
3.1 KiB
Go
package accounting
|
|
|
|
import "time"
|
|
|
|
// Counts are one limiter's counts by counter: this minute's and the one
|
|
// before, as absolute figures, so reporting them twice changes nothing.
|
|
type Counts struct {
|
|
Minute time.Time `json:"minute"`
|
|
Current map[string]int64 `json:"current,omitempty"`
|
|
Previous map[string]int64 `json:"previous,omitempty"`
|
|
}
|
|
|
|
// Add sums other into c, aligned to c's minute: counts of a minute that is
|
|
// c's previous one land there, older ones are dropped. Clocks a second apart
|
|
// at a minute's turn are why.
|
|
func (c *Counts) Add(other Counts) {
|
|
switch {
|
|
case other.Minute.Equal(c.Minute):
|
|
c.Current = addInto(c.Current, other.Current)
|
|
c.Previous = addInto(c.Previous, other.Previous)
|
|
case other.Minute.Add(windowLen).Equal(c.Minute):
|
|
c.Previous = addInto(c.Previous, other.Current)
|
|
}
|
|
}
|
|
|
|
func addInto(dst, src map[string]int64) map[string]int64 {
|
|
if len(src) == 0 {
|
|
return dst
|
|
}
|
|
if dst == nil {
|
|
dst = make(map[string]int64, len(src))
|
|
}
|
|
for k, v := range src {
|
|
dst[k] += v
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// Counts reads this limiter's own counts, without the others'.
|
|
func (l *Limiter) Counts() Counts {
|
|
now := l.now()
|
|
out := Counts{Minute: now.Truncate(windowLen), Current: map[string]int64{}, Previous: map[string]int64{}}
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
for k, w := range l.keys {
|
|
w.roll(now)
|
|
if w.current > 0 {
|
|
out.Current[k] = w.current
|
|
}
|
|
if w.previous > 0 {
|
|
out.Previous[k] = w.previous
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// SetOthers replaces what the other control planes have counted. A counter
|
|
// they did not mention has nothing from them.
|
|
func (l *Limiter) SetOthers(c Counts) {
|
|
now := l.now()
|
|
aligned := Counts{Minute: now.Truncate(windowLen)}
|
|
aligned.Add(c)
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
for k, w := range l.keys {
|
|
w.roll(now)
|
|
w.othersCurrent, w.othersPrevious = aligned.Current[k], aligned.Previous[k]
|
|
delete(aligned.Current, k)
|
|
delete(aligned.Previous, k)
|
|
}
|
|
// Counters only others have used yet: a key spread over the cluster is
|
|
// limited here from its first request.
|
|
for k := range aligned.Current {
|
|
w := l.windowLocked(k, now)
|
|
w.othersCurrent, w.othersPrevious = aligned.Current[k], aligned.Previous[k]
|
|
delete(aligned.Previous, k)
|
|
}
|
|
for k, v := range aligned.Previous {
|
|
l.windowLocked(k, now).othersPrevious = v
|
|
}
|
|
}
|
|
|
|
// Rates are an accountant's two limiters' counts.
|
|
type Rates struct {
|
|
Requests Counts `json:"requests"`
|
|
Tokens Counts `json:"tokens"`
|
|
}
|
|
|
|
// Add sums other into r.
|
|
func (r *Rates) Add(other Rates) {
|
|
r.Requests.Add(other.Requests)
|
|
r.Tokens.Add(other.Tokens)
|
|
}
|
|
|
|
// NewRates is an empty sum for the minute now is in.
|
|
func NewRates(now time.Time) Rates {
|
|
m := now.Truncate(windowLen)
|
|
return Rates{Requests: Counts{Minute: m}, Tokens: Counts{Minute: m}}
|
|
}
|
|
|
|
// Rates reads this process's own request and token counts.
|
|
func (a *Accountant) Rates() Rates {
|
|
return Rates{Requests: a.requests.Counts(), Tokens: a.tokens.Counts()}
|
|
}
|
|
|
|
// SetOtherRates puts the other control planes' counts in force here.
|
|
func (a *Accountant) SetOtherRates(r Rates) {
|
|
a.requests.SetOthers(r.Requests)
|
|
a.tokens.SetOthers(r.Tokens)
|
|
}
|