Skip to content
This repository was archived by the owner on Apr 19, 2024. It is now read-only.

Commit 72b9541

Browse files
authored
Merge pull request #93 from mailgun/thrawn/develop
Performance improvements in GetRateLimits()
2 parents c797905 + 252db4f commit 72b9541

File tree

3 files changed

+165
-92
lines changed

3 files changed

+165
-92
lines changed

CHANGELOG

+9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.0.0-rc.5] - 2021-06-03
8+
## Changes
9+
* Implemented performance recommendations reported in Issue #74
10+
11+
## [2.0.0-rc.4] - 2021-06-03
12+
## Changes
13+
* Add support for burst in leaky bucket #103
14+
* Add working example of aws ecs service discovery deployment #102
15+
716
## [2.0.0-rc.2] - 2021-07-11
817
## Changes
918
* Deprecated github.com/golang/protobuf was replaced with google.golang.org/protobuf

benchmark_test.go

+29-2
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,33 @@ func BenchmarkServer_GetRateLimit(b *testing.B) {
7979
})
8080
}
8181

82+
func BenchmarkServer_GetRateLimitGlobal(b *testing.B) {
83+
client, err := guber.DialV1Server(cluster.GetRandomPeer(cluster.DataCenterNone).GRPCAddress, nil)
84+
if err != nil {
85+
b.Errorf("NewV1Client err: %s", err)
86+
}
87+
88+
b.Run("GetRateLimitGlobal", func(b *testing.B) {
89+
for n := 0; n < b.N; n++ {
90+
_, err := client.GetRateLimits(context.Background(), &guber.GetRateLimitsReq{
91+
Requests: []*guber.RateLimitReq{
92+
{
93+
Name: "get_rate_limit_benchmark",
94+
UniqueKey: guber.RandomString(10),
95+
Behavior: guber.Behavior_GLOBAL,
96+
Limit: 10,
97+
Duration: guber.Second * 5,
98+
Hits: 1,
99+
},
100+
},
101+
})
102+
if err != nil {
103+
b.Errorf("client.RateLimit() err: %s", err)
104+
}
105+
}
106+
})
107+
}
108+
82109
func BenchmarkServer_Ping(b *testing.B) {
83110
client, err := guber.DialV1Server(cluster.GetRandomPeer(cluster.DataCenterNone).GRPCAddress, nil)
84111
if err != nil {
@@ -107,13 +134,13 @@ func BenchmarkServer_Ping(b *testing.B) {
107134
}
108135
}*/
109136

110-
func BenchmarkServer_ThunderingHeard(b *testing.B) {
137+
func BenchmarkServer_ThunderingHerd(b *testing.B) {
111138
client, err := guber.DialV1Server(cluster.GetRandomPeer(cluster.DataCenterNone).GRPCAddress, nil)
112139
if err != nil {
113140
b.Errorf("NewV1Client err: %s", err)
114141
}
115142

116-
b.Run("ThunderingHeard", func(b *testing.B) {
143+
b.Run("ThunderingHerd", func(b *testing.B) {
117144
fan := syncutil.NewFanOut(100)
118145
for n := 0; n < b.N; n++ {
119146
fan.Run(func(o interface{}) error {

gubernator.go

+127-90
Original file line numberDiff line numberDiff line change
@@ -114,116 +114,153 @@ func (s *V1Instance) Close() error {
114114
// rate limit `Name` and `UniqueKey` is not owned by this instance then we forward the request to the
115115
// peer that does.
116116
func (s *V1Instance) GetRateLimits(ctx context.Context, r *GetRateLimitsReq) (*GetRateLimitsResp, error) {
117-
var resp GetRateLimitsResp
118117
if len(r.Requests) > maxBatchSize {
119118
return nil, status.Errorf(codes.OutOfRange,
120119
"Requests.RateLimits list too large; max size is '%d'", maxBatchSize)
121120
}
122121

123-
type InOut struct {
124-
In *RateLimitReq
125-
Idx int
126-
Out *RateLimitResp
122+
resp := GetRateLimitsResp{
123+
Responses: make([]*RateLimitResp, len(r.Requests)),
127124
}
128125

129-
// Asynchronously fetch rate limits
130-
out := make(chan InOut)
131-
go func() {
132-
fan := syncutil.NewFanOut(1000)
133-
// For each item in the request body
134-
for i, item := range r.Requests {
135-
fan.Run(func(data interface{}) error {
136-
inOut := data.(InOut)
137-
138-
globalKey := inOut.In.Name + "_" + inOut.In.UniqueKey
139-
var peer *PeerClient
140-
var err error
141-
142-
if len(inOut.In.UniqueKey) == 0 {
143-
inOut.Out = &RateLimitResp{Error: "field 'unique_key' cannot be empty"}
144-
out <- inOut
145-
return nil
146-
}
126+
var wg sync.WaitGroup
127+
asyncCh := make(chan AsyncResp, len(r.Requests))
147128

148-
if len(inOut.In.Name) == 0 {
149-
inOut.Out = &RateLimitResp{Error: "field 'namespace' cannot be empty"}
150-
out <- inOut
151-
return nil
152-
}
129+
// For each item in the request body
130+
for i, req := range r.Requests {
131+
key := req.Name + "_" + req.UniqueKey
132+
var peer *PeerClient
133+
var err error
153134

154-
var attempts int
155-
getPeer:
156-
if attempts > 5 {
157-
inOut.Out = &RateLimitResp{
158-
Error: fmt.Sprintf("GetPeer() keeps returning peers that are not connected for '%s' - '%s'", globalKey, err),
159-
}
160-
out <- inOut
161-
return nil
162-
}
135+
if len(req.UniqueKey) == 0 {
136+
resp.Responses[i] = &RateLimitResp{Error: "field 'unique_key' cannot be empty"}
137+
continue
138+
}
139+
140+
if len(req.Name) == 0 {
141+
resp.Responses[i] = &RateLimitResp{Error: "field 'namespace' cannot be empty"}
142+
continue
143+
}
144+
145+
peer, err = s.GetPeer(key)
146+
if err != nil {
147+
resp.Responses[i] = &RateLimitResp{
148+
Error: fmt.Sprintf("while finding peer that owns rate limit '%s' - '%s'", key, err),
149+
}
150+
continue
151+
}
163152

164-
peer, err = s.GetPeer(globalKey)
153+
// If our server instance is the owner of this rate limit
154+
if peer.Info().IsOwner {
155+
// Apply our rate limit algorithm to the request
156+
resp.Responses[i], err = s.getRateLimit(req)
157+
if err != nil {
158+
resp.Responses[i] = &RateLimitResp{
159+
Error: fmt.Sprintf("while applying rate limit for '%s' - '%s'", key, err),
160+
}
161+
}
162+
} else {
163+
if HasBehavior(req.Behavior, Behavior_GLOBAL) {
164+
resp.Responses[i], err = s.getGlobalRateLimit(req)
165165
if err != nil {
166-
inOut.Out = &RateLimitResp{
167-
Error: fmt.Sprintf("while finding peer that owns rate limit '%s' - '%s'", globalKey, err),
168-
}
169-
out <- inOut
170-
return nil
166+
resp.Responses[i] = &RateLimitResp{Error: err.Error()}
171167
}
172168

173-
// If our server instance is the owner of this rate limit
174-
if peer.Info().IsOwner {
175-
// Apply our rate limit algorithm to the request
176-
inOut.Out, err = s.getRateLimit(inOut.In)
177-
if err != nil {
178-
inOut.Out = &RateLimitResp{
179-
Error: fmt.Sprintf("while applying rate limit for '%s' - '%s'", globalKey, err),
180-
}
181-
}
182-
} else {
183-
if HasBehavior(inOut.In.Behavior, Behavior_GLOBAL) {
184-
inOut.Out, err = s.getGlobalRateLimit(inOut.In)
185-
if err != nil {
186-
inOut.Out = &RateLimitResp{Error: err.Error()}
187-
}
188-
189-
// Inform the client of the owner key of the key
190-
inOut.Out.Metadata = map[string]string{"owner": peer.Info().GRPCAddress}
191-
192-
out <- inOut
193-
return nil
194-
}
169+
// Inform the client of the owner key of the key
170+
resp.Responses[i].Metadata = map[string]string{"owner": peer.Info().GRPCAddress}
171+
continue
172+
}
195173

196-
// Make an RPC call to the peer that owns this rate limit
197-
inOut.Out, err = peer.GetPeerRateLimit(ctx, inOut.In)
198-
if err != nil {
199-
if IsNotReady(err) {
200-
attempts++
201-
goto getPeer
202-
}
203-
inOut.Out = &RateLimitResp{
204-
Error: fmt.Sprintf("while fetching rate limit '%s' from peer - '%s'", globalKey, err),
205-
}
206-
}
174+
wg.Add(1)
175+
go s.asyncRequests(ctx, &AsyncReq{
176+
AsyncCh: asyncCh,
177+
Peer: peer,
178+
Req: req,
179+
WG: &wg,
180+
Key: key,
181+
})
182+
}
183+
}
207184

208-
// Inform the client of the owner key of the key
209-
inOut.Out.Metadata = map[string]string{"owner": peer.Info().GRPCAddress}
210-
}
185+
// Wait for any async responses if any
186+
wg.Wait()
187+
close(asyncCh)
188+
for a := range asyncCh {
189+
resp.Responses[a.Idx] = a.Resp
190+
}
191+
192+
return &resp, nil
193+
}
194+
195+
type AsyncResp struct {
196+
Idx int
197+
Resp *RateLimitResp
198+
}
199+
200+
type AsyncReq struct {
201+
WG *sync.WaitGroup
202+
AsyncCh chan AsyncResp
203+
Req *RateLimitReq
204+
Peer *PeerClient
205+
Key string
206+
Idx int
207+
}
208+
209+
func (s *V1Instance) asyncRequests(ctx context.Context, req *AsyncReq) {
210+
var attempts int
211+
var err error
212+
213+
resp := AsyncResp{
214+
Idx: req.Idx,
215+
}
211216

212-
out <- inOut
213-
return nil
214-
}, InOut{In: item, Idx: i})
217+
for {
218+
if attempts > 5 {
219+
resp.Resp = &RateLimitResp{
220+
Error: fmt.Sprintf("GetPeer() keeps returning peers that are not connected for '%s' - '%s'", req.Key, err),
221+
}
222+
break
215223
}
216-
fan.Wait()
217-
close(out)
218-
}()
219224

220-
resp.Responses = make([]*RateLimitResp, len(r.Requests))
221-
// Collect the async responses as they return
222-
for i := range out {
223-
resp.Responses[i.Idx] = i.Out
225+
// If we are attempting again, the owner of the this rate limit might have changed to us!
226+
if attempts != 0 {
227+
if req.Peer.Info().IsOwner {
228+
resp.Resp, err = s.getRateLimit(req.Req)
229+
if err != nil {
230+
resp.Resp = &RateLimitResp{
231+
Error: fmt.Sprintf("while applying rate limit for '%s' - '%s'", req.Key, err),
232+
}
233+
}
234+
break
235+
}
236+
}
237+
238+
// Make an RPC call to the peer that owns this rate limit
239+
r, err := req.Peer.GetPeerRateLimit(ctx, req.Req)
240+
if err != nil {
241+
if IsNotReady(err) {
242+
attempts++
243+
req.Peer, err = s.GetPeer(req.Key)
244+
if err != nil {
245+
resp.Resp = &RateLimitResp{
246+
Error: fmt.Sprintf("while finding peer that owns rate limit '%s' - '%s'", req.Key, err),
247+
}
248+
break
249+
}
250+
continue
251+
}
252+
resp.Resp = &RateLimitResp{
253+
Error: fmt.Sprintf("while fetching rate limit '%s' from peer - '%s'", req.Key, err),
254+
}
255+
}
256+
// Inform the client of the owner key of the key
257+
resp.Resp = r
258+
resp.Resp.Metadata = map[string]string{"owner": req.Peer.Info().GRPCAddress}
259+
break
224260
}
225261

226-
return &resp, nil
262+
req.AsyncCh <- resp
263+
req.WG.Done()
227264
}
228265

229266
// getGlobalRateLimit handles rate limits that are marked as `Behavior = GLOBAL`. Rate limit responses

0 commit comments

Comments
 (0)