go.temporal.io/server@v1.23.0/common/rpc/interceptor/concurrent_request_limit_test.go (about) 1 // The MIT License 2 // 3 // Copyright (c) 2020 Temporal Technologies Inc. All rights reserved. 4 // 5 // Copyright (c) 2020 Uber Technologies, Inc. 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in 15 // all copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 // THE SOFTWARE. 24 25 package interceptor 26 27 import ( 28 "context" 29 "testing" 30 31 "github.com/golang/mock/gomock" 32 "github.com/stretchr/testify/assert" 33 "go.temporal.io/api/workflowservice/v1" 34 "go.temporal.io/server/common/dynamicconfig" 35 "go.temporal.io/server/common/quotas" 36 "go.temporal.io/server/common/quotas/quotastest" 37 "google.golang.org/grpc" 38 39 "go.temporal.io/server/common/log" 40 "go.temporal.io/server/common/namespace" 41 ) 42 43 type nsCountLimitTestCase struct { 44 // name of the test case 45 name string 46 // request to be intercepted by the ConcurrentRequestLimitInterceptor 47 request any 48 // numBlockedRequests is the number of pending requests that will be blocked including the final request. 49 numBlockedRequests int 50 // memberCounter returns the number of members in the namespace. 51 memberCounter quotas.MemberCounter 52 // perInstanceLimit is the limit on the number of pending requests per-instance. 53 perInstanceLimit int 54 // globalLimit is the limit on the number of pending requests across all instances. 55 globalLimit int 56 // methodName is the fully-qualified name of the gRPC method being intercepted. 57 methodName string 58 // tokens is a map of method slugs (e.g. just the part of the method name after the final slash) to the number of 59 // tokens that will be consumed by that method. 60 tokens map[string]int 61 // expectRateLimit is true if the interceptor should respond with a rate limit error. 62 expectRateLimit bool 63 } 64 65 // TestNamespaceCountLimitInterceptor_Intercept verifies that the ConcurrentRequestLimitInterceptor responds with a rate 66 // limit error when requests would exceed the concurrent poller limit for a namespace. 67 func TestNamespaceCountLimitInterceptor_Intercept(t *testing.T) { 68 t.Parallel() 69 for _, tc := range []nsCountLimitTestCase{ 70 { 71 name: "no limit exceeded", 72 request: nil, 73 numBlockedRequests: 2, 74 perInstanceLimit: 2, 75 globalLimit: 4, 76 memberCounter: quotastest.NewFakeMemberCounter(2), 77 methodName: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace", 78 tokens: map[string]int{ 79 "DescribeNamespace": 1, 80 }, 81 expectRateLimit: false, 82 }, 83 { 84 name: "per-instance limit exceeded", 85 request: nil, 86 numBlockedRequests: 3, 87 perInstanceLimit: 2, 88 globalLimit: 4, 89 memberCounter: quotastest.NewFakeMemberCounter(2), 90 methodName: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace", 91 tokens: map[string]int{ 92 "DescribeNamespace": 1, 93 }, 94 expectRateLimit: true, 95 }, 96 { 97 name: "global limit exceeded", 98 request: nil, 99 numBlockedRequests: 3, 100 perInstanceLimit: 3, 101 globalLimit: 4, 102 memberCounter: quotastest.NewFakeMemberCounter(2), 103 methodName: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace", 104 tokens: map[string]int{ 105 "DescribeNamespace": 1, 106 }, 107 expectRateLimit: true, 108 }, 109 { 110 name: "global limit zero", 111 request: nil, 112 numBlockedRequests: 3, 113 perInstanceLimit: 3, 114 globalLimit: 0, 115 memberCounter: quotastest.NewFakeMemberCounter(2), 116 methodName: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace", 117 tokens: map[string]int{ 118 "DescribeNamespace": 1, 119 }, 120 expectRateLimit: false, 121 }, 122 { 123 name: "method name does not consume token", 124 request: nil, 125 numBlockedRequests: 3, 126 perInstanceLimit: 2, 127 globalLimit: 4, 128 memberCounter: quotastest.NewFakeMemberCounter(2), 129 methodName: "/temporal.api.workflowservice.v1.WorkflowService/DescribeNamespace", 130 tokens: map[string]int{}, 131 expectRateLimit: false, 132 }, 133 { 134 name: "long poll request", 135 request: &workflowservice.GetWorkflowExecutionHistoryRequest{WaitNewEvent: true}, 136 numBlockedRequests: 3, 137 perInstanceLimit: 2, 138 globalLimit: 4, 139 memberCounter: quotastest.NewFakeMemberCounter(2), 140 methodName: "/temporal.api.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory", 141 tokens: map[string]int{ 142 "GetWorkflowExecutionHistory": 1, 143 }, 144 expectRateLimit: true, 145 }, 146 { 147 name: "non-long poll request", 148 request: &workflowservice.GetWorkflowExecutionHistoryRequest{WaitNewEvent: false}, 149 numBlockedRequests: 3, 150 perInstanceLimit: 2, 151 globalLimit: 4, 152 memberCounter: quotastest.NewFakeMemberCounter(2), 153 methodName: "/temporal.api.workflowservice.v1.WorkflowService/GetWorkflowExecutionHistory", 154 tokens: map[string]int{ 155 "GetWorkflowExecutionHistory": 1, 156 }, 157 expectRateLimit: false, 158 }, 159 } { 160 tc := tc 161 t.Run(tc.name, func(t *testing.T) { 162 t.Parallel() 163 tc.run(t) 164 }) 165 } 166 } 167 168 // run the test case by simulating a bunch of blocked pollers, sending a final request, and verifying that it is either 169 // rate limited or not. 170 func (tc *nsCountLimitTestCase) run(t *testing.T) { 171 ctrl := gomock.NewController(t) 172 handler := tc.createRequestHandler() 173 interceptor := tc.createInterceptor(ctrl) 174 // Spawn a bunch of blocked requests in the background. 175 tc.spawnBlockedRequests(handler, interceptor) 176 177 // With all the blocked requests in flight, send the final request and verify whether it is rate limited or not. 178 _, err := interceptor.Intercept(context.Background(), tc.request, &grpc.UnaryServerInfo{ 179 FullMethod: tc.methodName, 180 }, noopHandler) 181 182 if tc.expectRateLimit { 183 assert.ErrorContains(t, err, "namespace concurrent poller limit exceeded") 184 } else { 185 assert.NoError(t, err) 186 } 187 188 // Clean up by unblocking all the requests. 189 handler.Unblock() 190 191 for i := 0; i < tc.numBlockedRequests-1; i++ { 192 assert.NoError(t, <-handler.errs) 193 } 194 } 195 196 func (tc *nsCountLimitTestCase) createRequestHandler() *testRequestHandler { 197 return &testRequestHandler{ 198 started: make(chan struct{}), 199 respond: make(chan struct{}), 200 errs: make(chan error, tc.numBlockedRequests-1), 201 } 202 } 203 204 // spawnBlockedRequests sends a bunch of requests to the interceptor which will block until signaled. 205 func (tc *nsCountLimitTestCase) spawnBlockedRequests( 206 handler *testRequestHandler, 207 interceptor *ConcurrentRequestLimitInterceptor, 208 ) { 209 for i := 0; i < tc.numBlockedRequests-1; i++ { 210 go func() { 211 _, err := interceptor.Intercept(context.Background(), tc.request, &grpc.UnaryServerInfo{ 212 FullMethod: tc.methodName, 213 }, handler.Handle) 214 handler.errs <- err 215 }() 216 } 217 218 for i := 0; i < tc.numBlockedRequests-1; i++ { 219 <-handler.started 220 } 221 } 222 223 func (tc *nsCountLimitTestCase) createInterceptor(ctrl *gomock.Controller) *ConcurrentRequestLimitInterceptor { 224 registry := namespace.NewMockRegistry(ctrl) 225 registry.EXPECT().GetNamespace(gomock.Any()).Return(&namespace.Namespace{}, nil).AnyTimes() 226 227 interceptor := NewConcurrentRequestLimitInterceptor( 228 registry, 229 tc.memberCounter, 230 log.NewNoopLogger(), 231 dynamicconfig.GetIntPropertyFilteredByNamespace(tc.perInstanceLimit), 232 dynamicconfig.GetIntPropertyFilteredByNamespace(tc.globalLimit), 233 tc.tokens, 234 ) 235 236 return interceptor 237 } 238 239 // noopHandler is a grpc.UnaryHandler which does nothing. 240 func noopHandler(context.Context, interface{}) (interface{}, error) { 241 return nil, nil 242 } 243 244 // testRequestHandler provides a grpc.UnaryHandler which signals when it starts and does not respond until signaled. 245 type testRequestHandler struct { 246 started chan struct{} 247 respond chan struct{} 248 errs chan error 249 } 250 251 func (h testRequestHandler) Unblock() { 252 close(h.respond) 253 } 254 255 // Handle signals that the request has started and then blocks until signaled to respond. 256 func (h testRequestHandler) Handle(context.Context, interface{}) (interface{}, error) { 257 h.started <- struct{}{} 258 <-h.respond 259 260 return nil, nil 261 }