diff --git a/README.md b/README.md index 4573dab..cb258be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # ratelimiter + +![Tests](https://github.com/Narasimha1997/ratelimiter/actions/workflows/test.yml/badge.svg) + A generic concurrent rate limiter library for Golang based on Sliding-window rate limitng algorithm. The implementation of rate-limiter algorithm is based on Scalable Distributed Rate Limiter algorithm used in Kong API gateway. Read [this blog](https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm/) for more details. @@ -20,27 +23,16 @@ There are two types of rate-limiters used. 1. **Generic rate-limiter**: ```go /* creates an instance of DefaultLimiter and returns it's pointer. - Parameters: - limit: The number of tasks to be allowd - size: duration - */ - func NewDefaultLimiter(limit uint64, size time.Duration) *DefaultLimiter - -``` - -2. **On-demand rate-limiter** -```go - /* creates an instance of DefaultLimiter and returns it's pointer. - Parameters: + Parameters: limit: The number of tasks to be allowd size: duration */ - func NewSyncLimiter(limit uint64, size time.Duration) *SyncLimiter + func NewDefaultLimiter(limit uint64, size time.Duration) *DefaultLimiter /* Kill the limiter, returns error if the limiter has been killed already. */ - func (s *SyncLimiter) Kill() error + func (s *DefaultLimiter) Kill() error /* Makes decison whether n tasks can be allowed or not. @@ -51,7 +43,22 @@ There are two types of rate-limiters used. if limiter is inactive (or it is killed), returns an error the boolean flag is either true - i.e n tasks can be allowed or false otherwise. */ - func (s *SyncLimiter) ShouldAllow(n uint64) (bool, error) + func (s *DefaultLimiter) ShouldAllow(n uint64) (bool, error) + + /* + Kill the limiter, returns error if the limiter has been killed already. + */ + func (s *DefaultLimiter) Kill() error +``` + +2. **On-demand rate-limiter** +```go + /* creates an instance of SyncLimiter and returns it's pointer. + Parameters: + limit: The number of tasks to be allowd + size: duration + */ + func NewSyncLimiter(limit uint64, size time.Duration) *SyncLimiter /* Kill the limiter, returns error if the limiter has been killed already. @@ -68,6 +75,11 @@ There are two types of rate-limiters used. the boolean flag is either true - i.e n tasks can be allowed or false otherwise. */ func (s *SyncLimiter) ShouldAllow(n uint64) (bool, error) + + /* + Kill the limiter, returns error if the limiter has been killed already. + */ + func (s *SyncLimiter) Kill() error ``` 3. **Attribute based Rate Limiter** diff --git a/attribute_limiter.go b/attribute_limiter.go index b502e52..d195d40 100644 --- a/attribute_limiter.go +++ b/attribute_limiter.go @@ -6,14 +6,24 @@ import ( "time" ) +// AttributeMap is a custom map type of string key and Limiter instance as value type AttributeMap map[string]Limiter +// AttributeBasedLimiter is an instance that can manage multiple rate limiter instances +// with different configutations. type AttributeBasedLimiter struct { attributeMap AttributeMap m sync.Mutex syncMode bool } +// Check if AttributeBasedLimiter has a limiter for the key. +// +// Parameters: +// +// 1. key: a unique key string, example: IP address, token, uuid etc +// +// Returns a boolean flag, if true, the key is already present, false otherwise. func (a *AttributeBasedLimiter) HasKey(key string) bool { a.m.Lock() _, ok := a.attributeMap[key] @@ -21,6 +31,17 @@ func (a *AttributeBasedLimiter) HasKey(key string) bool { return ok } +// Create a new key-limiter assiociation. +// +// Parameters: +// +// 1. key: a unique key string, example: IP address, token, uuid etc +// +// 2. limit: The number of tasks to be allowd +// +// 3. size: duration +// +// Returns error if the key already exists. func (a *AttributeBasedLimiter) CreateNewKey(key string, limit uint64, size time.Duration) error { a.m.Lock() defer a.m.Unlock() @@ -40,6 +61,18 @@ func (a *AttributeBasedLimiter) CreateNewKey(key string, limit uint64, size time return nil } +// Makes decison whether n tasks can be allowed or not. +// +// Parameters: +// +// key: a unique key string, example: IP address, token, uuid etc +// +// n: number of tasks to be processed, set this as 1 for a single task. +// (Example: An HTTP request) +// +// Returns (bool, error). +// (false, error) when limiter is inactive (or it is killed) or key is not present. +// (true/false, nil) if key exists and n tasks can be allowed or not. func (a *AttributeBasedLimiter) ShouldAllow(key string, n uint64) (bool, error) { a.m.Lock() defer a.m.Unlock() @@ -52,6 +85,13 @@ func (a *AttributeBasedLimiter) ShouldAllow(key string, n uint64) (bool, error) return false, fmt.Errorf("key %s not found", key) } +// Remove the key and kill its underlying limiter. +// +// Parameters: +// +// 1.key: a unique key string, example: IP address, token, uuid etc +// +// Returns an error if the key is not present. func (a *AttributeBasedLimiter) DeleteKey(key string) error { a.m.Lock() @@ -69,6 +109,12 @@ func (a *AttributeBasedLimiter) DeleteKey(key string) error { return fmt.Errorf("key %s not found", key) } +// Creates an instance of AttributeBasedLimiter and returns it's pointer. +// +// Parameters: +// +// 1. backgroundSliding: if set to true, DefaultLimiter will be used as an underlying limiter, +// else, SyncLimiter will be used. func NewAttributeBasedLimiter(backgroundSliding bool) *AttributeBasedLimiter { return &AttributeBasedLimiter{ attributeMap: make(AttributeMap), diff --git a/limiter.go b/limiter.go index bd50503..24c93b2 100644 --- a/limiter.go +++ b/limiter.go @@ -7,13 +7,13 @@ import ( "time" ) +// Limiter is an interface that is implemented by DefaultLimiter and SyncLimiter type Limiter interface { - // limiter interface is simple, - // Kill() and ShouldAllow() these are the functions you need. Kill() error ShouldAllow(n uint64) (bool, error) } +// DefaultLimiter maintains all the structures used for rate limting using a background goroutine. type DefaultLimiter struct { previous *Window current *Window @@ -25,6 +25,14 @@ type DefaultLimiter struct { cancelFn func() } +// Makes decison whether n tasks can be allowed or not. +// +// Parameters: +// +// 1. n: number of tasks to be processed, set this as 1 for a single task. (Example: An HTTP request) +// +// Returns (bool, error). (false, error) if limiter is inactive (or it is killed). Otherwise, +// (true/false, nil) depending on whether n tasks can be allowed or not. func (l *DefaultLimiter) ShouldAllow(n uint64) (bool, error) { l.lock.Lock() @@ -67,6 +75,7 @@ func (l *DefaultLimiter) progressiveWindowSlider() { } } +// Kill the limiter, returns error if the limiter has been killed already. func (l *DefaultLimiter) Kill() error { l.lock.Lock() @@ -81,6 +90,13 @@ func (l *DefaultLimiter) Kill() error { return nil } +// Creates an instance of DefaultLimiter and returns it's pointer. +// +// Parameters: +// +// 1. limit: The number of tasks to be allowd +// +// 2. size: duration func NewDefaultLimiter(limit uint64, size time.Duration) *DefaultLimiter { previous := NewWindow(0, time.Unix(0, 0)) current := NewWindow(0, time.Unix(0, 0)) @@ -102,6 +118,7 @@ func NewDefaultLimiter(limit uint64, size time.Duration) *DefaultLimiter { return limiter } +// SyncLimiter maintains all the structures used for rate limting on demand. type SyncLimiter struct { previous *Window current *Window @@ -118,6 +135,14 @@ func (s *SyncLimiter) getNSlidesSince(now time.Time) (time.Duration, time.Time) return timeSinceStart / s.size, sizeAlignedTime } +// Makes decison whether n tasks can be allowed or not. +// +// Parameters: +// +// 1. n: number of tasks to be processed, set this as 1 for a single task. (Example: An HTTP request) +// +// Returns (bool, error). (false, error) if limiter is inactive (or it is killed). Otherwise, +// (true/false, error) depending on whether n tasks can be allowed or not. func (s *SyncLimiter) ShouldAllow(n uint64) (bool, error) { s.lock.Lock() defer s.lock.Unlock() @@ -166,6 +191,7 @@ func (s *SyncLimiter) ShouldAllow(n uint64) (bool, error) { return true, nil } +// Kill the limiter, returns error if the limiter has been killed already. func (s *SyncLimiter) Kill() error { s.lock.Lock() defer s.lock.Unlock() @@ -180,6 +206,13 @@ func (s *SyncLimiter) Kill() error { return nil } +// Creates an instance of SyncLimiter and returns it's pointer. +// +// Parameters: +// +// 1. limit: The number of tasks to be allowd +// +// 2. size: duration func NewSyncLimiter(limit uint64, size time.Duration) *SyncLimiter { current := NewWindow(0, time.Unix(0, 0)) diff --git a/window.go b/window.go index 3c3c6c6..84cd6cb 100644 --- a/window.go +++ b/window.go @@ -4,6 +4,7 @@ import ( "time" ) +// Window represents the structure of timing-window at given point of time. type Window struct { count uint64 startTime time.Time @@ -32,6 +33,13 @@ func (w *Window) setToState(startTime time.Time, count uint64) { w.count = count } +// Creates and returns a pointer to the new Window instance. +// +// Parameters: +// +// 1. count: The initial count of the window. +// +// 2. startTime: The initial starting time of the window. func NewWindow(count uint64, startTime time.Time) *Window { return &Window{