gandalf

package module
v0.0.0-...-cda3f7b Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 9, 2025 License: MIT Imports: 8 Imported by: 0

README

The Go Gopher as Gandalf, facing down a fiery Rust-colored crab monster.

GANDALF: Golang Application Network Defense And Limit Filter

Go Reference Go Report Card Coverage Status CI

Used reliably in production since 2022 at https://feedbackfrog.io

With GANDALF, You Shall Not Pass… the Rate Limit

In the deepest forges of application development, where the fires of high traffic burn brightest, GANDALF stands as a formidable guardian. This rate limiting library for Go applications, it provides a simple yet powerful API to shield your services from overwhelming requests. Like a wizard's staff, it offers protection and control, ensuring that only the worthy may pass through your system's gates. A true upgrade to your arsenal.

Features

  • Ultra-fast performance: Up to 38,000+ requests/second for sequential operations
  • Reliable concurrent throughput: 3,600+ requests/second
  • Persistent storage using BadgerDB (significantly faster than BoltDB equivalents for writes)
  • Support for different time units (millisecond, second, minute, hour, day, month)
  • Customizable rate limits per API key
  • Thread-safe operations with intelligent retry mechanisms
  • Easy integration with existing applications
  • Advanced transaction conflict resolution with exponential backoff and jitter

Performance Highlights

Our comprehensive benchmarks demonstrate exceptional performance with BadgerDB:

  • Sequential Operations: 38,327 requests/second
  • Concurrent Operations: 3,604 requests/second
  • Mixed Workload: 38,988 requests/second (multiple rate limit tiers)
  • Millisecond Precision: 100+ requests per millisecond with deterministic resets
  • Persistence: BadgerDB with 10x+ write performance improvement over BoltDB

Installation

go get github.com/feedback-frog/gandalf

Basic Usage

package main

import (
    "fmt"
    "log"

    "github.com/feedback-frog/gandalf"
)

func main() {
    // Create a static provider (1000 requests per second for all keys)
    provider := &gandalf.StaticProvider{
        TimeInterval: 1000,
        TimeUnit:     "second",
    }

    // Create a rate limiter
    limiter, err := gandalf.NewRateLimiterWithProvider("rate_limits.db", provider)
    if err != nil {
        log.Fatal(err)
    }
    defer limiter.Close()

    // Use rate limiting
    result, funcErr, rateLimitErr := limiter.Limit("user-123", func() (any, error) {
        return "API call successful", nil
    })

    if rateLimitErr != nil {
        if rateLimitErr == gandalf.ErrRateLimitExceeded {
            fmt.Println("Rate limit exceeded!")
            return
        }
        log.Fatal(rateLimitErr)
    }

    if funcErr != nil {
        log.Fatal(funcErr)
    }

    fmt.Printf("Result: %v\n", result)
}

Gin (or any mux) HTTP Middleware

Here's how to integrate GANDALF as middleware:

package main

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/feedback-frog/gandalf"
)

// RateLimitMiddleware creates a Gin middleware that uses GANDALF for rate limiting
func RateLimitMiddleware(limiter *gandalf.RateLimiter) gin.HandlerFunc {
    return func(c *gin.Context) {
        // Extract client identifier (IP address, API key, user ID, service ID, etc.)
        clientID := getClientID(c)

        // Use GANDALF to check rate limit
        _, funcErr, rateLimitErr := limiter.Limit(clientID, func() (any, error) {
            // This function will only execute if rate limit allows
            // We return nil since we just want to check/consume the rate limit
            return nil, nil
        })

        if rateLimitErr != nil {
            if rateLimitErr == gandalf.ErrRateLimitExceeded {
                c.JSON(http.StatusTooManyRequests, gin.H{
                    "error": "Rate limit exceeded",
                    "message": "Too many requests, please try again later",
                })
                c.Abort()
                return
            }
            // Handle other rate limiting errors
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Rate limiting service error",
            })
            c.Abort()
            return
        }

        if funcErr != nil {
            c.JSON(http.StatusInternalServerError, gin.H{
                "error": "Rate limiting function error",
            })
            c.Abort()
            return
        }

        // Rate limit check passed, continue to next middleware/handler
        c.Next()
    }
}

