@@ -3,6 +3,7 @@ package middleware
33import (
44 "net/http"
55 "sync"
6+ "time"
67
78 "github.com/studiolambda/cosmos/framework"
89 "github.com/studiolambda/cosmos/problem"
@@ -21,11 +22,11 @@ var ErrRateLimited = problem.Problem{
2122// RateLimitOptions configures the rate limiter middleware.
2223type RateLimitOptions struct {
2324 // RequestsPerSecond is the sustained request rate allowed
24- // per key (typically per IP). Defaults to 10 .
25+ // per key (typically per IP). Defaults to 15 .
2526 RequestsPerSecond float64
2627
2728 // Burst is the maximum number of requests allowed in a
28- // single burst above the sustained rate. Defaults to 20 .
29+ // single burst above the sustained rate. Defaults to 30 .
2930 Burst int
3031
3132 // KeyFunc extracts the rate-limit key from a request.
@@ -35,17 +36,28 @@ type RateLimitOptions struct {
3536 // ErrorResponse is the problem returned when a request is
3637 // rate-limited. Defaults to [ErrRateLimited].
3738 ErrorResponse problem.Problem
39+
40+ // CleanupInterval is how often the registry sweeps for
41+ // idle entries. Defaults to 1 minute.
42+ CleanupInterval time.Duration
43+
44+ // MaxIdleTime is how long an entry can be idle before
45+ // being evicted. Defaults to 5 minutes.
46+ MaxIdleTime time.Duration
3847}
3948
40- // DefaultRateLimitOptions holds sensible defaults: 10 req/s
41- // sustained with a burst of 20, keyed by remote address.
49+ // DefaultRateLimitOptions holds sensible defaults: 15 req/s
50+ // sustained with a burst of 30, keyed by remote address.
51+ // Idle entries are evicted after 5 minutes of inactivity.
4252var DefaultRateLimitOptions = RateLimitOptions {
43- RequestsPerSecond : 10 ,
44- Burst : 20 ,
53+ RequestsPerSecond : 15 ,
54+ Burst : 30 ,
4555 KeyFunc : func (r * http.Request ) string {
4656 return r .RemoteAddr
4757 },
48- ErrorResponse : ErrRateLimited ,
58+ ErrorResponse : ErrRateLimited ,
59+ CleanupInterval : 1 * time .Minute ,
60+ MaxIdleTime : 5 * time .Minute ,
4961}
5062
5163// withDefaults returns a copy of the options with zero values
@@ -67,18 +79,38 @@ func (options RateLimitOptions) withDefaults() RateLimitOptions {
6779 options .ErrorResponse = DefaultRateLimitOptions .ErrorResponse
6880 }
6981
82+ if options .CleanupInterval == 0 {
83+ options .CleanupInterval = DefaultRateLimitOptions .CleanupInterval
84+ }
85+
86+ if options .MaxIdleTime == 0 {
87+ options .MaxIdleTime = DefaultRateLimitOptions .MaxIdleTime
88+ }
89+
7090 return options
7191}
7292
73- // rateLimitRegistry manages per-key token bucket rate limiters.
74- // Each unique key gets its own [rate.Limiter] instance, created
75- // on first access and reused for subsequent requests.
93+ // rateLimitEntry pairs a token bucket limiter with the time it
94+ // was last accessed. Entries idle longer than [RateLimitOptions.MaxIdleTime]
95+ // are evicted by the cleanup goroutine.
96+ type rateLimitEntry struct {
97+ limiter * rate.Limiter
98+ lastSeen time.Time
99+ }
100+
101+ // rateLimitRegistry manages per-key token bucket rate limiters
102+ // with automatic eviction of idle entries. Each unique key gets
103+ // its own [rate.Limiter] instance, created on first access and
104+ // reused for subsequent requests. A background goroutine
105+ // periodically removes entries that have been idle longer than
106+ // the configured maximum idle time.
76107type rateLimitRegistry struct {
77- // mu protects concurrent access to the limiters map.
108+ // mu protects concurrent access to the entries map.
78109 mu sync.Mutex
79110
80- // limiters maps rate-limit keys to their token bucket.
81- limiters map [string ]* rate.Limiter
111+ // entries maps rate-limit keys to their limiter and
112+ // last-seen timestamp.
113+ entries map [string ]* rateLimitEntry
82114
83115 // rps is the sustained requests-per-second rate for
84116 // newly created limiters.
@@ -87,46 +119,120 @@ type rateLimitRegistry struct {
87119 // burst is the maximum burst size for newly created
88120 // limiters.
89121 burst int
122+
123+ // stop signals the cleanup goroutine to exit.
124+ stop chan struct {}
90125}
91126
92127// newRateLimitRegistry creates a registry that produces limiters
93- // with the given sustained rate and burst size.
94- func newRateLimitRegistry (rps float64 , burst int ) * rateLimitRegistry {
95- return & rateLimitRegistry {
96- limiters : make (map [string ]* rate.Limiter ),
97- rps : rps ,
98- burst : burst ,
128+ // with the given sustained rate and burst size. It starts a
129+ // background goroutine that evicts entries idle longer than
130+ // maxIdle at the given interval. Call [rateLimitRegistry.close]
131+ // to stop the goroutine.
132+ func newRateLimitRegistry (
133+ rps float64 ,
134+ burst int ,
135+ cleanupInterval time.Duration ,
136+ maxIdle time.Duration ,
137+ ) * rateLimitRegistry {
138+ registry := & rateLimitRegistry {
139+ entries : make (map [string ]* rateLimitEntry ),
140+ rps : rps ,
141+ burst : burst ,
142+ stop : make (chan struct {}),
99143 }
144+
145+ go registry .cleanup (cleanupInterval , maxIdle )
146+
147+ return registry
100148}
101149
102150// get returns the limiter for key, creating one if it does not
103- // already exist.
151+ // already exist. It updates the entry's last-seen timestamp on
152+ // every access.
104153func (registry * rateLimitRegistry ) get (key string ) * rate.Limiter {
105154 registry .mu .Lock ()
106155 defer registry .mu .Unlock ()
107156
108- if limiter , ok := registry .limiters [key ]; ok {
109- return limiter
157+ if entry , ok := registry .entries [key ]; ok {
158+ entry .lastSeen = time .Now ()
159+
160+ return entry .limiter
110161 }
111162
112163 limiter := rate .NewLimiter (rate .Limit (registry .rps ), registry .burst )
113- registry .limiters [key ] = limiter
164+
165+ registry .entries [key ] = & rateLimitEntry {
166+ limiter : limiter ,
167+ lastSeen : time .Now (),
168+ }
114169
115170 return limiter
116171}
117172
118- // RateLimit returns middleware that limits requests to 10 req/s
119- // per IP with a burst of 20 using [DefaultRateLimitOptions].
173+ // size returns the number of entries in the registry.
174+ func (registry * rateLimitRegistry ) size () int {
175+ registry .mu .Lock ()
176+ defer registry .mu .Unlock ()
177+
178+ return len (registry .entries )
179+ }
180+
181+ // cleanup periodically removes entries that have been idle
182+ // longer than maxIdle. It runs until [rateLimitRegistry.close]
183+ // is called.
184+ func (registry * rateLimitRegistry ) cleanup (
185+ interval time.Duration ,
186+ maxIdle time.Duration ,
187+ ) {
188+ ticker := time .NewTicker (interval )
189+ defer ticker .Stop ()
190+
191+ for {
192+ select {
193+ case <- registry .stop :
194+ return
195+ case now := <- ticker .C :
196+ registry .mu .Lock ()
197+
198+ for key , entry := range registry .entries {
199+ if now .Sub (entry .lastSeen ) > maxIdle {
200+ delete (registry .entries , key )
201+ }
202+ }
203+
204+ registry .mu .Unlock ()
205+ }
206+ }
207+ }
208+
209+ // close stops the background cleanup goroutine.
210+ func (registry * rateLimitRegistry ) close () {
211+ close (registry .stop )
212+ }
213+
214+ // RateLimit returns middleware that limits requests to 15 req/s
215+ // per IP with a burst of 30 using [DefaultRateLimitOptions].
216+ // Idle entries are automatically evicted after 5 minutes.
120217func RateLimit () framework.Middleware {
121218 return RateLimitWith (DefaultRateLimitOptions )
122219}
123220
124221// RateLimitWith returns middleware that limits requests using
125222// the provided options. It uses a per-key token bucket algorithm
126- // backed by [golang.org/x/time/rate].
223+ // backed by [golang.org/x/time/rate]. A background goroutine
224+ // periodically evicts entries that have been idle longer than
225+ // [RateLimitOptions.MaxIdleTime] to prevent unbounded memory
226+ // growth.
127227func RateLimitWith (opts RateLimitOptions ) framework.Middleware {
128228 opts = opts .withDefaults ()
129- registry := newRateLimitRegistry (opts .RequestsPerSecond , opts .Burst )
229+
230+ registry := newRateLimitRegistry (
231+ opts .RequestsPerSecond ,
232+ opts .Burst ,
233+ opts .CleanupInterval ,
234+ opts .MaxIdleTime ,
235+ )
130236
131237 return func (next framework.Handler ) framework.Handler {
132238 return func (w http.ResponseWriter , r * http.Request ) error {
0 commit comments