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

Ratelimit interceptor #181

Merged
merged 8 commits into from
Jun 3, 2019
Merged
Show file tree
Hide file tree
Changes from 7 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ myServer := grpc.NewServer(
#### Server
* [`grpc_validator`](validator/) - codegen inbound message validation from `.proto` options
* [`grpc_recovery`](recovery/) - turn panics into gRPC errors
* [`ratelimit`](ratelimit/) - grpc rate limiting by your own limiter


## Status
Expand Down
12 changes: 12 additions & 0 deletions ratelimit/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
`ratelimit` a generic server-side ratelimit middleware for gRPC.

Server Side Ratelimit Middleware

It allows to do grpc rate limit by your own rate limiter (e.g. token bucket, leaky bucket, etc.)

`ratelimit/tokenbucket`provides an implementation based on token bucket `github.com/juju/ratelimit`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we update ... I dont think this is true anymore :)


Please see examples for simple examples of use.
*/
package ratelimit
26 changes: 26 additions & 0 deletions ratelimit/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package ratelimit_test

import (
"time"

"github.com/ceshihao/ratelimiter/tokenbucket"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is no longer in the project.

Tio do the example can we create a test rate limiter that meets the interface 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean to add a fake limiter (e.g. allwaysPassLimiter) in the example instead of the one based on juju/ratelimit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, lets just add a fake that returns true (or get creative) but one that is not exported ... dont really want the dep just for an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"github.com/grpc-ecosystem/go-grpc-middleware"
"github.com/grpc-ecosystem/go-grpc-middleware/ratelimit"
"google.golang.org/grpc"
)

// Simple example of server initialization code.
func Example() {
// Create unary/stream rateLimiters, based on token bucket here.
// You can implement your own ratelimiter for the interface.
unaryRateLimiter := tokenbucket.NewTokenBucketRateLimiter(1*time.Second, 10, 10, 10*time.Second)
streamRateLimiter := tokenbucket.NewTokenBucketRateLimiter(1*time.Second, 5, 5, 5*time.Second)
_ = grpc.NewServer(
grpc_middleware.WithUnaryServerChain(
ratelimit.UnaryServerInterceptor(unaryRateLimiter),
),
grpc_middleware.WithStreamServerChain(
ratelimit.StreamServerInterceptor(streamRateLimiter),
),
)
}
35 changes: 35 additions & 0 deletions ratelimit/ratelimit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ratelimit

import (
"golang.org/x/net/context"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// Limiter defines the interface to perform request rate limiting.
// If Limit function return true, the request will be rejected.
// Otherwise, the request will pass.
type Limiter interface {
Limit() bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add documentation on the the type and func :)

}

// UnaryServerInterceptor returns a new unary server interceptors that performs request rate limiting.
func UnaryServerInterceptor(limiter Limiter) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if limiter.Limit() {
return nil, status.Errorf(codes.ResourceExhausted, "%s is rejected by grpc_ratelimit middleare, please retry later.", info.FullMethod)
}
return handler(ctx, req)
}
}

// StreamServerInterceptor returns a new stream server interceptor that performs rate limiting on the request.
func StreamServerInterceptor(limiter Limiter) grpc.StreamServerInterceptor {
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if limiter.Limit() {
return status.Errorf(codes.ResourceExhausted, "%s is rejected by grpc_ratelimit middleare, please retry later.", info.FullMethod)
}
return handler(srv, stream)
}
}
75 changes: 75 additions & 0 deletions ratelimit/ratelimit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package ratelimit

import (
"context"
"errors"
"testing"

"google.golang.org/grpc"

"github.com/stretchr/testify/assert"
)

const errMsgFake = "fake error"

type mockPassLimiter struct{}

func (*mockPassLimiter) Limit() bool {
return false
}

func TestUnaryServerInterceptor_RateLimitPass(t *testing.T) {
interceptor := UnaryServerInterceptor(&mockPassLimiter{})
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, errors.New(errMsgFake)
}
info := &grpc.UnaryServerInfo{
FullMethod: "FakeMethod",
}
req, err := interceptor(nil, nil, info, handler)
assert.Nil(t, req)
assert.EqualError(t, err, errMsgFake)
}

type mockFailLimiter struct{}

func (*mockFailLimiter) Limit() bool {
return true
}

func TestUnaryServerInterceptor_RateLimitFail(t *testing.T) {
interceptor := UnaryServerInterceptor(&mockFailLimiter{})
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return nil, errors.New(errMsgFake)
}
info := &grpc.UnaryServerInfo{
FullMethod: "FakeMethod",
}
req, err := interceptor(nil, nil, info, handler)
assert.Nil(t, req)
assert.EqualError(t, err, "rpc error: code = ResourceExhausted desc = FakeMethod is rejected by grpc_ratelimit middleare, please retry later.")
}

func TestStreamServerInterceptor_RateLimitPass(t *testing.T) {
interceptor := StreamServerInterceptor(&mockPassLimiter{})
handler := func(srv interface{}, stream grpc.ServerStream) error {
return errors.New(errMsgFake)
}
info := &grpc.StreamServerInfo{
FullMethod: "FakeMethod",
}
err := interceptor(nil, nil, info, handler)
assert.EqualError(t, err, errMsgFake)
}

func TestStreamServerInterceptor_RateLimitFail(t *testing.T) {
interceptor := StreamServerInterceptor(&mockFailLimiter{})
handler := func(srv interface{}, stream grpc.ServerStream) error {
return errors.New(errMsgFake)
}
info := &grpc.StreamServerInfo{
FullMethod: "FakeMethod",
}
err := interceptor(nil, nil, info, handler)
assert.EqualError(t, err, "rpc error: code = ResourceExhausted desc = FakeMethod is rejected by grpc_ratelimit middleare, please retry later.")
}