github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/limits/query_limits_test.go (about) 1 // Copyright (c) 2020 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 "testing" 26 "time" 27 28 xclock "github.com/m3db/m3/src/x/clock" 29 xerrors "github.com/m3db/m3/src/x/errors" 30 "github.com/m3db/m3/src/x/instrument" 31 "github.com/m3db/m3/src/x/tallytest" 32 33 "github.com/stretchr/testify/assert" 34 "github.com/stretchr/testify/require" 35 "github.com/uber-go/tally" 36 ) 37 38 func testQueryLimitOptions( 39 docOpts LookbackLimitOptions, 40 bytesOpts LookbackLimitOptions, 41 seriesOpts LookbackLimitOptions, 42 aggDocsOpts LookbackLimitOptions, 43 iOpts instrument.Options, 44 ) Options { 45 return NewOptions(). 46 SetDocsLimitOpts(docOpts). 47 SetBytesReadLimitOpts(bytesOpts). 48 SetDiskSeriesReadLimitOpts(seriesOpts). 49 SetAggregateDocsLimitOpts(aggDocsOpts). 50 SetInstrumentOptions(iOpts) 51 } 52 53 func TestQueryLimits(t *testing.T) { 54 l := int64(1) 55 docOpts := LookbackLimitOptions{ 56 Limit: l, 57 Lookback: time.Second, 58 } 59 bytesOpts := LookbackLimitOptions{ 60 Limit: l, 61 Lookback: time.Second, 62 } 63 seriesOpts := LookbackLimitOptions{ 64 Limit: l, 65 Lookback: time.Second, 66 } 67 aggOpts := LookbackLimitOptions{ 68 Limit: l, 69 Lookback: time.Second, 70 } 71 opts := testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions()) 72 queryLimits, err := NewQueryLimits(opts) 73 require.NoError(t, err) 74 require.NotNil(t, queryLimits) 75 76 // No error yet. 77 require.NoError(t, queryLimits.AnyFetchExceeded()) 78 79 // Limit from docs. 80 require.Error(t, queryLimits.FetchDocsLimit().Inc(2, nil)) 81 err = queryLimits.AnyFetchExceeded() 82 require.Error(t, err) 83 require.True(t, xerrors.IsInvalidParams(err)) 84 require.True(t, IsQueryLimitExceededError(err)) 85 86 opts = testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions()) 87 queryLimits, err = NewQueryLimits(opts) 88 require.NoError(t, err) 89 require.NotNil(t, queryLimits) 90 91 // No error yet. 92 err = queryLimits.AnyFetchExceeded() 93 require.NoError(t, err) 94 95 // Limit from bytes. 96 require.Error(t, queryLimits.BytesReadLimit().Inc(2, nil)) 97 err = queryLimits.AnyFetchExceeded() 98 require.Error(t, err) 99 require.True(t, xerrors.IsInvalidParams(err)) 100 require.True(t, IsQueryLimitExceededError(err)) 101 102 opts = testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions()) 103 queryLimits, err = NewQueryLimits(opts) 104 require.NoError(t, err) 105 require.NotNil(t, queryLimits) 106 107 // No error yet. 108 err = queryLimits.AnyFetchExceeded() 109 require.NoError(t, err) 110 111 // Limit from aggregate does not trip any fetched exceeded. 112 require.Error(t, queryLimits.AggregateDocsLimit().Inc(2, nil)) 113 err = queryLimits.AnyFetchExceeded() 114 require.NoError(t, err) 115 require.NotNil(t, queryLimits) 116 117 opts = testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions()) 118 queryLimits, err = NewQueryLimits(opts) 119 require.NoError(t, err) 120 require.NotNil(t, queryLimits) 121 122 // No error yet. 123 err = queryLimits.AnyFetchExceeded() 124 require.NoError(t, err) 125 } 126 127 func TestLookbackLimit(t *testing.T) { 128 for _, test := range []struct { 129 name string 130 limit int64 131 forceExceeded bool 132 }{ 133 {name: "no limit", limit: 0}, 134 {name: "limit", limit: 5}, 135 {name: "force exceeded limit", limit: 5, forceExceeded: true}, 136 } { 137 t.Run(test.name, func(t *testing.T) { 138 scope := tally.NewTestScope("", nil) 139 iOpts := instrument.NewOptions().SetMetricsScope(scope) 140 opts := LookbackLimitOptions{ 141 Limit: test.limit, 142 Lookback: time.Millisecond * 100, 143 ForceExceeded: test.forceExceeded, 144 } 145 name := "test" 146 limit := newLookbackLimit(limitNames{ 147 limitName: name, 148 metricName: name, 149 metricType: name, 150 }, opts, iOpts, &sourceLoggerBuilder{}) 151 152 require.Equal(t, int64(0), limit.current()) 153 154 var exceededCount int64 155 err := limit.exceeded() 156 if test.limit >= 0 && !test.forceExceeded { 157 require.NoError(t, err) 158 } else { 159 require.Error(t, err) 160 exceededCount++ 161 } 162 163 // Validate ascending while checking limits. 164 exceededCount += verifyLimit(t, limit, 3, test.limit, test.forceExceeded) 165 require.Equal(t, int64(3), limit.current()) 166 verifyMetrics(t, scope, name, 3, 0, 3, exceededCount) 167 168 exceededCount += verifyLimit(t, limit, 2, test.limit, test.forceExceeded) 169 require.Equal(t, int64(5), limit.current()) 170 verifyMetrics(t, scope, name, 5, 0, 5, exceededCount) 171 172 exceededCount += verifyLimit(t, limit, 1, test.limit, test.forceExceeded) 173 require.Equal(t, int64(6), limit.current()) 174 verifyMetrics(t, scope, name, 6, 0, 6, exceededCount) 175 176 exceededCount += verifyLimit(t, limit, 4, test.limit, test.forceExceeded) 177 require.Equal(t, int64(10), limit.current()) 178 verifyMetrics(t, scope, name, 10, 0, 10, exceededCount) 179 180 // Validate first reset. 181 limit.reset() 182 require.Equal(t, int64(0), limit.current()) 183 verifyMetrics(t, scope, name, 0, 10, 10, exceededCount) 184 185 // Validate ascending again post-reset. 186 exceededCount += verifyLimit(t, limit, 2, test.limit, test.forceExceeded) 187 require.Equal(t, int64(2), limit.current()) 188 verifyMetrics(t, scope, name, 2, 10, 12, exceededCount) 189 190 exceededCount += verifyLimit(t, limit, 5, test.limit, test.forceExceeded) 191 require.Equal(t, int64(7), limit.current()) 192 verifyMetrics(t, scope, name, 7, 10, 17, exceededCount) 193 194 // Validate second reset. 195 limit.reset() 196 197 require.Equal(t, int64(0), limit.current()) 198 verifyMetrics(t, scope, name, 0, 7, 17, exceededCount) 199 200 // Validate consecutive reset (ensure peak goes to zero). 201 limit.reset() 202 203 require.Equal(t, int64(0), limit.current()) 204 verifyMetrics(t, scope, name, 0, 0, 17, exceededCount) 205 206 limit.reset() 207 208 opts.Limit = 0 209 require.NoError(t, limit.Update(opts)) 210 211 exceededCount += verifyLimit(t, limit, 0, opts.Limit, test.forceExceeded) 212 require.Equal(t, int64(0), limit.current()) 213 214 opts.Limit = 2 215 require.NoError(t, limit.Update(opts)) 216 217 exceededCount += verifyLimit(t, limit, 1, opts.Limit, test.forceExceeded) 218 require.Equal(t, int64(1), limit.current()) 219 verifyMetrics(t, scope, name, 1, 0, 18, exceededCount) 220 221 exceededCount += verifyLimit(t, limit, 1, opts.Limit, test.forceExceeded) 222 require.Equal(t, int64(2), limit.current()) 223 verifyMetrics(t, scope, name, 2, 0, 19, exceededCount) 224 225 exceededCount += verifyLimit(t, limit, 1, opts.Limit, test.forceExceeded) 226 require.Equal(t, int64(3), limit.current()) 227 verifyMetrics(t, scope, name, 3, 0, 20, exceededCount) 228 }) 229 } 230 } 231 232 func verifyLimit(t *testing.T, limit *lookbackLimit, inc int, expectedLimit int64, forceExceeded bool) int64 { 233 var exceededCount int64 234 err := limit.Inc(inc, nil) 235 if (expectedLimit == 0 || limit.current() < expectedLimit) && !forceExceeded { 236 require.NoError(t, err) 237 } else { 238 require.Error(t, err) 239 require.True(t, xerrors.IsInvalidParams(err)) 240 require.True(t, IsQueryLimitExceededError(err)) 241 exceededCount++ 242 } 243 244 err = limit.exceeded() 245 if (expectedLimit == 0 || limit.current() < expectedLimit) && !forceExceeded { 246 require.NoError(t, err) 247 } else { 248 require.Error(t, err) 249 require.True(t, xerrors.IsInvalidParams(err)) 250 require.True(t, IsQueryLimitExceededError(err)) 251 exceededCount++ 252 } 253 return exceededCount 254 } 255 256 func TestLookbackReset(t *testing.T) { 257 scope := tally.NewTestScope("", nil) 258 iOpts := instrument.NewOptions().SetMetricsScope(scope) 259 opts := LookbackLimitOptions{ 260 Limit: 5, 261 Lookback: time.Millisecond * 100, 262 } 263 name := "test" 264 limit := newLookbackLimit(limitNames{ 265 limitName: name, 266 metricName: name, 267 metricType: name, 268 }, opts, iOpts, &sourceLoggerBuilder{}) 269 270 err := limit.Inc(3, nil) 271 require.NoError(t, err) 272 require.Equal(t, int64(3), limit.current()) 273 274 limit.start() 275 defer limit.stop() 276 time.Sleep(opts.Lookback * 2) 277 278 success := xclock.WaitUntil(func() bool { 279 return limit.current() == 0 280 }, 5*time.Second) 281 require.True(t, success, "did not eventually reset to zero") 282 } 283 284 func TestValidateLookbackLimitOptions(t *testing.T) { 285 for _, test := range []struct { 286 name string 287 max int64 288 lookback time.Duration 289 expectError bool 290 }{ 291 { 292 name: "valid lookback without limit", 293 max: 0, 294 lookback: time.Millisecond, 295 }, 296 { 297 name: "valid lookback with valid limit", 298 max: 1, 299 lookback: time.Millisecond, 300 }, 301 { 302 name: "negative lookback", 303 max: 0, 304 lookback: -time.Millisecond, 305 expectError: true, 306 }, 307 { 308 name: "zero lookback", 309 max: 0, 310 lookback: time.Duration(0), 311 expectError: true, 312 }, 313 { 314 name: "negative max", 315 max: -1, 316 lookback: time.Millisecond, 317 expectError: true, 318 }, 319 } { 320 t.Run(test.name, func(t *testing.T) { 321 err := LookbackLimitOptions{ 322 Limit: test.max, 323 Lookback: test.lookback, 324 }.validate() 325 if test.expectError { 326 require.Error(t, err) 327 } else { 328 require.NoError(t, err) 329 } 330 331 // Validate empty. 332 require.Error(t, LookbackLimitOptions{}.validate()) 333 }) 334 } 335 } 336 337 func verifyMetrics(t *testing.T, 338 scope tally.TestScope, 339 name string, 340 expectedRecent float64, 341 expectedRecentPeak float64, 342 expectedTotal int64, 343 expectedExceeded int64, 344 ) { 345 snapshot := scope.Snapshot() 346 tallytest.AssertGaugeValue( 347 t, expectedRecent, snapshot, 348 fmt.Sprintf("query-limit.recent-count-%s", name), 349 map[string]string{"type": "test"}) 350 351 tallytest.AssertGaugeValue( 352 t, expectedRecentPeak, snapshot, 353 fmt.Sprintf("query-limit.recent-max-%s", name), 354 map[string]string{"type": "test"}) 355 356 tallytest.AssertCounterValue( 357 t, expectedTotal, snapshot, 358 fmt.Sprintf("query-limit.total-%s", name), 359 map[string]string{"type": "test"}) 360 361 tallytest.AssertCounterValue( 362 t, expectedExceeded, snapshot, 363 "query-limit.exceeded", 364 map[string]string{"type": "test", "limit": "test"}) 365 } 366 367 type testLoggerRecord struct { 368 name string 369 val int64 370 source []byte 371 } 372 373 func TestSourceLogger(t *testing.T) { 374 var ( 375 scope = tally.NewTestScope("test", nil) 376 iOpts = instrument.NewOptions().SetMetricsScope(scope) 377 noLimit = LookbackLimitOptions{ 378 Limit: 0, 379 Lookback: time.Millisecond * 100, 380 } 381 382 builder = &testBuilder{records: []testLoggerRecord{}} 383 opts = testQueryLimitOptions(noLimit, noLimit, noLimit, noLimit, iOpts). 384 SetSourceLoggerBuilder(builder) 385 ) 386 387 require.NoError(t, opts.Validate()) 388 389 queryLimits, err := NewQueryLimits(opts) 390 require.NoError(t, err) 391 require.NotNil(t, queryLimits) 392 393 require.NoError(t, queryLimits.FetchDocsLimit().Inc(100, []byte("docs"))) 394 require.NoError(t, queryLimits.BytesReadLimit().Inc(200, []byte("bytes"))) 395 396 assert.Equal(t, []testLoggerRecord{ 397 {name: "docs-matched", val: 100, source: []byte("docs")}, 398 {name: "disk-bytes-read", val: 200, source: []byte("bytes")}, 399 }, builder.records) 400 401 require.NoError(t, queryLimits.AggregateDocsLimit().Inc(1000, []byte("docs"))) 402 assert.Equal(t, []testLoggerRecord{ 403 {name: "docs-matched", val: 100, source: []byte("docs")}, 404 {name: "disk-bytes-read", val: 200, source: []byte("bytes")}, 405 {name: "docs-matched", val: 1000, source: []byte("docs")}, 406 }, builder.records) 407 } 408 409 // NB: creates test logger records that share an underlying record set, 410 // differentiated by source logger name. 411 type testBuilder struct { 412 records []testLoggerRecord 413 } 414 415 var _ SourceLoggerBuilder = (*testBuilder)(nil) 416 417 func (s *testBuilder) NewSourceLogger(n string, opts instrument.Options) SourceLogger { 418 return &testSourceLogger{name: n, builder: s} 419 } 420 421 type testSourceLogger struct { 422 name string 423 builder *testBuilder 424 } 425 426 var _ SourceLogger = (*testSourceLogger)(nil) 427 428 func (l *testSourceLogger) LogSourceValue(val int64, source []byte) { 429 l.builder.records = append(l.builder.records, testLoggerRecord{ 430 name: l.name, 431 val: val, 432 source: source, 433 }) 434 }