Skip to content

Commit

Permalink
improve functionality
Browse files Browse the repository at this point in the history
- add DefaultRedis wrapper
- fix middleware nil pointer
- upd linter
- upd pipeline
- upd docs
  • Loading branch information
robotomize committed May 6, 2022
1 parent 47694f6 commit 7d77436
Show file tree
Hide file tree
Showing 5 changed files with 149 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
- name: go-lint
uses: golangci/golangci-lint-action@v2
with:
version: 'v1.41.1'
version: 'v1.45.2'
skip-go-installation: true
skip-pkg-cache: true
skip-build-cache: true
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ linters:
- prealloc

service:
golangci-lint-version: 1.41.x # use the fixed version to not introduce new linters unexpectedly
golangci-lint-version: 1.45.x

severity:
default-severity: error
129 changes: 128 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,130 @@
# go-ratelimiter
A super easy rate limiting package for Go

A super easy rate limiting package for Go. Package defines Store storage interface, for which you can use your own
implementations

# Install

```shell
go get github.com/robotomize/go-ratelimiter
```

# Usage

Example of using redis datastore

```go

package main

import (
"context"
"fmt"
"log"
"time"

"github.com/robotomize/go-ratelimiter"
)

func main() {
// set a limit of 10 request per 1 seconds per api key
redisStore, err := ratelimiter.DefaultRedisStore(context.Background(), ":6379", 1*time.Second, 10)
if err != nil {
log.Fatal(err)
}

// Retrieve data by the api key in the datastore
limit, remaining, resetTime, ok, err := redisStore.Take(context.Background(), "apikey-1")
if err != nil {
log.Fatal(err)
}

if !ok {
fmt.Println("limit exceeded")
}

// Print the constraints from the datastore
fmt.Printf(
"resource: maximum %d, remaining %d, reset time %s",
limit, remaining, time.Unix(0, int64(resetTime)).UTC().Format(time.RFC1123),
)
}

```

To limit access at the http level, you can use middleware, which can block the request by providing the http code 429
Too Many Requests

Example of using http middleware with redis datastore

```go
package main

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"time"

"github.com/robotomize/go-ratelimiter"
)

func main() {
// set a limit of 5 request per 1 seconds per api key
redisStore, err := ratelimiter.DefaultRedisStore(context.Background(), ":6379", 1*time.Second, 5)
if err != nil {
log.Fatal(err)
}

mx := http.NewServeMux()
// Let's create a test handler
healthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status": "ok"}`))
})

mx.Handle(
"/health", ratelimiter.LimiterMiddleware(
redisStore, func(r *http.Request) (string, error) {
// Example key func
ctx := r.Context()
// Get key value out of context
ctxValue := ctx.Value("apikey")
if key, ok := ctxValue.(string); ok {
return key, nil
}

return "", errors.New("get api key from ctx")
}, ratelimiter.WithSkipper(func() bool {
// set a skipper, skip ratelimiter if DEBUG == 1
return os.Getenv("DEBUG") == "1"
}, ),
)(healthHandler),
)

// start listener
if err = http.ListenAndServe(":8888", mx); err != nil {
log.Fatal(err)
}
}
```

# TODO

* ~add http middleware~
* ~add simple redis datastore~
* extend Store methods
* improve redis datastore
* try using tarantool datastore
* try using aerospike datastore

## Contributing


## License

go-ratelimiter is under the Apache 2.0 license. See the [LICENSE](LICENSE) file for details.
7 changes: 7 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ func LimiterMiddleware(s Store, keyFunc KeyFunc, opts ...Option) func(next http.
}
}

if keyFunc == nil {
// if key func is nil return 500 Internal Server Error
w.WriteHeader(http.StatusInternalServerError)

return
}

// extract entity
key, err := keyFunc(r)
if err != nil {
Expand Down
14 changes: 12 additions & 2 deletions redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strconv"
"time"

redis "github.com/go-redis/redis/v8"
"github.com/go-redis/redis/v8"
)

const (
Expand Down Expand Up @@ -52,6 +52,16 @@ type RedisClient interface {
SMembers(ctx context.Context, key string) *redis.StringSliceCmd
}

// DefaultRedisStore return Store instance of default redis options
func DefaultRedisStore(ctx context.Context, addr string, interval time.Duration, points uint64) (Store, error) {
r := redis.NewClient(&redis.Options{Addr: addr})
if err := r.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("redis client ping: %w", err)
}

return NewRedisStore(r, RedisConfig{Interval: interval, Points: points}), nil
}

// NewRedisStore make redis store
func NewRedisStore(instance RedisClient, cfg RedisConfig) Store {
var prefix, tagPrefix string
Expand Down Expand Up @@ -127,7 +137,7 @@ func (r redisStore) TakeExcl(ctx context.Context, key string, f ExclFunc) (limit
func (r redisStore) take(ctx context.Context, key string) (limit, remaining, resetTimeUint uint64, ok bool, err error) {
prefixedKey := fmt.Sprintf("%s%s", r.prefix, key)

//Trying to get points from the current key
// Trying to get points from the current key
vals, err := r.client.HMGet(ctx, prefixedKey, redisMaxPointsFieldName, redisActualPointsFieldName, redisResetTimeFieldName).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
Expand Down

0 comments on commit 7d77436

Please sign in to comment.