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  }