Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for x-ratelimit-reset header #182

Merged
merged 3 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ go 1.14
require (
github.com/alicebob/miniredis/v2 v2.11.4
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354 // indirect
github.com/coocood/freecache v1.1.0
github.com/envoyproxy/go-control-plane v0.9.6
github.com/envoyproxy/go-control-plane v0.9.7
github.com/fsnotify/fsnotify v1.4.7 // indirect
github.com/golang/mock v1.4.1
github.com/golang/protobuf v1.4.2
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533 h1:8wZizuKuZVu5COB7EsBYxBQz8nRcXXn5d4Gt91eJLvU=
github.com/cncf/udpa/go v0.0.0-20200313221541-5f7e5dd04533/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354 h1:9kRtNpqLHbZVO/NNxhHp2ymxFxsHOe3x2efJGn//Tas=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/coocood/freecache v1.1.0 h1:ENiHOsWdj1BrrlPwblhbn4GdAsMymK3pZORJ+bJGAjA=
Expand All @@ -24,8 +22,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.6 h1:GgblEiDzxf5ajlAZY4aC8xp7DwkrGfauFNMGdB2bBv0=
github.com/envoyproxy/go-control-plane v0.9.6/go.mod h1:GFqM7v0B62MraO4PWRedIbhThr/Rf7ev6aHOOPXeaDA=
github.com/envoyproxy/go-control-plane v0.9.7 h1:EARl0OvqMoxq/UMgMSCLnXzkaXbxzskluEBlMQCJPms=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
Expand Down
21 changes: 2 additions & 19 deletions src/limiter/cache_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
pb_struct "github.com/envoyproxy/go-control-plane/envoy/extensions/common/ratelimit/v3"
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
"github.com/envoyproxy/ratelimit/src/config"
"github.com/envoyproxy/ratelimit/src/utils"
)

