Skip to content

Commit e079298

Browse files
committed
proxy: apply rate limits to individual L402s
1 parent 32a88bc commit e079298

File tree

2 files changed

+79
-13
lines changed

2 files changed

+79
-13
lines changed

proxy/proxy.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"os"
1111
"strconv"
1212
"strings"
13-
"time"
1413

1514
"github.com/lightninglabs/aperture/auth"
1615
"github.com/lightninglabs/aperture/l402"
@@ -168,23 +167,25 @@ func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
168167
return
169168
}
170169

171-
// Apply per-endpoint rate limits, if configured.
170+
// Apply per-endpoint rate limits, if configured. Determine the L402 key
171+
// (preimage hash) if present, otherwise fallback to global limiter.
172+
var l402Key string
173+
if _, preimage, err := l402.FromHeader(&r.Header); err == nil {
174+
l402Key = preimage.Hash().String()
175+
}
172176
for _, rl := range target.compiledRateLimits {
173177
if !rl.re.MatchString(r.URL.Path) {
174178
continue
175179
}
176180

177181
// Fast path: allow if a token is available now.
178-
if rl.allow() {
182+
if rl.allowFor(l402Key) {
179183
continue
180184
}
181185

182186
// Otherwise, compute suggested retry delay without consuming
183187
// tokens.
184-
res := rl.limiter.Reserve()
185-
if res.OK() {
186-
delay := res.Delay()
187-
res.CancelAt(time.Now())
188+
if delay, ok := rl.reserveDelay(l402Key); ok {
188189
if delay > 0 {
189190
// As seconds; for sub-second delays we still
190191
// send 1 second.

proxy/ratelimiter.go

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package proxy
22

33
import (
44
"regexp"
5+
"sync"
56
"time"
67

78
"golang.org/x/time/rate"
@@ -31,8 +32,23 @@ type RateLimit struct {
3132
}
3233

3334
type compiledRateLimit struct {
34-
re *regexp.Regexp
35+
// protects the l402Limiters map.
36+
sync.Mutex
37+
38+
// re is the regular expression used to match the path of the URL.
39+
re *regexp.Regexp
40+
41+
// global limiter is used when no per-L402 key can be derived.
3542
limiter *rate.Limiter
43+
44+
// limiter per L402 key.
45+
limit rate.Limit
46+
47+
// burst is the burst size allowed in addition to steady rate.
48+
burst int
49+
50+
// l402Limiters is a map of per-L402 key limiters.
51+
l402Limiters map[string]*rate.Limiter
3652
}
3753

3854
// compile prepares the regular expression and the limiter.
@@ -57,13 +73,62 @@ func (r *RateLimit) compile() error {
5773

5874
// rate.Every(per/requests) creates an average rate of requests
5975
// per 'per'.
60-
lim := rate.NewLimiter(rate.Every(per/time.Duration(requests)), burst)
61-
r.compiled = &compiledRateLimit{re: re, limiter: lim}
76+
limit := rate.Every(per / time.Duration(requests))
77+
lim := rate.NewLimiter(limit, burst)
78+
r.compiled = &compiledRateLimit{
79+
re: re,
80+
limiter: lim,
81+
limit: limit,
82+
burst: burst,
83+
l402Limiters: make(map[string]*rate.Limiter),
84+
}
6285

6386
return nil
6487
}
6588

66-
// allow returns true if the rate limit permits an event now.
67-
func (c *compiledRateLimit) allow() bool {
68-
return c.limiter.Allow()
89+
// allowFor returns true if the rate limit permits an event now for the given
90+
// key. If the key is empty, the global limiter is used.
91+
func (c *compiledRateLimit) allowFor(key string) bool {
92+
if key == "" {
93+
return c.limiter.Allow()
94+
}
95+
l := c.getOrCreate(key)
96+
97+
return l.Allow()
98+
}
99+
100+
// reserveDelay reserves a token on the limiter for the given key and returns
101+
// the suggested delay. Callers can use the delay to set Retry-After without
102+
// consuming tokens.
103+
func (c *compiledRateLimit) reserveDelay(key string) (time.Duration, bool) {
104+
var l *rate.Limiter
105+
if key == "" {
106+
l = c.limiter
107+
} else {
108+
l = c.getOrCreate(key)
109+
}
110+
111+
res := l.Reserve()
112+
if !res.OK() {
113+
return 0, false
114+
}
115+
116+
delay := res.Delay()
117+
res.CancelAt(time.Now())
118+
119+
return delay, true
120+
}
121+
122+
func (c *compiledRateLimit) getOrCreate(key string) *rate.Limiter {
123+
c.Lock()
124+
defer c.Unlock()
125+
126+
if l, ok := c.l402Limiters[key]; ok {
127+
return l
128+
}
129+
130+
l := rate.NewLimiter(c.limit, c.burst)
131+
c.l402Limiters[key] = l
132+
133+
return l
69134
}

0 commit comments

Comments
 (0)