github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/index/mutable_segments_test.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 index
    22  
    23  import (
    24  	"fmt"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/m3db/m3/src/dbnode/namespace"
    29  	"github.com/m3db/m3/src/m3ninx/doc"
    30  	"github.com/m3db/m3/src/m3ninx/search"
    31  	"github.com/m3db/m3/src/m3ninx/search/query"
    32  	"github.com/m3db/m3/src/x/instrument"
    33  	xsync "github.com/m3db/m3/src/x/sync"
    34  	xtest "github.com/m3db/m3/src/x/test"
    35  	xtime "github.com/m3db/m3/src/x/time"
    36  
    37  	"github.com/stretchr/testify/assert"
    38  	"github.com/stretchr/testify/require"
    39  	"go.uber.org/zap"
    40  )
    41  
    42  type testMutableSegmentsResult struct {
    43  	logger      *zap.Logger
    44  	searchCache *PostingsListCache
    45  }
    46  
    47  func newTestMutableSegments(
    48  	t *testing.T,
    49  	md namespace.Metadata,
    50  	blockStart xtime.UnixNano,
    51  ) (*mutableSegments, testMutableSegmentsResult) {
    52  	cachedSearchesWorkers := xsync.NewWorkerPool(2)
    53  	cachedSearchesWorkers.Init()
    54  
    55  	iOpts := instrument.NewTestOptions(t)
    56  
    57  	cache, err := NewPostingsListCache(10, PostingsListCacheOptions{
    58  		InstrumentOptions: iOpts,
    59  	})
    60  	require.NoError(t, err)
    61  
    62  	searchCache, err := NewPostingsListCache(10, PostingsListCacheOptions{
    63  		InstrumentOptions: iOpts,
    64  	})
    65  	require.NoError(t, err)
    66  
    67  	opts := testOpts.
    68  		SetPostingsListCache(cache).
    69  		SetSearchPostingsListCache(searchCache).
    70  		SetReadThroughSegmentOptions(ReadThroughSegmentOptions{
    71  			CacheRegexp:   true,
    72  			CacheTerms:    true,
    73  			CacheSearches: true,
    74  		})
    75  
    76  	segs := newMutableSegments(md, blockStart, opts, BlockOptions{},
    77  		cachedSearchesWorkers, namespace.NewRuntimeOptionsManager("foo"), iOpts)
    78  	require.NoError(t, err)
    79  
    80  	return segs, testMutableSegmentsResult{
    81  		logger:      iOpts.Logger(),
    82  		searchCache: searchCache,
    83  	}
    84  }
    85  
    86  func TestMutableSegmentsBackgroundCompactGCReconstructCachedSearches(t *testing.T) {
    87  	ctrl := xtest.NewController(t)
    88  	defer ctrl.Finish()
    89  
    90  	blockSize := time.Hour
    91  	testMD := newTestNSMetadata(t)
    92  	blockStart := xtime.Now().Truncate(blockSize)
    93  
    94  	nowNotBlockStartAligned := blockStart.Add(time.Minute)
    95  
    96  	segs, result := newTestMutableSegments(t, testMD, blockStart)
    97  	segs.backgroundCompactDisable = true // Disable to explicitly test.
    98  
    99  	inserted := 0
   100  	segs.Lock()
   101  	segsBackground := len(segs.backgroundSegments)
   102  	segs.Unlock()
   103  
   104  	for runs := 0; runs < 10; runs++ {
   105  		runs := runs
   106  		t.Run(fmt.Sprintf("run-%d", runs), func(t *testing.T) {
   107  			logger := result.logger.With(zap.Int("run", runs))
   108  
   109  			// Insert until we have a new background segment.
   110  			for {
   111  				segs.Lock()
   112  				curr := len(segs.backgroundSegments)
   113  				segs.Unlock()
   114  				if curr > segsBackground {
   115  					segsBackground = curr
   116  					break
   117  				}
   118  
   119  				batch := NewWriteBatch(WriteBatchOptions{
   120  					IndexBlockSize: blockSize,
   121  				})
   122  				for i := 0; i < 128; i++ {
   123  					onIndexSeries := doc.NewMockOnIndexSeries(ctrl)
   124  					onIndexSeries.EXPECT().
   125  						TryMarkIndexGarbageCollected().
   126  						// Every other is "empty".
   127  						Return(inserted%2 == 0).
   128  						AnyTimes()
   129  					onIndexSeries.EXPECT().
   130  						NeedsIndexGarbageCollected().
   131  						// Every other is "empty".
   132  						Return(inserted%2 == 0).
   133  						AnyTimes()
   134  
   135  					batch.Append(WriteBatchEntry{
   136  						Timestamp:     nowNotBlockStartAligned,
   137  						OnIndexSeries: onIndexSeries,
   138  					}, testDocN(inserted))
   139  					inserted++
   140  				}
   141  
   142  				_, err := segs.WriteBatch(batch)
   143  				require.NoError(t, err)
   144  			}
   145  
   146  			// Perform some searches.
   147  			testDocSearches(t, segs)
   148  
   149  			// Make sure search postings cache was populated.
   150  			require.True(t, result.searchCache.lru.Len() > 0)
   151  			logger.Info("search cache populated", zap.Int("n", result.searchCache.lru.Len()))
   152  
   153  			// Start some async searches so we have searches going on while
   154  			// executing background compact GC.
   155  			doneCh := make(chan struct{}, 2)
   156  			defer close(doneCh)
   157  			for i := 0; i < 2; i++ {
   158  				go func() {
   159  					for {
   160  						select {
   161  						case <-doneCh:
   162  							return
   163  						default:
   164  						}
   165  						// Search continuously.
   166  						testDocSearches(t, segs)
   167  					}
   168  				}()
   169  			}
   170  
   171  			// Explicitly background compact and make sure that background segment
   172  			// is GC'd of series no longer present.
   173  			segs.Lock()
   174  			segs.backgroundCompactWithLock(false)
   175  			compactingBackgroundStandard := segs.compact.compactingBackgroundStandard
   176  			compactingBackgroundGarbageCollect := segs.compact.compactingBackgroundGarbageCollect
   177  			segs.Unlock()
   178  
   179  			// Should have kicked off a background compact GC.
   180  			require.True(t, compactingBackgroundStandard || compactingBackgroundGarbageCollect)
   181  
   182  			// Wait for background compact GC to run.
   183  			for {
   184  				segs.Lock()
   185  				compactingBackgroundStandard := segs.compact.compactingBackgroundStandard
   186  				compactingBackgroundGarbageCollect := segs.compact.compactingBackgroundGarbageCollect
   187  				segs.Unlock()
   188  				if !compactingBackgroundStandard && !compactingBackgroundGarbageCollect {
   189  					break
   190  				}
   191  				time.Sleep(100 * time.Millisecond)
   192  			}
   193  
   194  			logger.Info("compaction done, search cache", zap.Int("n", result.searchCache.lru.Len()))
   195  		})
   196  	}
   197  }
   198  
   199  func testDocSearches(
   200  	t *testing.T,
   201  	segs *mutableSegments,
   202  ) {
   203  	for i := 0; i < len(testDocBucket0Values); i++ {
   204  		for j := 0; j < len(testDocBucket1Values); j++ {
   205  			readers, err := segs.AddReaders(nil)
   206  			assert.NoError(t, err)
   207  
   208  			regexp0 := fmt.Sprintf("(%s|%s)", moduloByteStr(testDocBucket0Values, i),
   209  				moduloByteStr(testDocBucket0Values, i+1))
   210  			b0, err := query.NewRegexpQuery([]byte(testDocBucket0Name), []byte(regexp0))
   211  			assert.NoError(t, err)
   212  
   213  			regexp1 := fmt.Sprintf("(%s|%s|%s)", moduloByteStr(testDocBucket1Values, j),
   214  				moduloByteStr(testDocBucket1Values, j+1),
   215  				moduloByteStr(testDocBucket1Values, j+2))
   216  			b1, err := query.NewRegexpQuery([]byte(testDocBucket1Name), []byte(regexp1))
   217  			assert.NoError(t, err)
   218  
   219  			q := query.NewConjunctionQuery([]search.Query{b0, b1})
   220  			searcher, err := q.Searcher()
   221  			assert.NoError(t, err)
   222  
   223  			for _, reader := range readers {
   224  				readThrough, ok := reader.(search.ReadThroughSegmentSearcher)
   225  				assert.True(t, ok)
   226  
   227  				pl, err := readThrough.Search(q, searcher)
   228  				assert.NoError(t, err)
   229  
   230  				assert.True(t, pl.Len() > 0)
   231  			}
   232  		}
   233  	}
   234  }
   235  
   236  var (
   237  	testDocBucket0Name   = "bucket_0"
   238  	testDocBucket0Values = []string{
   239  		"one",
   240  		"two",
   241  		"three",
   242  	}
   243  	testDocBucket1Name   = "bucket_1"
   244  	testDocBucket1Values = []string{
   245  		"one",
   246  		"two",
   247  		"three",
   248  		"four",
   249  		"five",
   250  	}
   251  )
   252  
   253  func testDocN(n int) doc.Metadata {
   254  	return doc.Metadata{
   255  		ID: []byte(fmt.Sprintf("doc-%d", n)),
   256  		Fields: []doc.Field{
   257  			{
   258  				Name:  []byte("foo"),
   259  				Value: []byte("bar"),
   260  			},
   261  			{
   262  				Name:  []byte(testDocBucket0Name),
   263  				Value: moduloByteStr(testDocBucket0Values, n),
   264  			},
   265  			{
   266  				Name:  []byte(testDocBucket1Name),
   267  				Value: moduloByteStr(testDocBucket1Values, n),
   268  			},
   269  		},
   270  	}
   271  }
   272  
   273  func moduloByteStr(strs []string, n int) []byte {
   274  	return []byte(strs[n%len(strs)])
   275  }