github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/integration/index_active_block_rotate_test.go (about)

     1  //go:build integration
     2  // +build integration
     3  
     4  //
     5  // Copyright (c) 2021 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package integration
    26  
    27  import (
    28  	"fmt"
    29  	"sync"
    30  	"testing"
    31  	"time"
    32  
    33  	"github.com/m3db/m3/src/dbnode/integration/generate"
    34  	"github.com/m3db/m3/src/dbnode/namespace"
    35  	"github.com/m3db/m3/src/dbnode/retention"
    36  	"github.com/m3db/m3/src/dbnode/storage/index/compaction"
    37  	xclock "github.com/m3db/m3/src/x/clock"
    38  	"github.com/m3db/m3/src/x/ident"
    39  	xtime "github.com/m3db/m3/src/x/time"
    40  
    41  	"github.com/stretchr/testify/assert"
    42  	"github.com/stretchr/testify/require"
    43  	"github.com/uber-go/tally"
    44  	"go.uber.org/atomic"
    45  	"go.uber.org/zap"
    46  	"go.uber.org/zap/zapcore"
    47  	"go.uber.org/zap/zaptest/observer"
    48  )
    49  
    50  func TestIndexActiveBlockRotate(t *testing.T) {
    51  	var (
    52  		testNsID        = ident.StringID("testns")
    53  		numWrites       = 50
    54  		numTags         = 10
    55  		blockSize       = 2 * time.Hour
    56  		indexBlockSize  = blockSize * 2
    57  		retentionPeriod = 12 * blockSize
    58  		bufferPast      = 10 * time.Minute
    59  		rOpts           = retention.NewOptions().
    60  				SetRetentionPeriod(retentionPeriod).
    61  				SetBlockSize(blockSize).
    62  				SetBufferPast(bufferPast)
    63  
    64  		idxOpts = namespace.NewIndexOptions().SetEnabled(true).SetBlockSize(indexBlockSize)
    65  		nsOpts  = namespace.NewOptions().
    66  			SetRetentionOptions(rOpts).
    67  			SetIndexOptions(idxOpts).
    68  			SetColdWritesEnabled(true)
    69  
    70  		defaultTimeout = time.Minute
    71  		// verifyTimeout  = time.Minute
    72  	)
    73  	ns, err := namespace.NewMetadata(testNsID, nsOpts)
    74  	require.NoError(t, err)
    75  
    76  	// Set time to next warm flushable block transition
    77  	// (i.e. align by block + bufferPast - time.Second)
    78  	currTime := time.Now().UTC()
    79  	progressTime := false
    80  	progressTimeDelta := time.Duration(0)
    81  	lockTime := sync.RWMutex{}
    82  	setTime := func(t time.Time) {
    83  		lockTime.Lock()
    84  		defer lockTime.Unlock()
    85  		progressTime = false
    86  		currTime = t.UTC()
    87  	}
    88  	setProgressTime := func() {
    89  		lockTime.Lock()
    90  		defer lockTime.Unlock()
    91  		progressTime = true
    92  		actualNow := time.Now().UTC()
    93  		progressTimeDelta = currTime.Sub(actualNow)
    94  	}
    95  	nowFn := func() time.Time {
    96  		lockTime.RLock()
    97  		at := currTime
    98  		progress := progressTime
    99  		progressDelta := progressTimeDelta
   100  		lockTime.RUnlock()
   101  		if progress {
   102  			return time.Now().UTC().Add(progressDelta)
   103  		}
   104  		return at
   105  	}
   106  
   107  	testOpts := NewTestOptions(t).
   108  		SetNamespaces([]namespace.Metadata{ns}).
   109  		SetWriteNewSeriesAsync(true).
   110  		SetNowFn(nowFn)
   111  
   112  	testSetup, err := NewTestSetup(t, testOpts, nil)
   113  	require.NoError(t, err)
   114  	defer testSetup.Close()
   115  
   116  	// Write test data to disk so that there's some blocks on disk to simulate
   117  	// some index blocks already having on disk segments already in them.
   118  	require.NoError(t, testSetup.InitializeBootstrappers(InitializeBootstrappersOptions{
   119  		WithFileSystem: true,
   120  	}))
   121  	now := testSetup.NowFn()()
   122  	fooSeries := generate.Series{
   123  		ID:   ident.StringID("foo"),
   124  		Tags: ident.NewTags(ident.StringTag("city", "new_york")),
   125  	}
   126  	barSeries := generate.Series{
   127  		ID:   ident.StringID("bar"),
   128  		Tags: ident.NewTags(ident.StringTag("city", "new_jersey")),
   129  	}
   130  	seriesMaps := generate.BlocksByStart([]generate.BlockConfig{
   131  		{
   132  			IDs:       []string{fooSeries.ID.String()},
   133  			Tags:      fooSeries.Tags,
   134  			NumPoints: 100,
   135  			Start:     now.Add(-3 * blockSize),
   136  		},
   137  		{
   138  			IDs:       []string{barSeries.ID.String()},
   139  			Tags:      barSeries.Tags,
   140  			NumPoints: 100,
   141  			Start:     now.Add(-3 * blockSize),
   142  		},
   143  	})
   144  	require.NoError(t, writeTestDataToDisk(ns, testSetup, seriesMaps, 0))
   145  
   146  	// Set foreground compaction planner options to force index compaction.
   147  	minCompactSize := 10
   148  	foregroundCompactionOpts := compaction.DefaultOptions
   149  	foregroundCompactionOpts.Levels = []compaction.Level{
   150  		{
   151  			MinSizeInclusive: 0,
   152  			MaxSizeExclusive: int64(minCompactSize),
   153  		},
   154  	}
   155  	indexOpts := testSetup.StorageOpts().IndexOptions().
   156  		SetForegroundCompactionPlannerOptions(foregroundCompactionOpts)
   157  	testSetup.SetStorageOpts(testSetup.StorageOpts().SetIndexOptions(indexOpts))
   158  
   159  	// Configure log capture
   160  	log := testSetup.StorageOpts().InstrumentOptions().Logger()
   161  	captureCore, logs := observer.New(zapcore.ErrorLevel)
   162  	zapOpt := zap.WrapCore(func(existingCore zapcore.Core) zapcore.Core {
   163  		return zapcore.NewTee(existingCore, captureCore)
   164  	})
   165  	log = log.WithOptions(zapOpt)
   166  
   167  	// Wire up logger.
   168  	instrumentOpts := testSetup.StorageOpts().InstrumentOptions().
   169  		SetLogger(log)
   170  	testSetup.SetStorageOpts(testSetup.StorageOpts().SetInstrumentOptions(instrumentOpts))
   171  	scope := testSetup.Scope()
   172  
   173  	// Start the server.
   174  	require.NoError(t, testSetup.StartServer())
   175  
   176  	// Stop the server.
   177  	defer func() {
   178  		require.NoError(t, testSetup.StopServer())
   179  		log.Debug("server is now down")
   180  	}()
   181  
   182  	// Write test data.
   183  	session, err := testSetup.M3DBClient().DefaultSession()
   184  	require.NoError(t, err)
   185  
   186  	var (
   187  		metricGCSeries   = "index.block.active-block.gc-series+namespace=" + testNsID.String()
   188  		metricFlushIndex = "database.flushIndex.success+namespace=" + testNsID.String()
   189  	)
   190  	prevWarmFlushes := counterValue(t, scope, metricFlushIndex)
   191  	prevNumGCSeries := 0
   192  	numGCSeries := counterValue(t, scope, metricGCSeries)
   193  	require.Equal(t, 0, numGCSeries)
   194  
   195  	prevLog := log
   196  	for i := 0; i < 4; i++ {
   197  		log = prevLog.With(zap.Int("checkIteration", i))
   198  
   199  		// Progress to next time just before a flush and freeze (using setTime).
   200  		prevTime := nowFn()
   201  		newTime := prevTime.
   202  			Truncate(indexBlockSize).
   203  			Add(2 * indexBlockSize)
   204  		setTime(newTime)
   205  		log.Info("progressing time to before next block edge",
   206  			zap.Stringer("prevTime", prevTime),
   207  			zap.Stringer("newTime", newTime))
   208  
   209  		start := time.Now()
   210  		log.Info("writing test data")
   211  
   212  		t0 := xtime.ToUnixNano(newTime.Add(-1 * (bufferPast / 2)))
   213  		t1 := xtime.ToUnixNano(newTime)
   214  		writesPeriodIter := GenerateTestIndexWrite(i, numWrites, numTags, t0, t1)
   215  		writesPeriodIter.Write(t, testNsID, session)
   216  		log.Info("test data written", zap.Duration("took", time.Since(start)))
   217  
   218  		log.Info("waiting till data is indexed")
   219  		indexed := xclock.WaitUntil(func() bool {
   220  			indexedPeriod := writesPeriodIter.NumIndexed(t, testNsID, session)
   221  			return indexedPeriod == len(writesPeriodIter)
   222  		}, 15*time.Second)
   223  		require.True(t, indexed,
   224  			fmt.Sprintf("unexpected data indexed: actual=%d, expected=%d",
   225  				writesPeriodIter.NumIndexedWithOptions(t, testNsID, session, NumIndexedOptions{Logger: log}),
   226  				len(writesPeriodIter)))
   227  		log.Info("verified data is indexed", zap.Duration("took", time.Since(start)))
   228  
   229  		newTime = prevTime.
   230  			Truncate(indexBlockSize).
   231  			Add(2 * indexBlockSize).
   232  			Add(bufferPast).
   233  			Add(-100 * time.Millisecond)
   234  		setTime(newTime)
   235  		log.Info("progressing time to before next flush",
   236  			zap.Stringer("prevTime", prevTime),
   237  			zap.Stringer("newTime", newTime))
   238  
   239  		log.Info("waiting till warm flush occurs")
   240  
   241  		// Resume time progressing by wall clock.
   242  		setProgressTime()
   243  
   244  		// Start checks to ensure metrics are visible the whole time.
   245  		checkFailed := atomic.NewUint64(0)
   246  		checkIndexable := func() {
   247  			numGCSeriesBefore := counterValue(t, scope, metricGCSeries)
   248  			indexedPeriod := writesPeriodIter.NumIndexed(t, testNsID, session)
   249  			numGCSeriesAfter := counterValue(t, scope, metricGCSeries)
   250  			if len(writesPeriodIter) != indexedPeriod {
   251  				assert.Equal(t, len(writesPeriodIter), indexedPeriod,
   252  					fmt.Sprintf("some metrics not indexed/visible: actual=%d, expected=%d, numGCBefore=%d, numGCAfter=%d",
   253  						writesPeriodIter.NumIndexedWithOptions(t, testNsID, session, NumIndexedOptions{Logger: log}),
   254  						len(writesPeriodIter),
   255  						numGCSeriesBefore,
   256  						numGCSeriesAfter))
   257  				checkFailed.Inc()
   258  			}
   259  		}
   260  
   261  		ticker := time.NewTicker(10 * time.Millisecond)
   262  		stopTickCh := make(chan struct{})
   263  		closedTickCh := make(chan struct{})
   264  		go func() {
   265  			defer func() {
   266  				ticker.Stop()
   267  				close(closedTickCh)
   268  			}()
   269  
   270  			for {
   271  				select {
   272  				case <-ticker.C:
   273  					checkIndexable()
   274  				case <-stopTickCh:
   275  					return
   276  				}
   277  			}
   278  		}()
   279  
   280  		start = time.Now()
   281  		warmFlushed := xclock.WaitUntil(func() bool {
   282  			return counterValue(t, scope, metricFlushIndex)-prevWarmFlushes > 0
   283  		}, defaultTimeout)
   284  		counter := counterValue(t, scope, metricFlushIndex)
   285  		require.True(t, warmFlushed,
   286  			fmt.Sprintf("warm flush stats: current=%d, previous=%d", counter, prevWarmFlushes))
   287  		log.Info("verified data has been warm flushed", zap.Duration("took", time.Since(start)))
   288  		prevWarmFlushes = counter
   289  
   290  		start = time.Now()
   291  		log.Info("waiting for GC of series")
   292  
   293  		expectedNumGCSeries := prevNumGCSeries + numWrites - minCompactSize
   294  		gcSeries := xclock.WaitUntil(func() bool {
   295  			// Run background compaction path to check that this path correctly
   296  			// identifies these series as "empty" post the warm flush above.
   297  			// Note: typically this path gets called from just WriteBatch calls but
   298  			// for this test we just explcitly invoke the background compact path.
   299  			for _, ns := range testSetup.DB().Namespaces() {
   300  				idx, err := ns.Index()
   301  				require.NoError(t, err)
   302  
   303  				idx.BackgroundCompact()
   304  			}
   305  
   306  			numGCSeries := counterValue(t, scope, metricGCSeries)
   307  			return numGCSeries >= expectedNumGCSeries
   308  		}, defaultTimeout)
   309  		numGCSeries := counterValue(t, scope, metricGCSeries)
   310  		require.True(t, gcSeries,
   311  			fmt.Sprintf("unexpected num gc series: actual=%d, expected=%d",
   312  				numGCSeries, expectedNumGCSeries))
   313  		require.True(t, numGCSeries >= expectedNumGCSeries)
   314  		log.Info("verified series have been GC'd", zap.Duration("took", time.Since(start)))
   315  		prevNumGCSeries = numGCSeries
   316  
   317  		require.Equal(t, 0, logs.Len(), "errors found in logs during flush/indexing")
   318  
   319  		// Keep running indexable check for a few seconds, then progress next iter.
   320  		time.Sleep(5 * time.Second)
   321  		close(stopTickCh)
   322  		<-closedTickCh
   323  
   324  		// Ensure check did not fail.
   325  		require.True(t, checkFailed.Load() == 0,
   326  			fmt.Sprintf("check indexable errors: %d", checkFailed.Load()))
   327  	}
   328  
   329  	log.Info("checks passed")
   330  }
   331  
   332  func counterValue(t *testing.T, r tally.TestScope, key string) int {
   333  	v, ok := r.Snapshot().Counters()[key]
   334  	require.True(t, ok)
   335  	return int(v.Value())
   336  }