github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/index_query_concurrent_test.go (about) 1 // +build big 2 // 3 // Copyright (c) 2018 Uber Technologies, Inc. 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in 13 // all copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 // THE SOFTWARE. 22 23 package storage 24 25 import ( 26 stdctx "context" 27 "errors" 28 "fmt" 29 "sync" 30 "testing" 31 "time" 32 33 "github.com/m3db/m3/src/dbnode/storage/index" 34 "github.com/m3db/m3/src/dbnode/storage/index/convert" 35 "github.com/m3db/m3/src/dbnode/storage/limits/permits" 36 testutil "github.com/m3db/m3/src/dbnode/test" 37 "github.com/m3db/m3/src/m3ninx/doc" 38 "github.com/m3db/m3/src/m3ninx/idx" 39 "github.com/m3db/m3/src/x/context" 40 "github.com/m3db/m3/src/x/ident" 41 "github.com/m3db/m3/src/x/instrument" 42 "github.com/m3db/m3/src/x/resource" 43 xtest "github.com/m3db/m3/src/x/test" 44 xtime "github.com/m3db/m3/src/x/time" 45 46 "github.com/fortytw2/leaktest" 47 "github.com/golang/mock/gomock" 48 opentracinglog "github.com/opentracing/opentracing-go/log" 49 "github.com/stretchr/testify/require" 50 "go.uber.org/zap" 51 ) 52 53 func TestNamespaceIndexHighConcurrentQueriesWithoutTimeouts(t *testing.T) { 54 testNamespaceIndexHighConcurrentQueries(t, 55 testNamespaceIndexHighConcurrentQueriesOptions{ 56 withTimeouts: false, 57 }) 58 } 59 60 func TestNamespaceIndexHighConcurrentQueriesWithTimeouts(t *testing.T) { 61 testNamespaceIndexHighConcurrentQueries(t, 62 testNamespaceIndexHighConcurrentQueriesOptions{ 63 withTimeouts: true, 64 }) 65 } 66 67 func TestNamespaceIndexHighConcurrentQueriesWithTimeoutsAndForceTimeout(t *testing.T) { 68 testNamespaceIndexHighConcurrentQueries(t, 69 testNamespaceIndexHighConcurrentQueriesOptions{ 70 withTimeouts: true, 71 forceTimeouts: true, 72 }) 73 } 74 75 func TestNamespaceIndexHighConcurrentQueriesWithBlockErrors(t *testing.T) { 76 testNamespaceIndexHighConcurrentQueries(t, 77 testNamespaceIndexHighConcurrentQueriesOptions{ 78 withTimeouts: false, 79 forceTimeouts: false, 80 blockErrors: true, 81 }) 82 } 83 84 type testNamespaceIndexHighConcurrentQueriesOptions struct { 85 withTimeouts bool 86 forceTimeouts bool 87 blockErrors bool 88 } 89 90 func testNamespaceIndexHighConcurrentQueries( 91 t *testing.T, 92 opts testNamespaceIndexHighConcurrentQueriesOptions, 93 ) { 94 if opts.forceTimeouts && opts.blockErrors { 95 t.Fatalf("force timeout and block errors cannot both be enabled") 96 } 97 98 ctrl := xtest.NewController(t) 99 defer ctrl.Finish() 100 101 defer leaktest.CheckTimeout(t, 2*time.Minute)() 102 103 test := newTestIndex(t, ctrl) 104 defer func() { 105 err := test.index.Close() 106 require.NoError(t, err) 107 }() 108 109 logger := test.opts.InstrumentOptions().Logger() 110 logger.Info("start high index concurrent index query test", 111 zap.Any("opts", opts)) 112 113 now := xtime.Now().Truncate(test.indexBlockSize) 114 115 min, max := now.Add(-6*test.indexBlockSize), now.Add(-test.indexBlockSize) 116 117 var timeoutValue time.Duration 118 if opts.withTimeouts { 119 timeoutValue = time.Minute 120 } 121 if opts.forceTimeouts { 122 timeoutValue = time.Second 123 } 124 125 nsIdx := test.index.(*nsIndex) 126 nsIdx.state.Lock() 127 // Make the query pool really high to improve concurrency likelihood 128 nsIdx.permitsManager = permits.NewFixedPermitsManager(1000, int64(time.Millisecond), instrument.NewOptions()) 129 130 currNow := min 131 nowLock := &sync.Mutex{} 132 nsIdx.nowFn = func() time.Time { 133 nowLock.Lock() 134 defer nowLock.Unlock() 135 return currNow.ToTime() 136 } 137 setNow := func(t xtime.UnixNano) { 138 nowLock.Lock() 139 defer nowLock.Unlock() 140 currNow = t 141 } 142 nsIdx.state.Unlock() 143 144 restoreNow := func() { 145 nsIdx.state.Lock() 146 nsIdx.nowFn = time.Now 147 nsIdx.state.Unlock() 148 } 149 150 var ( 151 idsPerBlock = 16 152 expectedResults = make(map[string]doc.Metadata) 153 blockStarts []xtime.UnixNano 154 blockIdx = -1 155 ) 156 for st := min; !st.After(max); st = st.Add(test.indexBlockSize) { 157 st := st 158 blockIdx++ 159 blockStarts = append(blockStarts, st) 160 161 mutableBlockTime := st.Add(test.indexBlockSize).Add(-1 * (test.blockSize / 2)) 162 setNow(mutableBlockTime) 163 164 var onIndexWg sync.WaitGroup 165 onIndexWg.Add(idsPerBlock) 166 onIndexSeries := doc.NewMockOnIndexSeries(ctrl) 167 onIndexSeries.EXPECT(). 168 OnIndexSuccess(gomock.Any()). 169 Times(idsPerBlock). 170 Do(func(arg interface{}) { 171 onIndexWg.Done() 172 }) 173 onIndexSeries.EXPECT(). 174 OnIndexFinalize(gomock.Any()). 175 Times(idsPerBlock) 176 onIndexSeries.EXPECT(). 177 IfAlreadyIndexedMarkIndexSuccessAndFinalize(gomock.Any()). 178 Times(idsPerBlock) 179 onIndexSeries.EXPECT(). 180 IndexedRange(). 181 Return(min, max). 182 AnyTimes() 183 onIndexSeries.EXPECT(). 184 IndexedForBlockStart(gomock.Any()). 185 DoAndReturn(func(ts xtime.UnixNano) bool { 186 return ts.Equal(st) 187 }). 188 AnyTimes() 189 onIndexSeries.EXPECT(). 190 ReconciledOnIndexSeries(). 191 Return(onIndexSeries, resource.SimpleCloserFn(func() {}), false). 192 AnyTimes() 193 194 batch := index.NewWriteBatch(index.WriteBatchOptions{ 195 InitialCapacity: idsPerBlock, 196 IndexBlockSize: test.indexBlockSize, 197 }) 198 for i := 0; i < idsPerBlock; i++ { 199 id := fmt.Sprintf("foo.block_%d.id_%d", blockIdx, i) 200 doc := doc.Metadata{ 201 ID: []byte(id), 202 Fields: []doc.Field{ 203 { 204 Name: []byte("bar"), 205 Value: []byte(fmt.Sprintf("baz.%d", i)), 206 }, 207 { 208 Name: []byte("qux"), 209 Value: []byte("qaz"), 210 }, 211 }, 212 } 213 expectedResults[id] = doc 214 batch.Append(index.WriteBatchEntry{ 215 Timestamp: mutableBlockTime, 216 OnIndexSeries: onIndexSeries, 217 }, doc) 218 } 219 220 err := test.index.WriteBatch(batch) 221 require.NoError(t, err) 222 onIndexWg.Wait() 223 } 224 225 // If force timeout or block errors are enabled, replace one of the blocks 226 // with a mock block that times out or returns an error respectively. 227 var timedOutQueriesWg sync.WaitGroup 228 if opts.forceTimeouts || opts.blockErrors { 229 // Need to restore now as timeouts are measured by looking at time.Now 230 restoreNow() 231 232 nsIdx.state.Lock() 233 234 for start, block := range nsIdx.state.blocksByTime { 235 nsIdx.state.blocksByTime[start] = newMockBlock(ctrl, opts, timeoutValue, block) 236 } 237 nsIdx.activeBlock = newMockBlock(ctrl, opts, timeoutValue, nsIdx.activeBlock) 238 239 nsIdx.state.Unlock() 240 } 241 242 var ( 243 query = idx.NewTermQuery([]byte("qux"), []byte("qaz")) 244 queryConcurrency = 16 245 startWg, readyWg sync.WaitGroup 246 timeoutContextsLock sync.Mutex 247 timeoutContexts []context.Context 248 ) 249 250 var enqueueWg sync.WaitGroup 251 startWg.Add(1) 252 for i := 0; i < queryConcurrency; i++ { 253 readyWg.Add(1) 254 enqueueWg.Add(1) 255 go func() { 256 var ctxs []context.Context 257 defer func() { 258 if !opts.forceTimeouts { 259 // Only close if not being closed by the force timeouts code 260 // at end of the test. 261 for _, ctx := range ctxs { 262 ctx.Close() 263 } 264 } 265 enqueueWg.Done() 266 }() 267 readyWg.Done() 268 startWg.Wait() 269 270 rangeStart := min 271 for k := 0; k < len(blockStarts); k++ { 272 rangeEnd := blockStarts[k].Add(test.indexBlockSize) 273 274 goCtx := stdctx.Background() 275 if timeoutValue > 0 { 276 goCtx, _ = stdctx.WithTimeout(stdctx.Background(), timeoutValue) 277 } 278 279 ctx := context.NewWithGoContext(goCtx) 280 ctxs = append(ctxs, ctx) 281 282 if opts.forceTimeouts { 283 // For the force timeout tests we just want to spin up the 284 // contexts for timeouts. 285 timeoutContextsLock.Lock() 286 timeoutContexts = append(timeoutContexts, ctx) 287 timeoutContextsLock.Unlock() 288 timedOutQueriesWg.Add(1) 289 go func() { 290 _, err := test.index.Query(ctx, index.Query{ 291 Query: query, 292 }, index.QueryOptions{ 293 StartInclusive: rangeStart, 294 EndExclusive: rangeEnd, 295 }) 296 timedOutQueriesWg.Done() 297 require.Error(t, err) 298 }() 299 continue 300 } 301 302 results, err := test.index.Query(ctx, index.Query{ 303 Query: query, 304 }, index.QueryOptions{ 305 StartInclusive: rangeStart, 306 EndExclusive: rangeEnd, 307 }) 308 309 if opts.blockErrors { 310 require.Error(t, err) 311 // Early return because we don't want to check the results. 312 return 313 } else { 314 require.NoError(t, err) 315 } 316 317 // Read the results concurrently too 318 hits := make(map[string]struct{}, results.Results.Size()) 319 id := ident.NewReusableBytesID() 320 for _, entry := range results.Results.Map().Iter() { 321 id.Reset(entry.Key()) 322 tags := testutil.DocumentToTagIter(t, entry.Value()) 323 doc, err := convert.FromSeriesIDAndTagIter(id, tags) 324 require.NoError(t, err) 325 if err != nil { 326 continue // this will fail the test anyway, but don't want to panic 327 } 328 329 expectedDoc, ok := expectedResults[id.String()] 330 require.True(t, ok) 331 if !ok { 332 continue // this will fail the test anyway, but don't want to panic 333 } 334 335 require.Equal(t, expectedDoc, doc, "docs") 336 hits[id.String()] = struct{}{} 337 } 338 expectedHits := idsPerBlock * (k + 1) 339 require.Equal(t, expectedHits, len(hits), "hits") 340 } 341 }() 342 } 343 344 // Wait for all routines to be ready then start 345 readyWg.Wait() 346 startWg.Done() 347 348 // Wait until done 349 enqueueWg.Wait() 350 351 // If forcing timeouts then fire off all the async request to finish 352 // while we close the contexts so any races with finalization and 353 // potentially aborted requests will race against each other. 354 if opts.forceTimeouts { 355 logger.Info("waiting for timeouts") 356 357 // First wait for timeouts 358 timedOutQueriesWg.Wait() 359 logger.Info("timeouts done") 360 361 var ctxCloseWg sync.WaitGroup 362 ctxCloseWg.Add(len(timeoutContexts)) 363 go func() { 364 // Start allowing timedout queries to complete. 365 logger.Info("allow block queries to begin returning") 366 367 // Race closing all contexts at once. 368 for _, ctx := range timeoutContexts { 369 ctx := ctx 370 go func() { 371 ctx.BlockingClose() 372 ctxCloseWg.Done() 373 }() 374 } 375 }() 376 logger.Info("waiting for contexts to finish blocking closing") 377 ctxCloseWg.Wait() 378 379 logger.Info("finished with timeouts") 380 } 381 } 382 383 func newMockBlock(ctrl *gomock.Controller, 384 opts testNamespaceIndexHighConcurrentQueriesOptions, 385 timeout time.Duration, 386 block index.Block, 387 ) *index.MockBlock { 388 mockBlock := index.NewMockBlock(ctrl) 389 mockBlock.EXPECT(). 390 StartTime(). 391 DoAndReturn(func() xtime.UnixNano { return block.StartTime() }). 392 AnyTimes() 393 mockBlock.EXPECT(). 394 EndTime(). 395 DoAndReturn(func() xtime.UnixNano { return block.EndTime() }). 396 AnyTimes() 397 mockBlock.EXPECT().QueryIter(gomock.Any(), gomock.Any()).DoAndReturn(func( 398 ctx context.Context, query index.Query) (index.QueryIterator, error) { 399 return block.QueryIter(ctx, query) 400 }, 401 ).AnyTimes() 402 403 if opts.blockErrors { 404 mockBlock.EXPECT(). 405 QueryWithIter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 406 DoAndReturn(func( 407 _ context.Context, 408 _ index.QueryOptions, 409 _ index.QueryIterator, 410 _ index.QueryResults, 411 _ time.Time, 412 _ []opentracinglog.Field, 413 ) error { 414 return errors.New("some-error") 415 }). 416 AnyTimes() 417 } else { 418 mockBlock.EXPECT(). 419 QueryWithIter(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). 420 DoAndReturn(func( 421 ctx context.Context, 422 opts index.QueryOptions, 423 iter index.QueryIterator, 424 r index.QueryResults, 425 deadline time.Time, 426 logFields []opentracinglog.Field, 427 ) error { 428 time.Sleep(timeout + time.Second) 429 return block.QueryWithIter(ctx, opts, iter, r, deadline, logFields) 430 }). 431 AnyTimes() 432 } 433 434 mockBlock.EXPECT(). 435 Stats(gomock.Any()). 436 Return(nil). 437 AnyTimes() 438 mockBlock.EXPECT(). 439 Close(). 440 DoAndReturn(func() error { 441 return block.Close() 442 }) 443 return mockBlock 444 }