Skip to content

Commit

Permalink
remove redundant
Browse files Browse the repository at this point in the history
  • Loading branch information
da440dil committed Dec 17, 2020
1 parent 6f5fb82 commit 8c8f659
Show file tree
Hide file tree
Showing 23 changed files with 473 additions and 956 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: go
go:
- 1.13.x
- 1.14.x
- 1.15.x
services:
- redis
env:
Expand Down
29 changes: 6 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,13 @@
[![GoDoc](https://godoc.org/github.com/da440dil/go-counter?status.svg)](https://godoc.org/github.com/da440dil/go-counter)
[![Go Report Card](https://goreportcard.com/badge/github.com/da440dil/go-counter)](https://goreportcard.com/report/github.com/da440dil/go-counter)

Distributed rate limiting with pluggable storage for storing counters state.
Distributed rate limiting using [Redis](https://redis.io/).

## Basic usage
Example usage:

```go
// Create new Counter
c, _ := counter.New(1, time.Millisecond*100)
// Increment counter and get remainder
if v, err := c.Count("key"); err != nil {
if e, ok := err.(*counter.TTLError); ok {
// Use e.TTL() if need
} else {
// Handle err
}
} else {
// Counter value equals 1
// Remainder value (v) equals 0
// Next c.Count("key") call will return TTLError
}
```
- [example](./examples/fixedwindow/main.go) using [fixed window](./fixedwindow.go) algorithm

## Example usage
```go run examples/fixedwindow/main.go```
- [example](./examples/slidingwindow/main.go) using [sliding window](./slidingwindow.go) algorithm

- [example](./examples/counter-gateway-default/main.go) usage with default [gateway](./gateway/memory/memory.go)
- [example](./examples/counter-gateway-memory/main.go) usage with memory [gateway](./gateway/memory/memory.go)
- [example](./examples/counter-gateway-redis/main.go) usage with [Redis](https://redis.io) [gateway](./gateway/redis/redis.go)
- [example](./examples/counter-with-retry/main.go) usage with [retry](https://github.com/da440dil/go-trier)
```go run examples/slidingwindow/main.go```
143 changes: 58 additions & 85 deletions counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,109 +2,82 @@
package counter

import (
"context"
"errors"
"time"

gw "github.com/da440dil/go-counter/gateway/memory"
"github.com/go-redis/redis/v8"
)

// Gateway to storage to store a counter value.
type Gateway interface {
// Incr sets key value and TTL of key if key not exists.
// Increments key value if key exists.
// Returns key value after increment.
// Returns TTL of a key in milliseconds.
Incr(key string, ttl int) (int, int, error)
// RedisClient is redis scripter interface.
type RedisClient interface {
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *redis.Cmd
EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *redis.Cmd
ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd
ScriptLoad(ctx context.Context, script string) *redis.StringCmd
}

// Option is function returned by functions for setting options.
type Option func(c *Counter) error
var errInvalidResponse = errors.New("counter: invalid redis response")

// WithGateway sets counter gateway.
// Gateway is gateway to storage to store a counter value.
// If gateway not set counter creates new memory gateway
// with expired keys cleanup every 100 milliseconds.
func WithGateway(v Gateway) Option {
return func(c *Counter) error {
c.gateway = v
return nil
}
// Result of count() operation.
type Result struct {
counter int64
ttl int64
}

// WithPrefix sets prefix of a key.
func WithPrefix(v string) Option {
return func(c *Counter) error {
if !isValidKey(v) {
return ErrInvalidKey
}
c.prefix = v
return nil
}
// OK is operation success flag.
func (r Result) OK() bool {
return r.ttl == -1
}

// Counter after increment.
// With fixed window algorithm in use counter is current window counter.
// With sliding window algorithm in use counter is sliding window counter.
func (r Result) Counter() int {
return int(r.counter)
}

// TTL of the current window in milliseconds.
// Makes sense if operation failed, otherwise ttl is less than 0.
func (r Result) TTL() time.Duration {
return time.Duration(r.ttl) * time.Millisecond
}

// Counter implements distributed rate limiting.
type Counter struct {
gateway Gateway
ttl int
limit int
prefix string
client RedisClient
size int
limit int
script *redis.Script
}

// New creates new Counter.
// Limit is maximum key value, must be greater than 0.
// TTL is TTL of a key, must be greater than or equal to 1 millisecond.
// Options are functional options.
func New(limit int, ttl time.Duration, options ...Option) (*Counter, error) {
if limit < 1 {
return nil, ErrInvalidLimit
}
if ttl < time.Millisecond {
return nil, ErrInvalidTTL
}
c := &Counter{
ttl: durationToMilliseconds(ttl),
limit: limit,
}
for _, fn := range options {
if err := fn(c); err != nil {
return nil, err
}
}
if c.gateway == nil {
c.gateway = gw.New(time.Millisecond * 100)
}
return c, nil
func newCounter(client RedisClient, size time.Duration, limit int, src string) *Counter {
return &Counter{client, int(size / time.Millisecond), limit, redis.NewScript(src)}
}

// Count increments key value.
// Returns limit remainder.
// Returns TTLError if limit exceeded.
func (c *Counter) Count(key string) (int, error) {
key = c.prefix + key
if !isValidKey(key) {
return -1, ErrInvalidKey
}
value, ttl, err := c.gateway.Incr(key, c.ttl)
// Count increments key by value.
func (c *Counter) Count(ctx context.Context, key string, value int) (Result, error) {
r := Result{}
res, err := c.script.Run(ctx, c.client, []string{key}, value, c.size, c.limit).Result()
if err != nil {
return -1, err
return r, err
}
rem := c.limit - value
if rem < 0 {
return rem, newTTLError(ttl)
var arr []interface{}
var ok bool
arr, ok = res.([]interface{})
if !ok {
return r, errInvalidResponse
}
return rem, nil
}

func durationToMilliseconds(duration time.Duration) int {
return int(duration / time.Millisecond)
}

func millisecondsToDuration(ttl int) time.Duration {
return time.Duration(ttl) * time.Millisecond
}

// MaxKeySize is maximum key size in bytes.
const MaxKeySize = 512000000

func isValidKey(key string) bool {
return len([]byte(key)) <= MaxKeySize
if len(arr) != 2 {
return r, errInvalidResponse
}
r.counter, ok = arr[0].(int64)
if !ok {
return r, errInvalidResponse
}
r.ttl, ok = arr[1].(int64)
if !ok {
return r, errInvalidResponse
}
return r, nil
}
Loading

0 comments on commit 8c8f659

Please sign in to comment.