go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/limiter/limiter.go (about) 1 // Copyright 2020 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package limiter 16 17 import ( 18 "context" 19 "fmt" 20 "sync/atomic" 21 22 "go.chromium.org/luci/common/errors" 23 "go.chromium.org/luci/common/logging" 24 "go.chromium.org/luci/common/tsmon/field" 25 "go.chromium.org/luci/common/tsmon/metric" 26 ) 27 28 // ErrLimitReached is returned by CheckRequest when some limit is reached. 29 var ErrLimitReached = errors.New("the server limit reached") 30 31 var ( 32 // Number of in-flight requests. 33 concurrencyCurGauge = metric.NewInt( 34 "server/limiter/concurrency/cur", 35 "Number of requests being processed right now.", 36 nil, 37 field.String("limiter"), // name of the limiter that reports the metric 38 ) 39 40 // Configured maximum number of in-flight requests. 41 concurrencyMaxGauge = metric.NewInt( 42 "server/limiter/concurrency/max", 43 "A configured limit on a number of concurrently processed requests.", 44 nil, 45 field.String("limiter"), // name of the limiter that reports the metric 46 ) 47 48 // Counter with rejected requests. 49 rejectedCounter = metric.NewCounter( 50 "server/limiter/rejected", 51 "Number of rejected requests.", 52 nil, 53 field.String("limiter"), // name of the limiter that did the rejection 54 field.String("call"), // an RPC or an endpoint being called (if known) 55 field.String("peer"), // who's making the request (if known), see also peer.go. 56 field.String("reason")) // why the request was rejected 57 ) 58 59 // Options contains configuration of a single Limiter instance. 60 type Options struct { 61 Name string // used for metric fields, logs and error messages 62 AdvisoryMode bool // if true, don't actually reject requests, just log 63 MaxConcurrentRequests int64 // a hard limit on a number of concurrent requests 64 } 65 66 // Limiter is a stateful runtime object that decides whether to accept or reject 67 // requests based on the current load (calculated from requests that went 68 // through it). 69 // 70 // It is also responsible for maintaining monitoring metrics that describe its 71 // state and what requests it rejects. 72 // 73 // May be running in advisory mode, in which it will do all the usual processing 74 // and logging, but won't actually reject the requests. 75 // 76 // All methods are safe for concurrent use. 77 type Limiter struct { 78 opts Options // options passed to New, as is 79 titleForLog string // how the limiter is named in logs and error replies 80 concurrency int64 // atomic int with number of current in-flight requests 81 } 82 83 // RequestInfo holds information about a single inbound request. 84 // 85 // Used by the limiter to decide whether to accept or reject the request. 86 // 87 // Pretty sparse now, but in the future will contain fields like cost, a QoS 88 // class and an attempt count, which will help the limiter to decide what 89 // requests to drop. 90 // 91 // Fields `CallLabel` and `PeerLabel` are intentionally pretty generic, since 92 // they will be used only as labels in internal maps and metric fields. Their 93 // internal structure and meaning are not important to the limiter, but the 94 // cardinality of the set of their possible values must be reasonably bounded. 95 type RequestInfo struct { 96 CallLabel string // an RPC or an endpoint being called (if known) 97 PeerLabel string // who's making the request (if known), see also peer.go 98 } 99 100 // New returns a new limiter. 101 // 102 // Returns an error if options are invalid. 103 func New(opts Options) (*Limiter, error) { 104 if opts.MaxConcurrentRequests <= 0 { 105 return nil, errors.New("max concurrent requests must be positive") 106 } 107 return &Limiter{ 108 opts: opts, 109 titleForLog: fmt.Sprintf("%s<=%d", opts.Name, opts.MaxConcurrentRequests), 110 }, nil 111 } 112 113 // ReportMetrics updates all limiter's gauge metrics to match the current state. 114 // 115 // Must be called periodically (at least once per every metrics flush). 116 func (l *Limiter) ReportMetrics(ctx context.Context) { 117 concurrencyCurGauge.Set(ctx, atomic.LoadInt64(&l.concurrency), l.opts.Name) 118 concurrencyMaxGauge.Set(ctx, l.opts.MaxConcurrentRequests, l.opts.Name) 119 } 120 121 // CheckRequest should be called before processing a request. 122 // 123 // If it returns an error, the request should be declined as soon as possible 124 // with Unavailable/HTTP 503 status and the given error (which is an annotated 125 // ErrLimitReached). 126 // 127 // If it succeeds, the request should be processed as usual, and the returned 128 // callback called afterwards to notify the limiter the processing is done. 129 func (l *Limiter) CheckRequest(ctx context.Context, ri *RequestInfo) (done func(), err error) { 130 // TODO(vadimsh): This is the simplest limiter implementation possible. It 131 // will likely learn more tricks once we understand what features we need. 132 for { 133 cur := atomic.LoadInt64(&l.concurrency) 134 if cur >= l.opts.MaxConcurrentRequests && !l.opts.AdvisoryMode { 135 return nil, l.reject(ctx, ri, "max concurrency") 136 } 137 if !atomic.CompareAndSwapInt64(&l.concurrency, cur, cur+1) { 138 continue // race, try again 139 } 140 // Now that we have definitely grabbed the execution slot, report the 141 // advisory rejection message. Doing it sooner may result in duplications. 142 if cur >= l.opts.MaxConcurrentRequests && l.opts.AdvisoryMode { 143 _ = l.reject(ctx, ri, "max concurrency") // actually ignore the error 144 } 145 return func() { atomic.AddInt64(&l.concurrency, -1) }, nil 146 } 147 } 148 149 // reject is called when the request is rejected (either for real or in 150 // advisory mode). 151 // 152 // It updates metrics and logs and returns an annotated ErrLimitReached error. 153 func (l *Limiter) reject(ctx context.Context, ri *RequestInfo, reason string) error { 154 rejectedCounter.Add(ctx, 1, l.opts.Name, ri.CallLabel, ri.PeerLabel, reason) 155 if l.opts.AdvisoryMode { 156 logging.Warningf(ctx, "limiter %q in advisory mode: the request hit the %s limit", l.titleForLog, reason) 157 } else { 158 logging.Errorf(ctx, "limiter %q: the request hit the %s limit", l.titleForLog, reason) 159 } 160 return errors.Annotate(ErrLimitReached, "limiter %q: %s limit", l.titleForLog, reason).Err() 161 }