Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,12 @@ func main() {
// - 1024 buckets (automatically rounded to nearest power of 2 if not already a power of 2)
// - 10 tokens burst capacity
// - 100 tokens per second refill rate
// - Rotate hash seeds every 30 seconds
limiter, err := rate.NewRotatingTokenBucketRateLimiter(
1024, // numBuckets
10, // burstCapacity
100, // refillRate
time.Second, // refillRateUnit
30*time.Second, // rotationRate
// - Rotation interval automatically calculated: (10/100)*5 = 0.5 seconds
limiter, err := rate.NewRotatingTokenBucketLimiter(
1024, // numBuckets
10, // burstCapacity
100, // refillRate
time.Second, // refillRateUnit
)
if err != nil {
panic(err)
Expand All @@ -182,7 +181,7 @@ func main() {
}
```

[Go Playground](https://go.dev/play/p/fw3z0BzorV3)
[Go Playground](https://go.dev/play/p/6UJN8B8F3um)

## Detailed Usage

Expand Down Expand Up @@ -368,15 +367,14 @@ rateMin ────────────────────────

### RotatingTokenBucketRateLimiter

The Rotating Token Bucket Rate Limiter addresses a fundamental limitation of hash-based rate limiters: hash collisions between different IDs can cause unfair rate limiting. This limiter maintains two TokenBucketLimiters with different hash seeds and periodically rotates between them to minimize collision impact.
The Rotating Token Bucket Rate Limiter addresses a fundamental limitation of hash-based rate limiters: hash collisions between different IDs can cause unfair rate limiting. This limiter maintains two TokenBucketLimiters with different hash seeds and automatically rotates between them to minimize collision impact while ensuring correctness.

```go
limiter, err := rate.NewRotatingTokenBucketRateLimiter(
limiter, err := rate.NewRotatingTokenBucketLimiter(
numBuckets, // Number of buckets (automatically rounded to nearest power of 2 if not already a power of 2)
burstCapacity, // Maximum tokens per bucket
refillRate, // Rate at which tokens are refilled
refillRateUnit, // Time unit for refill rate
rotationRate, // How often to rotate hash seeds
)
```

Expand All @@ -386,15 +384,25 @@ limiter, err := rate.NewRotatingTokenBucketRateLimiter(
- `burstCapacity`: Maximum number of tokens that can be consumed at once
- `refillRate`: Rate at which tokens are refilled
- `refillRateUnit`: Time unit for refill rate calculations (e.g., time.Second)
- `rotationRate`: How often to rotate the bucket pairs and generate new hash seeds (must be a positive duration)

#### Automatic Rotation Calculation:

The rotation interval is automatically calculated to ensure 99.99% statistical convergence of all token buckets to steady state before rotation occurs. This eliminates state inconsistency issues and guarantees correctness:

```
rotationInterval = (burstCapacity / refillRate * refillRateUnit) * 5.0
```

For example:
- `burstCapacity=100, refillRate=250/second` → rotation every 2 seconds
- `burstCapacity=10, refillRate=1/second` → rotation every 50 seconds

#### Input Validation:

The constructor performs validation on all parameters and returns descriptive errors:

- `refillRate` must be a positive, finite number (not NaN, infinity, zero, or negative)
- `refillRateUnit` must represent a positive duration
- `rotationRate` must represent a positive duration
- The product of `refillRate` and `refillRateUnit` must not overflow when converted to nanoseconds

#### Collision-Resistant Algorithm Explained
Expand Down
60 changes: 43 additions & 17 deletions rotating.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package rate

import (
"fmt"
"hash/maphash"
"sync/atomic"
"time"
Expand Down Expand Up @@ -45,7 +44,7 @@ type RotatingTokenBucketRateLimiter struct {
nanosPerRotation int64 // Rotation interval in nanoseconds
}

// NewRotatingTokenBucketRateLimiter creates a new collision-resistant
// NewRotatingTokenBucketLimiter creates a new collision-resistant
// token bucket rate limiter with the specified parameters:
//
// - numBuckets: number of token buckets per limiter (automatically
Expand All @@ -57,12 +56,18 @@ type RotatingTokenBucketRateLimiter struct {
// and finite)
// - refillRateUnit: time unit for refill rate calculations (e.g.,
// time.Second, must be a positive duration)
// - rotationRate: how often to rotate the bucket pairs and generate
// new hash seeds (must be a positive duration)
//
// The rotation interval is automatically calculated to ensure 99.99%
// statistical convergence of all token buckets to steady state before
// rotation occurs. This guarantees correctness by eliminating state
// inconsistency issues when hash mappings change during rotation.
//
// The calculation is: rotationInterval = (burstCapacity/refillRate *
// refillRateUnit) * 5.0
//
// The limiter creates two identical TokenBucketLimiters with
// different hash seeds. It rotates between them every rotationRate
// duration to minimize the impact of hash collisions. When rotation
// different hash seeds and rotates between them at the calculated
// interval to minimize the impact of hash collisions. When rotation
// occurs:
//
// 1. The current "ignored" limiter becomes the new "checked" limiter
Expand All @@ -71,26 +76,24 @@ type RotatingTokenBucketRateLimiter struct {
// 3. Both limiters are consulted on every operation, but only the
// "checked" result determines the rate limiting decision
//
// This design ensures that any hash collisions between different IDs
// will only persist for at most one rotation period, providing better
// fairness and accuracy than a single TokenBucketLimiter.
// This design ensures that:
//
// - Hash collisions between different IDs only persist for one
// rotation period
// - State inconsistency is eliminated through steady-state
// convergence
// - Better fairness and accuracy than a single TokenBucketLimiter
//
// Input validation follows the same rules as NewTokenBucketLimiter,
// with an additional requirement that rotationRate must be positive.
// Input validation follows the same rules as NewTokenBucketLimiter.
//
// Returns a new RotatingTokenBucketRateLimiter instance and any error
// that occurred during creation.
func NewRotatingTokenBucketRateLimiter(
func NewRotatingTokenBucketLimiter(
numBuckets uint,
burstCapacity uint8,
refillRate float64,
refillRateUnit time.Duration,
rotationRate time.Duration,
) (*RotatingTokenBucketRateLimiter, error) {
if rotationRate <= 0 {
return nil, fmt.Errorf("rotationRate must represent a positive duration")
}

checked, err := NewTokenBucketLimiter(
numBuckets,
burstCapacity,
Expand All @@ -111,6 +114,14 @@ func NewRotatingTokenBucketRateLimiter(
refillRateUnit,
)

// Calculate the rotation interval that ensures 99.99% statistical
// convergence of all token buckets to steady state before rotation
// occurs. This guarantees correctness by eliminating state
// inconsistency issues when hash mappings change during rotation.
refillTime := time.Duration(float64(burstCapacity) / refillRate * float64(refillRateUnit))
safetyFactor := 5.0
rotationRate := time.Duration(float64(refillTime) * safetyFactor)

limiter := &RotatingTokenBucketRateLimiter{
nanosPerRotation: rotationRate.Nanoseconds(),
}
Expand Down Expand Up @@ -226,3 +237,18 @@ func (r *RotatingTokenBucketRateLimiter) TakeToken(id []byte) bool {
pair.ignored.takeTokenWithNow(id, now)
return pair.checked.takeTokenWithNow(id, now)
}

// RotationInterval returns the automatically calculated rotation
// interval duration. This interval ensures 99.99% statistical
// convergence of all token buckets to steady state before rotation
// occurs, guaranteeing correctness by eliminating state inconsistency
// issues.
//
// The rotation interval is calculated as: (burstCapacity / refillRate
// * refillRateUnit) * 5.0
//
// This method is thread-safe and can be called concurrently from
// multiple goroutines.
func (r *RotatingTokenBucketRateLimiter) RotationInterval() time.Duration {
return time.Duration(r.nanosPerRotation)
}
Loading