carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package relay
2
3import (
4 "strconv"
5 "sync"
6 "time"
7)
8
9type Limits struct {
10 AppLive, AppPerMinute int
11 AppBytesPerHour int64
12 AccountLive, AccountPerMinute int
13 AccountBytesPerHour int64
14 BoxLive, BoxPerMinute int
15 AllocBitsPerSec, BoxBitsPerSec int64
16}
17
18func DefaultLimits() Limits {
19 return Limits{
20 AppLive: 64, AppPerMinute: 30, AppBytesPerHour: 20 << 30,
21 AccountLive: 256, AccountPerMinute: 60, AccountBytesPerHour: 60 << 30,
22 BoxLive: 2048, BoxPerMinute: 500,
23 AllocBitsPerSec: 8_000_000, BoxBitsPerSec: 200_000_000,
24 }
25}
26
27func LimitsFromEnv(getenv func(string) string) Limits {
28 l := DefaultLimits()
29 i := func(name string, dst *int) {
30 if v, err := strconv.Atoi(getenv("CARLOS_RELAY_LIMIT_" + name)); err == nil && v > 0 {
31 *dst = v
32 }
33 }
34 i64 := func(name string, dst *int64) {
35 if v, err := strconv.ParseInt(getenv("CARLOS_RELAY_LIMIT_"+name), 10, 64); err == nil && v > 0 {
36 *dst = v
37 }
38 }
39 i("APP_LIVE", &l.AppLive)
40 i("APP_PER_MINUTE", &l.AppPerMinute)
41 i64("APP_BYTES_PER_HOUR", &l.AppBytesPerHour)
42 i("ACCOUNT_LIVE", &l.AccountLive)
43 i("ACCOUNT_PER_MINUTE", &l.AccountPerMinute)
44 i64("ACCOUNT_BYTES_PER_HOUR", &l.AccountBytesPerHour)
45 i("BOX_LIVE", &l.BoxLive)
46 i("BOX_PER_MINUTE", &l.BoxPerMinute)
47 i64("ALLOC_BPS", &l.AllocBitsPerSec)
48 i64("BOX_BPS", &l.BoxBitsPerSec)
49 return l
50}
51
52type Identity struct{ Account, App string }
53
54type window struct { // sliding counters bucketed per minute, 60 buckets for an hour
55 at [60]time.Time
56 n [60]int64
57}
58
59func (w *window) add(now time.Time, n int64) {
60 m := now.Truncate(time.Minute)
61 i := int(m.Unix()/60) % 60
62 if !w.at[i].Equal(m) {
63 w.at[i], w.n[i] = m, 0
64 }
65 w.n[i] += n
66}
67
68func (w *window) sum(now time.Time, span time.Duration) int64 {
69 var s int64
70 cut := now.Add(-span)
71 for i := range w.at {
72 if !w.at[i].IsZero() && w.at[i].After(cut) {
73 s += w.n[i]
74 }
75 }
76 return s
77}
78
79type counters struct {
80 live int
81 starts window // attempts per minute
82 bytes window // bytes per rolling hour
83}
84
85type Admission struct {
86 mu sync.Mutex
87 l Limits
88 now func() time.Time
89 app map[Identity]*counters
90 acct map[string]*counters
91 box counters
92}
93
94func NewAdmission(l Limits, now func() time.Time) *Admission {
95 return &Admission{l: l, now: now, app: map[Identity]*counters{}, acct: map[string]*counters{}}
96}
97
98func (a *Admission) get(id Identity) (*counters, *counters) {
99 c, ok := a.app[id]
100 if !ok {
101 c = &counters{}
102 a.app[id] = c
103 }
104 ac, ok := a.acct[id.Account]
105 if !ok {
106 ac = &counters{}
107 a.acct[id.Account] = ac
108 }
109 return c, ac
110}
111
112// Admit counts an attempt for the app, the account and the box whether or
113// not it is admitted, then checks live caps, rates and budgets in that order.
114func (a *Admission) Admit(id Identity) (bool, string) {
115 a.mu.Lock()
116 defer a.mu.Unlock()
117 now := a.now()
118 c, ac := a.get(id)
119 c.starts.add(now, 1)
120 ac.starts.add(now, 1)
121 a.box.starts.add(now, 1)
122 switch {
123 case c.live >= a.l.AppLive:
124 return false, "app live"
125 case ac.live >= a.l.AccountLive:
126 return false, "account live"
127 case a.box.live >= a.l.BoxLive:
128 return false, "box live"
129 case c.starts.sum(now, time.Minute) > int64(a.l.AppPerMinute):
130 return false, "app rate"
131 case ac.starts.sum(now, time.Minute) > int64(a.l.AccountPerMinute):
132 return false, "account rate"
133 case a.box.starts.sum(now, time.Minute) > int64(a.l.BoxPerMinute):
134 return false, "box rate"
135 case c.bytes.sum(now, time.Hour) > a.l.AppBytesPerHour:
136 return false, "app budget"
137 case ac.bytes.sum(now, time.Hour) > a.l.AccountBytesPerHour:
138 return false, "account budget"
139 }
140 c.live++
141 ac.live++
142 a.box.live++
143 return true, ""
144}
145
146func (a *Admission) Release(id Identity) {
147 a.mu.Lock()
148 defer a.mu.Unlock()
149 c, ac := a.get(id)
150 if c.live > 0 {
151 c.live--
152 }
153 if ac.live > 0 {
154 ac.live--
155 }
156 if a.box.live > 0 {
157 a.box.live--
158 }
159}
160
161// AddBytes records forwarded bytes and reports whether the app or account
162// rolling-hour budget is now exceeded.
163func (a *Admission) AddBytes(id Identity, n int64) bool {
164 a.mu.Lock()
165 defer a.mu.Unlock()
166 now := a.now()
167 c, ac := a.get(id)
168 c.bytes.add(now, n)
169 ac.bytes.add(now, n)
170 return c.bytes.sum(now, time.Hour) > a.l.AppBytesPerHour || ac.bytes.sum(now, time.Hour) > a.l.AccountBytesPerHour
171}
172
173func (a *Admission) Seed(id Identity, at time.Time, n int64) {
174 a.mu.Lock()
175 defer a.mu.Unlock()
176 c, ac := a.get(id)
177 c.bytes.add(at, n)
178 ac.bytes.add(at, n)
179}
180
181func (a *Admission) OverBudget(id Identity, now time.Time) bool {
182 a.mu.Lock()
183 defer a.mu.Unlock()
184 c, ac := a.get(id)
185 return c.bytes.sum(now, time.Hour) > a.l.AppBytesPerHour || ac.bytes.sum(now, time.Hour) > a.l.AccountBytesPerHour
186}
187
188// Bucket is a token bucket in bytes with a one-second burst.
189type Bucket struct {
190 mu sync.Mutex
191 rate float64 // bytes per second
192 tokens float64
193 last time.Time
194 now func() time.Time
195}
196
197func NewBucket(bitsPerSec int64, now func() time.Time) *Bucket {
198 r := float64(bitsPerSec) / 8
199 return &Bucket{rate: r, tokens: r, last: now(), now: now}
200}
201
202func (b *Bucket) Allow(nbytes int) bool {
203 b.mu.Lock()
204 defer b.mu.Unlock()
205 t := b.now()
206 b.tokens += t.Sub(b.last).Seconds() * b.rate
207 if b.tokens > b.rate {
208 b.tokens = b.rate
209 }
210 b.last = t
211 if float64(nbytes) > b.tokens {
212 return false
213 }
214 b.tokens -= float64(nbytes)
215 return true
216}
217
218// Peek refills the bucket and reports whether nbytes would fit, without
219// spending anything. Pair it with Spend so multiple buckets can be checked
220// before any of them is debited.
221func (b *Bucket) Peek(nbytes int) bool {
222 b.mu.Lock()
223 defer b.mu.Unlock()
224 t := b.now()
225 b.tokens += t.Sub(b.last).Seconds() * b.rate
226 if b.tokens > b.rate {
227 b.tokens = b.rate
228 }
229 b.last = t
230 return float64(nbytes) <= b.tokens
231}
232
233// Spend debits nbytes unconditionally, clamping at zero.
234func (b *Bucket) Spend(nbytes int) {
235 b.mu.Lock()
236 defer b.mu.Unlock()
237 b.tokens -= float64(nbytes)
238 if b.tokens < 0 {
239 b.tokens = 0
240 }
241}
242