ratelimit

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Aug 22, 2022 License: MIT Imports: 2 Imported by: 5

README

~ ratelimit ~

A simple token bucket based rate limiter.

     

go get github.com/zekroTJA/ratelimit

Intro

This package provides a verry simple, token bucket based rate limiter with the ability to return the status of the limiter on reserving a token.

Here you can read the docs of this package, generated by godoc.org.


Usage Example

In examples/webserver you can find a simple HTTP REST API design limiting accesses with this rate limit package:

Taking a look on just these two functions in webserver.go, for example:

// ...

const (
	limiterLimit = 10 * time.Second
	limiterBurst = 3
)

// ...

// checkLimit is a helper function checking
// the availability of a limiter for the connections
// address or creating it if not existing. Then,
// the availability of tokens will be checked
// to perform an action. This state will be returned
// as boolean.
func (ws *webServer) checkLimit(w http.ResponseWriter, r *http.Request) bool {
	// Getting the address of the incomming connection.
	// Because you will likely test this with a local connection,
	// the local port number will be attached and differ on every
	// request. So, we need to cut away everything behind the last
	// ":", if existent.
	addr := r.RemoteAddr
	if strings.Contains(addr, ":") {
		split := strings.Split(addr, ":")
		addr = strings.Join(split[0:len(split)-1], ":")
	}

	// Getting the limiter for the current connections address
	// or create one if not existent.
	limiter, ok := ws.limiters[addr]
	if !ok {
		limiter = ratelimit.NewLimiter(limiterLimit, limiterBurst)
		ws.limiters[addr] = limiter
	}

	// Reserve a token from the limiter.
	a, res := limiter.Reserve()

	// Attach the reservation result to the three headers
	// "X-RateLimit-Limit"
	//    - containing the absolute burst rate
	//      of the limiter,
	// "X-RateLimit-Remaining"
	//    - the number of remaining tickets after
	//      the request
	// "X-RateLimit-Reset"
	//    - the UnixNano timestamp until a new token
	//      will be generated (only if remaining == 0)
	w.Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", res.Burst))
	w.Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", res.Remaining))
	w.Header().Set("X-RateLimit-Reset", fmt.Sprintf("%d", res.Reset.UnixNano()))

	// Return the succeed status of
	// the token request
	return a
}

// handlerTest is the handler for /api/test root
func (ws *webServer) handlerTest(w http.ResponseWriter, r *http.Request) {
	// Check and consume a token from the limiter,
	// if available. If succeed, return status 200,
	// else status 429.
	if ws.checkLimit(w, r) {
		w.WriteHeader(http.StatusOK)
	} else {
		w.WriteHeader(http.StatusTooManyRequests)
	}
}

Our limiter has a total token volume (= burst) of 3 tokens and a limit of 1 token per 10 seconds, which means, that every 10 seconds a new token will be added to the token bucket (until the bucket has is "full").

So, if you send 4 HTTP GET requests in a time span of under 10 Seconds to /api/test, you will get following result:

///////////////////////////
// REQUEST #1

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Ratelimit-Limit: 3
< X-Ratelimit-Remaining: 2
< X-Ratelimit-Reset: 0
< Date: Thu, 21 Mar 2019 09:03:11 GMT
< Content-Length: 0
<


///////////////////////////
// REQUEST #2

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Ratelimit-Limit: 3
< X-Ratelimit-Remaining: 1
< X-Ratelimit-Reset: 0
< Date: Thu, 21 Mar 2019 09:03:12 GMT
< Content-Length: 0
<


///////////////////////////
// REQUEST #3

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Ratelimit-Limit: 3
< X-Ratelimit-Remaining: 0
< X-Ratelimit-Reset: 1553159004253249800
< Date: Thu, 21 Mar 2019 09:03:14 GMT
< Content-Length: 0
<


///////////////////////////
// REQUEST #4

*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/test HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 429 Too Many Requests
< X-Ratelimit-Limit: 3
< X-Ratelimit-Remaining: 0
< X-Ratelimit-Reset: 1553159004253249800
< Date: Thu, 21 Mar 2019 09:03:15 GMT
< Content-Length: 0
<

As you can see, we are receiving status code 200 OK exactly 3 times. Also, you can see that the ammount of remaining tokens is returned in a X-Ratelimit-Remaining header and also the time until a new token will be generated in the X-Ratelimit-Reset header (as UnixNano timestamp) when no tokens are remaining.

