| new file mode 100644 |
| index 000000000..8fe0a1621 |
| --- /dev/null |
| +++ b/internal/edge/doorlimit.go |
| @@ -0,0 +1,111 @@ |
| +package edge |
| + |
| +import ( |
| + "container/list" |
| + "sync" |
| + "time" |
| +) |
| + |
| +// The browser door's rate (spec §3.4): token buckets per app for each |
| +// door and per visitor, all in edge memory, all dropping silently and |
| +// counting dropped.rate. Visitor state is bounded per app with LRU |
| +// eviction so a scan of a million addresses costs a fixed table. |
| +const ( |
| + doorAppPerMinute = 600 |
| + doorVisitorPerMinute = 60 |
| + doorVisitorEntries = 10000 |
| +) |
| + |
| +type tokenBucket struct { |
| + tokens float64 |
| + cap float64 |
| + last time.Time |
| +} |
| + |
| +func (b *tokenBucket) take(now time.Time) bool { |
| + if b.last.IsZero() { |
| + b.tokens, b.last = b.cap, now |
| + } |
| + if el := now.Sub(b.last); el > 0 { |
| + b.tokens += el.Seconds() * b.cap / 60 |
| + if b.tokens > b.cap { |
| + b.tokens = b.cap |
| + } |
| + b.last = now |
| + } |
| + if b.tokens < 1 { |
| + return false |
| + } |
| + b.tokens-- |
| + return true |
| +} |
| + |
| +func (b *tokenBucket) has(now time.Time) bool { // peek: refill, but spend nothing |
| + if b.last.IsZero() { |
| + b.tokens, b.last = b.cap, now |
| + } |
| + if el := now.Sub(b.last); el > 0 { |
| + b.tokens += el.Seconds() * b.cap / 60 |
| + if b.tokens > b.cap { |
| + b.tokens = b.cap |
| + } |
| + b.last = now |
| + } |
| + return b.tokens >= 1 |
| +} |
| + |
| +type visitorEntry struct { |
| + key uint64 |
| + b tokenBucket |
| +} |
| + |
| +type doorAppState struct { |
| + bucket map[byte]*tokenBucket // 'e', 'a' |
| + visitors *list.List // front = most recent; *visitorEntry values |
| + visitorIdx map[uint64]*list.Element |
| +} |
| + |
| +type doorLimiter struct { |
| + mu sync.Mutex |
| + apps map[string]*doorAppState |
| +} |
| + |
| +func newDoorLimiter() *doorLimiter { return &doorLimiter{apps: map[string]*doorAppState{}} } |
| + |
| +// allow spends one token from the app's door bucket and one from the |
| +// visitor's bucket; when either is empty nothing is spent and the post is |
| +// refused. Both are checked before either is charged. |
| +func (l *doorLimiter) allow(app string, door byte, visitor uint64, now time.Time) bool { |
| + l.mu.Lock() |
| + defer l.mu.Unlock() |
| + st := l.apps[app] |
| + if st == nil { |
| + st = &doorAppState{ |
| + bucket: map[byte]*tokenBucket{'e': {cap: doorAppPerMinute}, 'a': {cap: doorAppPerMinute}}, |
| + visitors: list.New(), |
| + visitorIdx: map[uint64]*list.Element{}, |
| + } |
| + l.apps[app] = st |
| + } |
| + ab := st.bucket[door] |
| + if ab == nil { |
| + return false |
| + } |
| + el := st.visitorIdx[visitor] |
| + if el == nil { |
| + if st.visitors.Len() >= doorVisitorEntries { |
| + old := st.visitors.Back() |
| + st.visitors.Remove(old) |
| + delete(st.visitorIdx, old.Value.(*visitorEntry).key) |
| + } |
| + el = st.visitors.PushFront(&visitorEntry{key: visitor, b: tokenBucket{cap: doorVisitorPerMinute}}) |
| + st.visitorIdx[visitor] = el |
| + } else { |
| + st.visitors.MoveToFront(el) |
| + } |
| + vb := &el.Value.(*visitorEntry).b |
| + if !ab.has(now) || !vb.has(now) { |
| + return false |
| + } |
| + return ab.take(now) && vb.take(now) |
| +} |