github.com/m3db/m3@v1.5.0/src/dbnode/storage/limits/query_limits.go (about) 1 // Copyright (c) 2021 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package limits 22 23 import ( 24 "fmt" 25 "sync" 26 "time" 27 28 "github.com/uber-go/tally" 29 "go.uber.org/atomic" 30 "go.uber.org/zap" 31 32 xerrors "github.com/m3db/m3/src/x/errors" 33 "github.com/m3db/m3/src/x/instrument" 34 ) 35 36 const ( 37 disabledLimitValue = 0 38 defaultLookback = time.Second * 15 39 ) 40 41 type queryLimits struct { 42 docsLimit *lookbackLimit 43 bytesReadLimit *lookbackLimit 44 aggregatedDocsLimit *lookbackLimit 45 } 46 47 type lookbackLimit struct { 48 name string 49 started bool 50 options LookbackLimitOptions 51 metrics lookbackLimitMetrics 52 logger *zap.Logger 53 recent *atomic.Int64 54 stopCh chan struct{} 55 stoppedCh chan struct{} 56 lock sync.RWMutex 57 iOpts instrument.Options 58 } 59 60 type lookbackLimitMetrics struct { 61 optionsLimit tally.Gauge 62 optionsLookback tally.Gauge 63 recentCount tally.Gauge 64 recentMax tally.Gauge 65 total tally.Counter 66 exceeded tally.Counter 67 68 sourceLogger SourceLogger 69 } 70 71 var ( 72 _ QueryLimits = (*queryLimits)(nil) 73 _ LookbackLimit = (*lookbackLimit)(nil) 74 ) 75 76 // DefaultLookbackLimitOptions returns a new query limits manager. 77 func DefaultLookbackLimitOptions() LookbackLimitOptions { 78 return LookbackLimitOptions{ 79 // Default to no limit. 80 Limit: disabledLimitValue, 81 Lookback: defaultLookback, 82 } 83 } 84 85 // DefaultLimitsOptions is the set of default limits options. 86 func DefaultLimitsOptions(iOpts instrument.Options) Options { 87 return NewOptions(). 88 SetInstrumentOptions(iOpts). 89 SetBytesReadLimitOpts(DefaultLookbackLimitOptions()). 90 SetDocsLimitOpts(DefaultLookbackLimitOptions()). 91 SetAggregateDocsLimitOpts(DefaultLookbackLimitOptions()). 92 SetDiskSeriesReadLimitOpts(DefaultLookbackLimitOptions()) 93 } 94 95 // NewQueryLimits returns a new query limits manager. 96 func NewQueryLimits(options Options) (QueryLimits, error) { 97 if err := options.Validate(); err != nil { 98 return nil, err 99 } 100 101 var ( 102 iOpts = options.InstrumentOptions() 103 docsLimitOpts = options.DocsLimitOpts() 104 bytesReadLimitOpts = options.BytesReadLimitOpts() 105 aggDocsLimitOpts = options.AggregateDocsLimitOpts() 106 sourceLoggerBuilder = options.SourceLoggerBuilder() 107 108 docsMatched = "docs-matched" 109 bytesRead = "disk-bytes-read" 110 aggregateMatched = "aggregate-matched" 111 docsLimit = newLookbackLimit(limitNames{ 112 limitName: docsMatched, 113 metricName: docsMatched, 114 metricType: "fetch", 115 }, docsLimitOpts, iOpts, sourceLoggerBuilder) 116 bytesReadLimit = newLookbackLimit(limitNames{ 117 limitName: bytesRead, 118 metricName: bytesRead, 119 metricType: "read", 120 }, bytesReadLimitOpts, iOpts, sourceLoggerBuilder) 121 aggregatedDocsLimit = newLookbackLimit(limitNames{ 122 limitName: aggregateMatched, 123 metricName: docsMatched, 124 metricType: "aggregate", 125 }, aggDocsLimitOpts, iOpts, sourceLoggerBuilder) 126 ) 127 128 return &queryLimits{ 129 docsLimit: docsLimit, 130 bytesReadLimit: bytesReadLimit, 131 aggregatedDocsLimit: aggregatedDocsLimit, 132 }, nil 133 } 134 135 // NewLookbackLimit returns a new lookback limit. 136 func NewLookbackLimit( 137 name string, 138 opts LookbackLimitOptions, 139 instrumentOpts instrument.Options, 140 sourceLoggerBuilder SourceLoggerBuilder, 141 ) LookbackLimit { 142 return newLookbackLimit(limitNames{ 143 limitName: name, 144 metricName: name, 145 metricType: name, 146 }, opts, instrumentOpts, sourceLoggerBuilder) 147 } 148 149 type limitNames struct { 150 limitName string 151 metricName string 152 metricType string 153 } 154 155 func newLookbackLimit( 156 limitNames limitNames, 157 opts LookbackLimitOptions, 158 instrumentOpts instrument.Options, 159 sourceLoggerBuilder SourceLoggerBuilder, 160 ) *lookbackLimit { 161 metrics := newLookbackLimitMetrics( 162 limitNames, 163 instrumentOpts, 164 sourceLoggerBuilder, 165 ) 166 167 return &lookbackLimit{ 168 name: limitNames.limitName, 169 options: opts, 170 metrics: metrics, 171 logger: instrumentOpts.Logger(), 172 recent: atomic.NewInt64(0), 173 stopCh: make(chan struct{}), 174 stoppedCh: make(chan struct{}), 175 iOpts: instrumentOpts, 176 } 177 } 178 179 func newLookbackLimitMetrics( 180 limitNames limitNames, 181 instrumentOpts instrument.Options, 182 sourceLoggerBuilder SourceLoggerBuilder, 183 ) lookbackLimitMetrics { 184 metricName := limitNames.metricName 185 loggerScope := instrumentOpts.MetricsScope().Tagged(map[string]string{ 186 "type": limitNames.metricType, 187 }) 188 189 var ( 190 loggerOpts = instrumentOpts.SetMetricsScope(loggerScope) 191 metricScope = loggerScope.SubScope("query-limit") 192 ) 193 194 return lookbackLimitMetrics{ 195 optionsLimit: metricScope.Gauge(fmt.Sprintf("current-limit-%s", metricName)), 196 optionsLookback: metricScope.Gauge(fmt.Sprintf("current-lookback-%s", metricName)), 197 recentCount: metricScope.Gauge(fmt.Sprintf("recent-count-%s", metricName)), 198 recentMax: metricScope.Gauge(fmt.Sprintf("recent-max-%s", metricName)), 199 total: metricScope.Counter(fmt.Sprintf("total-%s", metricName)), 200 exceeded: metricScope.Tagged(map[string]string{"limit": metricName}).Counter("exceeded"), 201 202 sourceLogger: sourceLoggerBuilder.NewSourceLogger(metricName, loggerOpts), 203 } 204 } 205 206 func (q *queryLimits) FetchDocsLimit() LookbackLimit { 207 return q.docsLimit 208 } 209 210 func (q *queryLimits) BytesReadLimit() LookbackLimit { 211 return q.bytesReadLimit 212 } 213 214 func (q *queryLimits) AggregateDocsLimit() LookbackLimit { 215 return q.aggregatedDocsLimit 216 } 217 218 func (q *queryLimits) Start() { 219 q.docsLimit.Start() 220 q.bytesReadLimit.Start() 221 q.aggregatedDocsLimit.Start() 222 } 223 224 func (q *queryLimits) Stop() { 225 q.docsLimit.Stop() 226 q.bytesReadLimit.Stop() 227 q.aggregatedDocsLimit.Stop() 228 } 229 230 func (q *queryLimits) AnyFetchExceeded() error { 231 if err := q.docsLimit.exceeded(); err != nil { 232 return err 233 } 234 235 return q.bytesReadLimit.exceeded() 236 } 237 238 func (q *lookbackLimit) Options() LookbackLimitOptions { 239 q.lock.RLock() 240 o := q.options 241 q.lock.RUnlock() 242 return o 243 } 244 245 // Update updates the limit. 246 func (q *lookbackLimit) Update(opts LookbackLimitOptions) error { 247 if err := opts.validate(); err != nil { 248 return err 249 } 250 251 q.lock.Lock() 252 defer q.lock.Unlock() 253 254 old := q.options 255 q.options = opts 256 257 // If the lookback changed, replace the background goroutine that manages the periodic resetting. 258 if q.options.Lookback != old.Lookback { 259 q.stop() 260 q.start() 261 } 262 263 q.logger.Info("query limit options updated", 264 zap.String("name", q.name), 265 zap.Any("new", opts), 266 zap.Any("old", old)) 267 268 return nil 269 } 270 271 // Inc increments the current value and returns an error if above the limit. 272 func (q *lookbackLimit) Inc(val int, source []byte) error { 273 if val < 0 { 274 return fmt.Errorf("invalid negative query limit inc %d", val) 275 } 276 if val == 0 { 277 return q.exceeded() 278 } 279 280 // Add the new stats to the global state. 281 valI64 := int64(val) 282 recent := q.recent.Add(valI64) 283 284 // Update metrics. 285 q.metrics.recentCount.Update(float64(recent)) 286 q.metrics.total.Inc(valI64) 287 q.metrics.sourceLogger.LogSourceValue(valI64, source) 288 289 // Enforce limit (if specified). 290 return q.checkLimit(recent) 291 } 292 293 func (q *lookbackLimit) exceeded() error { 294 return q.checkLimit(q.recent.Load()) 295 } 296 297 func (q *lookbackLimit) checkLimit(recent int64) error { 298 q.lock.RLock() 299 currentOpts := q.options 300 q.lock.RUnlock() 301 302 if currentOpts.ForceExceeded { 303 q.metrics.exceeded.Inc(1) 304 305 return xerrors.NewInvalidParamsError(NewQueryLimitExceededError(fmt.Sprintf( 306 "query aborted due to forced limit: name=%s", q.name))) 307 } 308 309 if currentOpts.Limit == disabledLimitValue { 310 return nil 311 } 312 313 if recent >= currentOpts.Limit { 314 q.metrics.exceeded.Inc(1) 315 316 return xerrors.NewInvalidParamsError(NewQueryLimitExceededError(fmt.Sprintf( 317 "query aborted due to limit: name=%s, limit=%d, current=%d, within=%s", 318 q.name, q.options.Limit, recent, q.options.Lookback))) 319 } 320 321 return nil 322 } 323 324 func (q *lookbackLimit) Start() { 325 // Lock on explicit start to avoid any collision with asynchronous updating 326 // which will call stop/start if the lookback has changed. 327 q.lock.Lock() 328 defer q.lock.Unlock() 329 q.start() 330 } 331 332 func (q *lookbackLimit) Stop() { 333 // Lock on explicit stop to avoid any collision with asynchronous updating 334 // which will call stop/start if the lookback has changed. 335 q.lock.Lock() 336 defer q.lock.Unlock() 337 q.stop() 338 } 339 340 func (q *lookbackLimit) start() { 341 q.started = true 342 ticker := time.NewTicker(q.options.Lookback) 343 go func() { 344 q.logger.Info("query limit interval started", zap.String("name", q.name)) 345 for { 346 select { 347 case <-ticker.C: 348 q.reset() 349 case <-q.stopCh: 350 ticker.Stop() 351 q.stoppedCh <- struct{}{} 352 return 353 } 354 } 355 }() 356 357 q.metrics.optionsLimit.Update(float64(q.options.Limit)) 358 q.metrics.optionsLookback.Update(q.options.Lookback.Seconds()) 359 } 360 361 func (q *lookbackLimit) stop() { 362 if !q.started { 363 // NB: this lookback limit has not yet been started. 364 instrument.EmitAndLogInvariantViolation(q.iOpts, func(l *zap.Logger) { 365 l.With( 366 zap.Any("limit_name", q.name), 367 ).Error("cannot stop non-started lookback limit") 368 }) 369 return 370 } 371 372 close(q.stopCh) 373 <-q.stoppedCh 374 q.stopCh = make(chan struct{}) 375 q.stoppedCh = make(chan struct{}) 376 377 q.logger.Info("query limit interval stopped", zap.String("name", q.name)) 378 } 379 380 func (q *lookbackLimit) current() int64 { 381 return q.recent.Load() 382 } 383 384 func (q *lookbackLimit) reset() { 385 // Update peak gauge only on resets so it only tracks 386 // the peak values for each lookback period. 387 recent := q.recent.Load() 388 389 q.metrics.recentMax.Update(float64(recent)) 390 // Update the standard recent gauge to reflect drop back to zero. 391 q.metrics.recentCount.Update(0) 392 q.recent.Store(0) 393 } 394 395 // Equals returns true if the other options match the current. 396 func (opts LookbackLimitOptions) Equals(other LookbackLimitOptions) bool { 397 return opts.Limit == other.Limit && 398 opts.Lookback == other.Lookback && 399 opts.ForceExceeded == other.ForceExceeded && 400 opts.ForceWaited == other.ForceWaited 401 } 402 403 func (opts LookbackLimitOptions) validate() error { 404 if opts.Limit < 0 { 405 return fmt.Errorf("query limit requires limit >= 0 (%d)", opts.Limit) 406 } 407 if opts.Lookback <= 0 { 408 return fmt.Errorf("query limit requires lookback > 0 (%d)", opts.Lookback) 409 } 410 return nil 411 }