github.com/m3db/m3@v1.5.0/src/dbnode/integration/index_active_block_rotate_test.go (about)

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