go.temporal.io/server@v1.23.0/common/rpc/interceptor/concurrent_request_limit.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 "sync" 30 "sync/atomic" 31 32 enumspb "go.temporal.io/api/enums/v1" 33 "go.temporal.io/api/serviceerror" 34 "go.temporal.io/api/workflowservice/v1" 35 "go.temporal.io/server/common/quotas" 36 "google.golang.org/grpc" 37 38 "go.temporal.io/server/common/log" 39 "go.temporal.io/server/common/metrics" 40 "go.temporal.io/server/common/namespace" 41 ) 42 43 type ( 44 // ConcurrentRequestLimitInterceptor intercepts requests to the server and enforces a limit on the number of 45 // requests that can be in-flight at any given time, according to the configured quotas. 46 ConcurrentRequestLimitInterceptor struct { 47 namespaceRegistry namespace.Registry 48 logger log.Logger 49 quotaCalculator quotas.ClusterAwareNamespaceSpecificQuotaCalculator 50 // tokens is a map of method name to the number of tokens that should be consumed for that method. If there is 51 // no entry for a method, then no tokens will be consumed, so the method will not be limited. 52 tokens map[string]int 53 54 sync.Mutex 55 activeTokensCount map[string]*int32 56 } 57 ) 58 59 var ( 60 _ grpc.UnaryServerInterceptor = (*ConcurrentRequestLimitInterceptor)(nil).Intercept 61 62 ErrNamespaceCountLimitServerBusy = serviceerror.NewResourceExhausted(enumspb.RESOURCE_EXHAUSTED_CAUSE_CONCURRENT_LIMIT, "namespace concurrent poller limit exceeded") 63 ) 64 65 func NewConcurrentRequestLimitInterceptor( 66 namespaceRegistry namespace.Registry, 67 memberCounter quotas.MemberCounter, 68 logger log.Logger, 69 perInstanceQuota func(ns string) int, 70 globalQuota func(ns string) int, 71 tokens map[string]int, 72 ) *ConcurrentRequestLimitInterceptor { 73 return &ConcurrentRequestLimitInterceptor{ 74 namespaceRegistry: namespaceRegistry, 75 logger: logger, 76 quotaCalculator: quotas.ClusterAwareNamespaceSpecificQuotaCalculator{ 77 MemberCounter: memberCounter, 78 PerInstanceQuota: perInstanceQuota, 79 GlobalQuota: globalQuota, 80 }, 81 tokens: tokens, 82 activeTokensCount: make(map[string]*int32), 83 } 84 } 85 86 func (ni *ConcurrentRequestLimitInterceptor) Intercept( 87 ctx context.Context, 88 req interface{}, 89 info *grpc.UnaryServerInfo, 90 handler grpc.UnaryHandler, 91 ) (interface{}, error) { 92 _, methodName := SplitMethodName(info.FullMethod) 93 // token will default to 0 94 token := ni.tokens[methodName] 95 96 if token != 0 { 97 // for GetWorkflowExecutionHistoryRequest, we only care about long poll requests 98 longPollReq, ok := req.(*workflowservice.GetWorkflowExecutionHistoryRequest) 99 if ok && !longPollReq.WaitNewEvent { 100 // ignore non-long-poll GetHistory calls. 101 token = 0 102 } 103 } 104 105 if token != 0 { 106 nsName := MustGetNamespaceName(ni.namespaceRegistry, req) 107 counter := ni.counter(nsName, methodName) 108 count := atomic.AddInt32(counter, int32(token)) 109 defer atomic.AddInt32(counter, -int32(token)) 110 111 handler := GetMetricsHandlerFromContext(ctx, ni.logger) 112 metrics.ServicePendingRequests.With(handler).Record(float64(count)) 113 114 // frontend.namespaceCount is applied per poller type temporarily to prevent 115 // one poller type to take all token waiting in the long poll. 116 if float64(count) > ni.quotaCalculator.GetQuota(nsName.String()) { 117 return nil, ErrNamespaceCountLimitServerBusy 118 } 119 } 120 121 return handler(ctx, req) 122 } 123 124 func (ni *ConcurrentRequestLimitInterceptor) counter( 125 namespace namespace.Name, 126 methodName string, 127 ) *int32 { 128 key := ni.getTokenKey(namespace, methodName) 129 130 ni.Lock() 131 defer ni.Unlock() 132 133 counter, ok := ni.activeTokensCount[key] 134 if !ok { 135 counter = new(int32) 136 ni.activeTokensCount[key] = counter 137 } 138 return counter 139 } 140 141 func (ni *ConcurrentRequestLimitInterceptor) getTokenKey( 142 namespace namespace.Name, 143 methodName string, 144 ) string { 145 return namespace.String() + "/" + methodName 146 }