This concept ofreturning these information as headers were inspired by the design of the REST API of discordapp.com.


Copyright (c) 2019 zekro Development (Ringo Hoffmann).
Covered by MIT licence.

Documentation

Overview

Package ratelimit provides a verry simple, token bucket based request rate limiter.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Limiter

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

A Limiter controls how frequently accesses should be allowed to happen. It implements the principle of the token bucket, which defines a bucket with an exact size of tokens. Also, a rate is defined after exactly 1 token will be added to the buckets volume, if the bucket is not "full" (nVolume == nSize). The amount of tokens in the bucket are defined as ability to perform an action, which then reduces the volume of the bucket by n tickets.

func NewLimiter

func NewLimiter(l time.Duration, b int) *Limiter

NewLimiter returns a new instance of Limiter with a burst rate of b and a limit time of l until a new token will be generated.

func (*Limiter) Allow

func (l *Limiter) Allow() bool

Allow is shorthand for AllowN(1).

func (*Limiter) AllowN

func (l *Limiter) AllowN(n int) bool

AllowN is shorthand for reserveN(n) but only returning a boolean which exposes the succeed of the reservation.

func (*Limiter) Burst

func (l *Limiter) Burst() int

Burst returns the defined burst value.

This function does not consume tokens.

func (*Limiter) Limit

func (l *Limiter) Limit() time.Duration

Limit returns the defined limit duration after which a new token will be generated.

This function does not consume tokens.

func (*Limiter) Reserve

func (l *Limiter) Reserve() (bool, Reservation)

Reserve is shorthand for ReserveN(1).

func (*Limiter) ReserveN

func (l *Limiter) ReserveN(n int) (bool, Reservation)

ReserveN checks if an amount of n tickets are currently available. If this is the case, true will be returned with a Reservation object as status information of the Limiter and n tokens will be consumed. If there are not enough tokens available to satisfy the reservation, false will be returned with a Reservation object containing the Limiters status containing the time until next token generation.

func (*Limiter) Reset

func (l *Limiter) Reset()

Reset sets the state of the limiter to the initial state with b tokens available and last set to 0.

func (*Limiter) SetBurst

func (l *Limiter) SetBurst(newB int)

SetBurst sets a new value for the rate limiters burst value withour resetting the state of the limiter.

func (*Limiter) SetLimit

func (l *Limiter) SetLimit(newL time.Duration)

SetLimit sets a new value for the rate limiters limit duration withour resetting the state of the limiter.

func (*Limiter) Tokens

func (l *Limiter) Tokens() int

Tokens returns the current available tokens. This value is unequal to the actual value of tokens, because this value is only refreshed after token consumption. So the returned value is the actial value of tokens plus the calculated amount of tokens which are virtually generated after last consumption.

This function does not consume tokens.

type Reservation

type Reservation struct {
	Burst     int       `json:"burst"`
	Remaining int       `json:"remaining"`
	Reset     ResetTime `json:"reset"`
}

Reservation contains the pre-defined burst rate of the Limiter, the amount of remaining tickets and the time until a new token will be added to the bucket if Remaining == 0. Else, reset will be time.Time{} (0001-01-01 00:00:00 +0000 UTC).

This struct contains JSON tags, so it can be easily parsed to JSON.

type ResetTime

type ResetTime struct {
	time.Time
	// contains filtered or unexported fields
}

ResetTime inherits from time.Time and contains an extra-vaule 'isNil', which identifies if Time == time.Time{}. Also, the functions Unix, UnixNano and Format are overwritten to take notice of this extra-value.

func (*ResetTime) Format

func (r *ResetTime) Format(layout string, def ...string) string

Format overwrites time.Time#Format() so it will return the value of def (if not defined it will return "") if IsNil is true. Else, it will behave like default.

func (*ResetTime) IsNil

func (r *ResetTime) IsNil() bool

IsNil returns the boolean value if the inner Time value is equal an empty time object (time.Time{}).

func (*ResetTime) Unix

func (r *ResetTime) Unix() int64

Unix overwrites time.Time#Unix() so it will return 0 if IsNil is true. Else, it will behave like default.

func (*ResetTime) UnixNano

func (r *ResetTime) UnixNano() int64

UnixNano overwrites time.Time#UnixNano() so it will return 0 if IsNil is true. Else, it will behave like default.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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