func main() {
    // Create a static provider (100 requests per minute for all clients)
    provider := &gandalf.StaticProvider{
        TimeInterval: 100,
        TimeUnit:     "minute",
    }

    // Create rate limiter with BadgerDB
    limiter, err := gandalf.NewRateLimiterWithProvider("api_rate_limits.db", provider)
    if err != nil {
        panic(err)
    }
    defer limiter.Close()

    // Create Gin router
    r := gin.Default()

    // Apply rate limiting middleware globally
    r.Use(RateLimitMiddleware(limiter))

    // Define routes
    r.GET("/api/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "healthy",
            "message": "API is running with GANDALF rate limiting",
        })
    })

    r.POST("/api/data", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Data processed successfully",
            "client_id": getClientID(c),
        })
    })

    // Start server
    r.Run(":8080")
}
Per-Route Rate Limiting

For different rate limits on different endpoints:


func main() {
    // Create different providers for different endpoints
    strictProvider := &gandalf.StaticProvider{
        TimeInterval: 10,
        TimeUnit:     "minute",
    }

    relaxedProvider := &gandalf.StaticProvider{
        TimeInterval: 1000,
        TimeUnit:     "hour",
    }

    strictLimiter, _ := gandalf.NewRateLimiterWithProvider("strict_limits.db", strictProvider)
    relaxedLimiter, _ := gandalf.NewRateLimiterWithProvider("relaxed_limits.db", relaxedProvider)
    defer strictLimiter.Close()
    defer relaxedLimiter.Close()

    r := gin.Default()

    // Strict rate limiting for sensitive endpoints
    sensitive := r.Group("/api/admin")
    sensitive.Use(RateLimitMiddleware(strictLimiter)) // RateLimitMiddleware defined earlier in the README
    {
        sensitive.GET("/users", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "Admin endpoint"})
        })
    }

    // Relaxed rate limiting for public endpoints
    public := r.Group("/api/public")
    public.Use(RateLimitMiddleware(relaxedLimiter))
    {
        public.GET("/data", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"data": "public data"})
        })
    }

    r.Run(":8080")

Database-Backed Rate Limiting

package main

