| new file mode 100644 |
| index 000000000..e0e208132 |
| --- /dev/null |
| +++ b/internal/relay/limits.go |
| @@ -0,0 +1,216 @@ |
| +package relay |
| + |
| +import ( |
| + "strconv" |
| + "sync" |
| + "time" |
| +) |
| + |
| +type Limits struct { |
| + AppLive, AppPerMinute int |
| + AppBytesPerHour int64 |
| + AccountLive, AccountPerMinute int |
| + AccountBytesPerHour int64 |
| + BoxLive, BoxPerMinute int |
| + AllocBitsPerSec, BoxBitsPerSec int64 |
| +} |
| + |
| +func DefaultLimits() Limits { |
| + return Limits{ |
| + AppLive: 64, AppPerMinute: 30, AppBytesPerHour: 20 << 30, |
| + AccountLive: 256, AccountPerMinute: 60, AccountBytesPerHour: 60 << 30, |
| + BoxLive: 2048, BoxPerMinute: 500, |
| + AllocBitsPerSec: 8_000_000, BoxBitsPerSec: 200_000_000, |
| + } |
| +} |
| + |
| +func LimitsFromEnv(getenv func(string) string) Limits { |
| + l := DefaultLimits() |
| + i := func(name string, dst *int) { |
| + if v, err := strconv.Atoi(getenv("CARLOS_RELAY_LIMIT_" + name)); err == nil && v > 0 { |
| + *dst = v |
| + } |
| + } |
| + i64 := func(name string, dst *int64) { |
| + if v, err := strconv.ParseInt(getenv("CARLOS_RELAY_LIMIT_"+name), 10, 64); err == nil && v > 0 { |
| + *dst = v |
| + } |
| + } |
| + i("APP_LIVE", &l.AppLive) |
| + i("APP_PER_MINUTE", &l.AppPerMinute) |
| + i64("APP_BYTES_PER_HOUR", &l.AppBytesPerHour) |
| + i("ACCOUNT_LIVE", &l.AccountLive) |
| + i("ACCOUNT_PER_MINUTE", &l.AccountPerMinute) |
| + i64("ACCOUNT_BYTES_PER_HOUR", &l.AccountBytesPerHour) |
| + i("BOX_LIVE", &l.BoxLive) |
| + i("BOX_PER_MINUTE", &l.BoxPerMinute) |
| + i64("ALLOC_BPS", &l.AllocBitsPerSec) |
| + i64("BOX_BPS", &l.BoxBitsPerSec) |
| + return l |
| +} |
| + |
| +type Identity struct{ Account, App string } |
| + |
| +type window struct { // sliding counters bucketed per minute, 60 buckets for an hour |
| + at [60]time.Time |
| + n [60]int64 |
| +} |
| + |
| +func (w *window) add(now time.Time, n int64) { |
| + m := now.Truncate(time.Minute) |
| + i := int(m.Unix()/60) % 60 |
| + if !w.at[i].Equal(m) { |
| + w.at[i], w.n[i] = m, 0 |
| + } |
| + w.n[i] += n |
| +} |
| + |
| +func (w *window) sum(now time.Time, span time.Duration) int64 { |
| + var s int64 |
| + cut := now.Add(-span) |
| + for i := range w.at { |
| + if !w.at[i].IsZero() && w.at[i].After(cut) { |
| + s += w.n[i] |
| + } |
| + } |
| + return s |
| +} |
| + |
| +type counters struct { |
| + live int |
| + starts window // attempts per minute |
| + bytes window // bytes per rolling hour |
| +} |
| + |
| +type Admission struct { |
| + mu sync.Mutex |
| + l Limits |
| + now func() time.Time |
| + app map[Identity]*counters |
| + acct map[string]*counters |
| + box counters |
| +} |
| + |
| +func NewAdmission(l Limits, now func() time.Time) *Admission { |
| + return &Admission{l: l, now: now, app: map[Identity]*counters{}, acct: map[string]*counters{}} |
| +} |
| + |
| +func (a *Admission) get(id Identity) (*counters, *counters) { |
| + c, ok := a.app[id] |
| + if !ok { |
| + c = &counters{} |
| + a.app[id] = c |
| + } |
| + ac, ok := a.acct[id.Account] |
| + if !ok { |
| + ac = &counters{} |
| + a.acct[id.Account] = ac |
| + } |
| + return c, ac |
| +} |
| + |
| +// Admit counts an attempt for the app, the account and the box whether or |
| +// not it is admitted, then checks live caps, rates and budgets in that order. |
| +func (a *Admission) Admit(id Identity) (bool, string) { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + now := a.now() |
| + c, ac := a.get(id) |
| + c.starts.add(now, 1) |
| + ac.starts.add(now, 1) |
| + a.box.starts.add(now, 1) |
| + switch { |
| + case c.live >= a.l.AppLive: |
| + return false, "app live" |
| + case ac.live >= a.l.AccountLive: |
| + return false, "account live" |
| + case a.box.live >= a.l.BoxLive: |
| + return false, "box live" |
| + case c.starts.sum(now, time.Minute) > int64(a.l.AppPerMinute): |
| + return false, "app rate" |
| + case ac.starts.sum(now, time.Minute) > int64(a.l.AccountPerMinute): |
| + return false, "account rate" |
| + case a.box.starts.sum(now, time.Minute) > int64(a.l.BoxPerMinute): |
| + return false, "box rate" |
| + case c.bytes.sum(now, time.Hour) > a.l.AppBytesPerHour: |
| + return false, "app budget" |
| + case ac.bytes.sum(now, time.Hour) > a.l.AccountBytesPerHour: |
| + return false, "account budget" |
| + } |
| + c.live++ |
| + ac.live++ |
| + a.box.live++ |
| + return true, "" |
| +} |
| + |
| +func (a *Admission) Release(id Identity) { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + c, ac := a.get(id) |
| + if c.live > 0 { |
| + c.live-- |
| + } |
| + if ac.live > 0 { |
| + ac.live-- |
| + } |
| + if a.box.live > 0 { |
| + a.box.live-- |
| + } |
| +} |
| + |
| +// AddBytes records forwarded bytes and reports whether the app or account |
| +// rolling-hour budget is now exceeded. |
| +func (a *Admission) AddBytes(id Identity, n int64) bool { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + now := a.now() |
| + c, ac := a.get(id) |
| + c.bytes.add(now, n) |
| + ac.bytes.add(now, n) |
| + return c.bytes.sum(now, time.Hour) > a.l.AppBytesPerHour || ac.bytes.sum(now, time.Hour) > a.l.AccountBytesPerHour |
| +} |
| + |
| +func (a *Admission) Seed(id Identity, at time.Time, n int64) { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + c, ac := a.get(id) |
| + c.bytes.add(at, n) |
| + ac.bytes.add(at, n) |
| +} |
| + |
| +func (a *Admission) OverBudget(id Identity, now time.Time) bool { |
| + a.mu.Lock() |
| + defer a.mu.Unlock() |
| + c, ac := a.get(id) |
| + return c.bytes.sum(now, time.Hour) > a.l.AppBytesPerHour || ac.bytes.sum(now, time.Hour) > a.l.AccountBytesPerHour |
| +} |
| + |
| +// Bucket is a token bucket in bytes with a one-second burst. |
| +type Bucket struct { |
| + mu sync.Mutex |
| + rate float64 // bytes per second |
| + tokens float64 |
| + last time.Time |
| + now func() time.Time |
| +} |
| + |
| +func NewBucket(bitsPerSec int64, now func() time.Time) *Bucket { |
| + r := float64(bitsPerSec) / 8 |
| + return &Bucket{rate: r, tokens: r, last: now(), now: now} |
| +} |
| + |
| +func (b *Bucket) Allow(nbytes int) bool { |
| + b.mu.Lock() |
| + defer b.mu.Unlock() |
| + t := b.now() |
| + b.tokens += t.Sub(b.last).Seconds() * b.rate |
| + if b.tokens > b.rate { |
| + b.tokens = b.rate |
| + } |
| + b.last = t |
| + if float64(nbytes) > b.tokens { |
| + return false |
| + } |
| + b.tokens -= float64(nbytes) |
| + return true |
| +} |