Skip to content

Commit 3513d1b

Browse files
authored
Merge pull request #12 from slashdevops/docs/custom-storage-guide
feat: Reserver interface + custom Storage example & Redis/Valkey guide
2 parents 5602e43 + 950a1da commit 3513d1b

11 files changed

Lines changed: 918 additions & 29 deletions

File tree

README.md

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,10 @@ idle for a configurable duration.
3737
- **Type-safe & generic**`Storage[K, V]` and `BucketLimiter[K]` use Go
3838
generics (Go 1.26+).
3939
- **Extensible** — implement the `Storage` interface for a custom in-process
40-
store, or the `Limiter` interface for a custom algorithm.
40+
store, or the `Limiter` interface for a custom algorithm (optionally adding
41+
`Reserver` for accurate `Retry-After`, even with a Redis/Valkey backend).
4142
- **HTTP middleware example** — with accurate `Retry-After` and `RateLimit-*`
42-
response headers.
43+
response headers, driven by the backend-agnostic `Reserver` interface.
4344

4445
## Architecture
4546

@@ -155,10 +156,12 @@ func main() {
155156
| Type / func | Role |
156157
|------------------------------------|----------------------------------------------------------------------------|
157158
| `Limiter` | Minimal interface (`Allow`, `Wait`, `Burst`). `*rate.Limiter` satisfies it. |
159+
| `Reserver` / `Reservation` | Optional capability: reserve a token and read its delay. Enables accurate `Retry-After` for any backend. |
160+
| `RateLimiter` | Default limiter: wraps `*rate.Limiter`, implements `Limiter` **and** `Reserver`. |
158161
| `Storage[K, V]` | Pluggable, concurrency-safe store for per-key limiters. |
159162
| `InMemoryStorage[K, V]` | Default `sync.Map`-backed store. |
160163
| `BucketLimiter[K]` | Manager: hands out one `Limiter` per key, handles creation and eviction. |
161-
| `NewRateLimiterFunc(limit, burst)` | Convenience factory for the common `*rate.Limiter` case. |
164+
| `NewRateLimiterFunc(limit, burst)` | Convenience factory producing `RateLimiter` values. |
162165

163166
### `limit` and `burst`
164167

@@ -186,6 +189,33 @@ if err := lim.Wait(ctx); err != nil {
186189
}
187190
```
188191

192+
### Reserve (for accurate `Retry-After`)
193+
194+
When you need the exact delay until the next token — to set a `Retry-After` or
195+
`RateLimit-Reset` header — use the optional `Reserver` capability. Feature-detect
196+
it so your code works with any limiter and degrades gracefully:
197+
198+
```go
199+
lim := manager.GetOrAdd(key)
200+
201+
if r, ok := lim.(ratelimiter.Reserver); ok {
202+
res := r.Reserve()
203+
if res.OK() && res.Delay() == 0 {
204+
// proceed now
205+
} else {
206+
res.Cancel() // return the token
207+
retryAfter := res.Delay() // tell the client exactly how long to wait
208+
}
209+
} else {
210+
_ = lim.Allow() // limiter without reservation support: no timing info
211+
}
212+
```
213+
214+
The default `RateLimiter` from `NewRateLimiterFunc` implements `Reserver`, and a
215+
custom (e.g. Redis/Valkey-backed) `Limiter` can too — so the same middleware
216+
produces accurate headers regardless of backend. See
217+
[docs/CUSTOM_STORAGE.md](docs/CUSTOM_STORAGE.md#distributed-limiting-with-redis--valkey).
218+
189219
## HTTP middleware
190220

191221
The [`examples/middleware`](examples/middleware/main.go) program limits requests
@@ -211,10 +241,12 @@ go run ./examples/key -limit 1 -burst 3
211241

212242
## Custom storage
213243

214-
Implement `Storage[K, V]` to back the manager with your own in-process store —
215-
for example a size-bounded LRU to cap memory instead of (or in addition to)
216-
time-based eviction. Implementations must be safe for concurrent use, and
217-
`LoadOrStore` must be atomic.
244+
`BucketLimiter` talks to the `Storage[K, V]` interface, never a concrete map,
245+
and you inject the implementation at construction time. `InMemoryStorage` is
246+
just the bundled **default** — implement the interface to bring your own
247+
in-process store (for example a size-bounded LRU to cap memory instead of, or in
248+
addition to, time-based eviction). Implementations must be safe for concurrent
249+
use, and `LoadOrStore` must be atomic.
218250

219251
```go
220252
type Storage[K comparable, V any] interface {
@@ -226,6 +258,19 @@ type Storage[K comparable, V any] interface {
226258
}
227259
```
228260

261+
A complete, runnable size-bounded LRU store lives in
262+
[`examples/customstorage`](examples/customstorage/main.go):
263+
264+
```bash
265+
go run ./examples/customstorage -cap 2
266+
```
267+
268+
**[docs/CUSTOM_STORAGE.md](docs/CUSTOM_STORAGE.md)** is a full guide: the method
269+
contracts, how to test atomicity, and — importantly — why a custom `Storage` is
270+
**in-process only**, plus the correct pattern for **distributed limiting with
271+
Redis / [Valkey](https://github.com/valkey-io/valkey-go)** (a datastore-backed
272+
`Limiter` wired through a `Storage` resolver, with the token-bucket Lua script).
273+
229274
## Scope: single-process only
230275

231276
Token state lives in memory inside each `*rate.Limiter`, so this library

doc.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,17 @@
2222
// // allowed
2323
// }
2424
//
25+
// Limiters are consumed through the [Limiter] interface (Allow, Wait, Burst).
26+
// A limiter may optionally also implement [Reserver] to reserve a token and
27+
// report the exact delay until it is valid; the default limiter from
28+
// [NewRateLimiterFunc] does, which lets HTTP middleware emit accurate
29+
// Retry-After headers for any backend. See the examples directory for a runnable
30+
// HTTP middleware.
31+
//
2532
// The [Storage] interface can be implemented to plug in a custom in-process
26-
// store. Note that the token-bucket state lives in memory inside each
27-
// *rate.Limiter, so this package targets single-process rate limiting.
28-
// Distributed rate limiting across multiple instances requires a different
29-
// algorithm and is out of scope.
33+
// store; see [Storage] and the customstorage example. Note that the
34+
// token-bucket state lives in memory inside each *rate.Limiter, so this package
35+
// targets single-process rate limiting. Distributed rate limiting across
36+
// multiple instances requires a different algorithm (a datastore-backed
37+
// [Limiter]) and is out of scope for the bundled types.
3038
package ratelimiter

0 commit comments

Comments
 (0)