import (
    "fmt"
    "log"

    "github.com/feedback-frog/gandalf"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

// ServiceLimits represents your application's service limits model
type ServiceLimits struct {
    ID            string `gorm:"primaryKey"`
    Key           string `gorm:"uniqueIndex"`
    ProjectID     string
    RateLimit     int
    RateLimitUnit string
}

func main() {
    // using GORM with SQLite as a simple example to copy/paste.
    // Gandalf is data store agnostic
    db, err := gorm.Open(sqlite.Open("api_keys.db"), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // Auto-migrate the API key table
    db.AutoMigrate(&ServiceLimits{})

    // Create some API keys
    db.Create(&ServiceLimits{
        ID:            "api-key-1",
        Key:           "sk-test-123",
        ProjectID:     "project-1",
        RateLimit:     1000, // High throughput: 1000 requests per day
        RateLimitUnit: "day",
    })

    // Create a rate limit fetcher function that gets data from your database
    rateLimitFetcher := func(key string) (int, string, error) {
        var apiKey ServiceLimits
        result := db.Where("key = ?", key).First(&apiKey)
        if result.Error != nil {
            if result.Error == gorm.ErrRecordNotFound {
                return 0, "", fmt.Errorf("unknown key")
            }
            return 0, "", result.Error
        }
        return apiKey.RateLimit, apiKey.RateLimitUnit, nil
    }

    // Create provider with your custom fetcher
    provider := gandalf.NewDataProvider(rateLimitFetcher)

    // Create rate limiter with BadgerDB for optimal performance
    limiter, err := gandalf.NewRateLimiterWithProvider("rate_limits.db", provider)
    if err != nil {
        log.Fatal(err)
    }
    defer limiter.Close()

    // Use with actual Service key
    result, funcErr, rateLimitErr := limiter.Limit("sk-test-123", func() (any, error) {
        return "Database-backed rate limiting with BadgerDB!", nil
    })

    if rateLimitErr == gandalf.ErrRateLimitExceeded {
        log.Println("Rate limit exceeded!")
        return
    }

    if funcErr != nil {
        log.Fatal(funcErr)
    }

    fmt.Printf("Result: %v\n", result)
}

API Reference

RateLimiter

The main type that provides rate limiting functionality with BadgerDB backend.

Methods
  • NewRateLimiter(dbPath string) (*RateLimiter, error)

    • Creates a new rate limiter with the specified BadgerDB path
    • Returns a RateLimiter instance and any error that occurred
  • NewRateLimiterWithProvider(dbPath string, provider RateLimitDataProvider) (*RateLimiter, error)

    • Creates a new rate limiter with a custom data provider
    • Returns a RateLimiter instance and any error that occurred
  • Limit(keyId string, fn func() (any, error)) (any, error, error)

    • Executes a function with rate limiting
    • Returns (functionResult, functionError, rateLimitError)
    • If rate limit is exceeded, returns (nil, nil, ErrRateLimitExceeded)
    • If rate limit allows execution, consumes one request and returns function results
    • Includes intelligent retry logic with exponential backoff and jitter for BadgerDB transaction conflicts
  • Peek(keyId string) (requestsLeft int, limitUnit string, err error)

    • Returns the current limit and limit unit for a given key without consuming a request
    • Returns the number of requests left, the limit unit, and any error that occurred
  • Purge(keyId string) error

    • Resets the current limits for a key, or optionally all keys if keyId is empty
    • Returns any error that occurred
  • Close() error

    • Closes the underlying BadgerDB database
    • Returns any error that occurred
Providers
StaticProvider

A provider that returns a static rate limit for all keys.

provider := &gandalf.StaticProvider{
    TimeInterval: 1000, // 1000 requests
    TimeUnit:     "second", // per second
}
DataProvider

A provider that gets rate limit data from any source using a fetcher function.

// Create a rate limit fetcher function
rateLimitFetcher := func(key string) (int, string, error) {
    // Get rate limit data from your source (database, API, etc.)
    // Return (rateLimit, rateLimitUnit, error)
    return 1000, "second", nil
}

// Create provider with your custom fetcher
provider := gandalf.NewDataProvider(rateLimitFetcher)

The fetcher function should:

  • Return the rate limit (number of requests allowed)
  • Return the rate limit unit ("millisecond", "second", "minute", "hour", "day", "month")
  • Return an error if the key is not found or any other error occurs
Rate Limit Units

The rate limiter supports the following time units:

  • millisecond: Resets every millisecond (up to 100+ requests/ms)
  • second: Resets every second (up to 38,000+ requests/sec)
  • minute: Resets every minute
  • hour: Resets every hour
  • day: Resets every day
  • month: Resets every month

Performance Characteristics

Throughput Benchmarks

Our comprehensive single-key testing shows real-world performance:

Scenario Throughput Success Rate Description
Sequential Operations 38,327 req/sec 100% Single-threaded, single key
Concurrent Operations 3,604 req/sec 99.8% Single key, concurrent requests
Mixed Workload 38,988 req/sec 100% Multiple rate limit tiers, distributed keys
Millisecond Precision 100+ req/ms 100% Deterministic millisecond-based rate limiting
Retry Logic Performance

Gandalf's intelligent retry mechanism dramatically improves reliability, much like how the Grey Wizard persevered against the Balrog and returned stronger as Gandalf the White:

  • 10 retry attempts with exponential backoff (0.5ms → 256ms)
  • Jitter (±25%) to prevent thundering herd problems
  • Automatic transaction conflict resolution for BadgerDB
Real-World Usage Patterns

For optimal performance:

  1. Sequential workloads: Achieve 38k+ req/sec with 100% success rate
  2. Concurrent workloads: Use multiple keys to distribute load
  3. High-throughput scenarios: Leverage BadgerDB's LSM tree structure
  4. Production deployments: Monitor transaction conflict rates

Error Handling

The rate limiter provides clear error handling for various scenarios:

result, funcErr, rateLimitErr := limiter.Limit("user-123", func() (any, error) {
    return "API call", nil
})

if rateLimitErr == gandalf.ErrRateLimitExceeded {
    fmt.Println("Rate limit exceeded!")
    return
}

if funcErr != nil {
    log.Printf("Function error: %v\n", funcErr)
    return
}

fmt.Printf("Result: %v\n", result)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under the MIT License.

Documentation

Overview

Package gandalf provides a simple rate limiting library for Go applications. It provides an easy-to-use API for rate limiting operations with persistence via BadgerDB.

Index

Constants

This section is empty.

Variables

View Source
var ErrDatabaseError = errors.New("database error")

ErrDatabaseError is returned for other database-related errors

View Source
var ErrKeyNotSet = errors.New("key not set")

ErrKeyNotSet is returned when attempting to peek at a key that has not been set

View Source
var (
	ErrRateLimitExceeded = errors.New("rate limit exceeded")
)
View Source
var ErrTransactionConflict = errors.New("database transaction conflict after retries exhausted")

ErrTransactionConflict is returned when a database transaction conflict occurs after all retries are exhausted

Functions

func GetDurationForUnit

func GetDurationForUnit(unit string) time.Duration

GetDurationForUnit returns the duration for a given rate limit unit

Types

type DataProvider

type DataProvider struct {
	FetchRateLimit RateLimitFetcher
}

DataProvider implements RateLimitDataProvider for database-backed rate limits

func NewDataProvider

func NewDataProvider(fetcher RateLimitFetcher) *DataProvider

NewDataProvider creates a new DataProvider with the given rate limit fetcher function

func (DataProvider) GetRateLimitData

func (d DataProvider) GetRateLimitData(key string) (RateLimitData, error)

type RateLimitData

type RateLimitData struct {
	RateLimit     int       `json:"rate_limit"`      // Number of requests allowed
	RateLimitUnit string    `json:"rate_limit_unit"` // "millisecond", "second", "minute", "hour", "day"
	ResetTime     time.Time `json:"reset_time"`      // When the current window resets
	LastReset     time.Time `json:"last_reset"`      // When the last reset occurred
	RequestsLeft  int       `json:"requests_left"`   // Remaining requests in current window
}

RateLimitData represents the rate limit information for a service

type RateLimitDataProvider

type RateLimitDataProvider interface {
	GetRateLimitData(keyId string) (RateLimitData, error)
}

RateLimitDataProvider is an interface for getting rate limit data

type RateLimitFetcher

type RateLimitFetcher func(key string) (int, string, error)

RateLimitFetcher is a function type that fetches rate limit data for a given key

type RateLimiter

type RateLimiter struct {
	// contains filtered or unexported fields
}

RateLimiter provides rate limiting functionality

func NewRateLimiter

func NewRateLimiter(dbPath string) (*RateLimiter, error)

NewRateLimiter creates a new rate limiter with the specified BadgerDB path

func NewRateLimiterWithProvider

func NewRateLimiterWithProvider(dbPath string, provider RateLimitDataProvider) (*RateLimiter, error)

NewRateLimiterWithProvider creates a new rate limiter with a custom data provider

func NewRateLimiterWithProviderAndTime

func NewRateLimiterWithProviderAndTime(dbPath string, provider RateLimitDataProvider, timeProvider TimeProvider) (*RateLimiter, error)

NewRateLimiterWithProviderAndTime creates a new rate limiter with a custom data provider and time provider This is primarily used for testing

func (*RateLimiter) Close

func (r *RateLimiter) Close() error

Close closes the underlying BadgerDB database

func (*RateLimiter) Limit

func (r *RateLimiter) Limit(keyId string, fn func() (any, error)) (any, error, error)

Limit executes a function with rate limiting and returns (functionResult, functionError, rateLimitError) If rate limit is exceeded, returns (nil, nil, ErrRateLimitExceeded) If rate limit allows execution, consumes one request and returns function results

func (*RateLimiter) Peek

func (r *RateLimiter) Peek(keyId string) (requestsLeft int, limitUnit string, err error)

Peek returns the current limit and limit unit for a given key without consuming a request. If the key is not set, it returns 0, "", and ErrKeyNotSet.

func (*RateLimiter) Purge

func (r *RateLimiter) Purge(keyId string) error

Purge resets the current limits for a key, or optionally all keys if keyId is empty

type RealTimeProvider

type RealTimeProvider struct{}

RealTimeProvider provides real time

func (*RealTimeProvider) Now

func (r *RealTimeProvider) Now() time.Time

type StaticProvider

type StaticProvider struct {
	TimeInterval int
	TimeUnit     string
}

StaticProvider implements RateLimitDataProvider for static rate limits Exported fields for struct literal usage

func (StaticProvider) GetRateLimitData

func (s StaticProvider) GetRateLimitData(key string) (RateLimitData, error)

type TimeProvider

type TimeProvider interface {
	Now() time.Time
}

TimeProvider interface for getting current time (allows mocking in tests)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL