carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package gate
2
3import (
4 "sync"
5 "time"
6)
7
8const (
9 limitBurst = 10
10 limitPeriod = time.Minute
11 limitPrune = 10_000
12)
13
14// Limiter is a per-key token bucket: limitBurst attempts, refilling at
15// limitBurst per limitPeriod. bcrypt at cost 10 already costs tens of
16// milliseconds a comparison; this exists so the CPU is spent on people and
17// not on a script.
18type Limiter struct {
19 mu sync.Mutex
20 now func() time.Time
21 buckets map[string]*bucket
22}
23
24type bucket struct {
25 tokens float64
26 last time.Time
27}
28
29func NewLimiter(now func() time.Time) *Limiter {
30 return &Limiter{now: now, buckets: map[string]*bucket{}}
31}
32
33// Allow spends one token for key, reporting whether one was available.
34func (l *Limiter) Allow(key string) bool {
35 l.mu.Lock()
36 defer l.mu.Unlock()
37 now := l.now()
38 b, ok := l.buckets[key]
39 if !ok {
40 if len(l.buckets) >= limitPrune {
41 l.prune(now)
42 }
43 b = &bucket{tokens: limitBurst, last: now}
44 l.buckets[key] = b
45 }
46 refill := now.Sub(b.last).Seconds() * (limitBurst / limitPeriod.Seconds())
47 b.tokens = min(limitBurst, b.tokens+refill)
48 b.last = now
49 if b.tokens < 1 {
50 return false
51 }
52 b.tokens--
53 return true
54}
55
56// prune drops every bucket that has been idle long enough to be full again;
57// keeping it would change no answer.
58func (l *Limiter) prune(now time.Time) {
59 for k, b := range l.buckets {
60 if now.Sub(b.last) >= limitPeriod {
61 delete(l.buckets, k)
62 }
63 }
64}
65