type CacheKeyGenerator struct {
Expand All @@ -33,24 +34,6 @@ func isPerSecondLimit(unit pb.RateLimitResponse_RateLimit_Unit) bool {
return unit == pb.RateLimitResponse_RateLimit_SECOND
}

// Convert a rate limit into a time divider.
// @param unit supplies the unit to convert.
// @return the divider to use in time computations.
func UnitToDivider(unit pb.RateLimitResponse_RateLimit_Unit) int64 {
switch unit {
case pb.RateLimitResponse_RateLimit_SECOND:
return 1
case pb.RateLimitResponse_RateLimit_MINUTE:
return 60
case pb.RateLimitResponse_RateLimit_HOUR:
return 60 * 60
case pb.RateLimitResponse_RateLimit_DAY:
return 60 * 60 * 24
}

panic("should not get here")
}

// Generate a cache key for a limit lookup.
// @param domain supplies the cache key domain.
// @param descriptor supplies the descriptor to generate the key for.
Expand Down Expand Up @@ -81,7 +64,7 @@ func (this *CacheKeyGenerator) GenerateCacheKey(
b.WriteByte('_')
}

divider := UnitToDivider(limit.Limit.Unit)
divider := utils.UnitToDivider(limit.Limit.Unit)
b.WriteString(strconv.FormatInt((now/divider)*divider, 10))

return CacheKey{
Expand Down
33 changes: 22 additions & 11 deletions src/redis/cache_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/envoyproxy/ratelimit/src/limiter"
"github.com/envoyproxy/ratelimit/src/server"
"github.com/envoyproxy/ratelimit/src/settings"
"github.com/envoyproxy/ratelimit/src/utils"
"github.com/golang/protobuf/ptypes/duration"
logger "github.com/sirupsen/logrus"
"golang.org/x/net/context"
)
Expand Down Expand Up @@ -89,7 +91,7 @@ func (this *rateLimitCacheImpl) DoLimit(

logger.Debugf("looking up cache key: %s", cacheKey.Key)

expirationSeconds := limiter.UnitToDivider(limits[i].Limit.Unit)
expirationSeconds := utils.UnitToDivider(limits[i].Limit.Unit)
if this.expirationJitterMaxSeconds > 0 {
expirationSeconds += this.jitterRand.Int63n(this.expirationJitterMaxSeconds)
}
Expand Down Expand Up @@ -132,9 +134,10 @@ func (this *rateLimitCacheImpl) DoLimit(
if isOverLimitWithLocalCache[i] {
responseDescriptorStatuses[i] =
&pb.RateLimitResponse_DescriptorStatus{
Code: pb.RateLimitResponse_OVER_LIMIT,
CurrentLimit: limits[i].Limit,
LimitRemaining: 0,
Code: pb.RateLimitResponse_OVER_LIMIT,
CurrentLimit: limits[i].Limit,
LimitRemaining: 0,
DurationUntilReset: CalculateReset(limits[i].Limit, this.timeSource),
}
limits[i].Stats.OverLimit.Add(uint64(hitsAddend))
limits[i].Stats.OverLimitWithLocalCache.Add(uint64(hitsAddend))
Expand All @@ -152,9 +155,10 @@ func (this *rateLimitCacheImpl) DoLimit(
if limitAfterIncrease > overLimitThreshold {
responseDescriptorStatuses[i] =
&pb.RateLimitResponse_DescriptorStatus{
Code: pb.RateLimitResponse_OVER_LIMIT,
CurrentLimit: limits[i].Limit,
LimitRemaining: 0,
Code: pb.RateLimitResponse_OVER_LIMIT,
CurrentLimit: limits[i].Limit,
LimitRemaining: 0,
DurationUntilReset: CalculateReset(limits[i].Limit, this.timeSource),
}

// Increase over limit statistics. Because we support += behavior for increasing the limit, we need to
Expand All @@ -179,17 +183,18 @@ func (this *rateLimitCacheImpl) DoLimit(
// similar to mongo_1h, mongo_2h, etc. In the hour 1 (0h0m - 0h59m), the cache key is mongo_1h, we start
// to get ratelimited in the 50th minute, the ttl of local_cache will be set as 1 hour(0h50m-1h49m).
// In the time of 1h1m, since the cache key becomes different (mongo_2h), it won't get ratelimited.
err := this.localCache.Set([]byte(cacheKey.Key), []byte{}, int(limiter.UnitToDivider(limits[i].Limit.Unit)))
err := this.localCache.Set([]byte(cacheKey.Key), []byte{}, int(utils.UnitToDivider(limits[i].Limit.Unit)))
if err != nil {
logger.Errorf("Failing to set local cache key: %s", cacheKey.Key)
}
}
} else {
responseDescriptorStatuses[i] =
&pb.RateLimitResponse_DescriptorStatus{
Code: pb.RateLimitResponse_OK,
CurrentLimit: limits[i].Limit,
LimitRemaining: overLimitThreshold - limitAfterIncrease,
Code: pb.RateLimitResponse_OK,
CurrentLimit: limits[i].Limit,
LimitRemaining: overLimitThreshold - limitAfterIncrease,
DurationUntilReset: CalculateReset(limits[i].Limit, this.timeSource),
}

// The limit is OK but we additionally want to know if we are near the limit.
Expand All @@ -210,6 +215,12 @@ func (this *rateLimitCacheImpl) DoLimit(
return responseDescriptorStatuses
}

func CalculateReset(currentLimit *pb.RateLimitResponse_RateLimit, timeSource limiter.TimeSource) *duration.Duration {
sec := utils.UnitToDivider(currentLimit.Unit)
now := timeSource.UnixNow()
return &duration.Duration{Seconds: sec - now%sec}
}

func NewRateLimitCacheImpl(client Client, perSecondClient Client, timeSource limiter.TimeSource, jitterRand *rand.Rand, expirationJitterMaxSeconds int64, localCache *freecache.Cache) limiter.RateLimitCache {
return &rateLimitCacheImpl{
client: client,
Expand Down
23 changes: 23 additions & 0 deletions src/utils/utilities.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package utils

import (
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
)

// Convert a rate limit into a time divider.
// @param unit supplies the unit to convert.
// @return the divider to use in time computations.
func UnitToDivider(unit pb.RateLimitResponse_RateLimit_Unit) int64 {
switch unit {
case pb.RateLimitResponse_RateLimit_SECOND:
return 1
case pb.RateLimitResponse_RateLimit_MINUTE:
return 60
case pb.RateLimitResponse_RateLimit_HOUR:
return 60 * 60
case pb.RateLimitResponse_RateLimit_DAY:
return 60 * 60 * 24
}

panic("should not get here")
}
39 changes: 7 additions & 32 deletions test/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@
package integration_test

import (
"bytes"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os"
"strconv"
"testing"
Expand All @@ -17,7 +14,9 @@ import (
pb_legacy "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v2"
pb "github.com/envoyproxy/go-control-plane/envoy/service/ratelimit/v3"
"github.com/envoyproxy/ratelimit/src/service_cmd/runner"
"github.com/envoyproxy/ratelimit/src/utils"
"github.com/envoyproxy/ratelimit/test/common"
"github.com/golang/protobuf/ptypes/duration"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
"google.golang.org/grpc"
Expand All @@ -27,10 +26,14 @@ func newDescriptorStatus(
status pb.RateLimitResponse_Code, requestsPerUnit uint32,
unit pb.RateLimitResponse_RateLimit_Unit, limitRemaining uint32) *pb.RateLimitResponse_DescriptorStatus {

limit := &pb.RateLimitResponse_RateLimit{RequestsPerUnit: requestsPerUnit, Unit: unit}
sec := utils.UnitToDivider(unit)
now := time.Now().Unix()
return &pb.RateLimitResponse_DescriptorStatus{
Code: status,
CurrentLimit: &pb.RateLimitResponse_RateLimit{RequestsPerUnit: requestsPerUnit, Unit: unit},
CurrentLimit: limit,
LimitRemaining: limitRemaining,
DurationUntilReset: &duration.Duration{Seconds: sec - now%sec},
}
}

Expand Down Expand Up @@ -509,34 +512,6 @@ func TestBasicConfigLegacy(t *testing.T) {
response)
assert.NoError(err)

json_body := []byte(`{
"domain": "basic",
"descriptors": [
{
"entries": [
{
"key": "one_per_minute"
}
]
}
]
}`)
http_resp, _ := http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(json_body))
assert.Equal(http_resp.StatusCode, 200)
body, _ := ioutil.ReadAll(http_resp.Body)
http_resp.Body.Close()
assert.Equal(`{"overallCode":"OK","statuses":[{"code":"OK","currentLimit":{"requestsPerUnit":1,"unit":"MINUTE"}}]}`, string(body))

http_resp, _ = http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(json_body))
assert.Equal(http_resp.StatusCode, 429)
body, _ = ioutil.ReadAll(http_resp.Body)
http_resp.Body.Close()
assert.Equal(`{"overallCode":"OVER_LIMIT","statuses":[{"code":"OVER_LIMIT","currentLimit":{"requestsPerUnit":1,"unit":"MINUTE"}}]}`, string(body))

invalid_json := []byte(`{"unclosed quote: []}`)
http_resp, _ = http.Post("http://localhost:8082/json", "application/json", bytes.NewBuffer(invalid_json))
assert.Equal(http_resp.StatusCode, 400)

response, err = c.ShouldRateLimit(
context.Background(),
common.NewRateLimitRequestLegacy("basic_legacy", [][][2]string{{{"key1", "foo"}}}, 1))
Expand Down
Loading