github.com/m3db/m3@v1.5.0/src/dbnode/integration/index_block_orphaned_entry_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  	"math/rand"
    28  	"runtime"
    29  	"strings"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/stretchr/testify/assert"
    35  	"github.com/stretchr/testify/require"
    36  	"go.uber.org/zap"
    37  
    38  	"github.com/m3db/m3/src/dbnode/client"
    39  	"github.com/m3db/m3/src/dbnode/namespace"
    40  	"github.com/m3db/m3/src/dbnode/persist/fs"
    41  	"github.com/m3db/m3/src/dbnode/storage"
    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  	xsync "github.com/m3db/m3/src/x/sync"
    46  	xtime "github.com/m3db/m3/src/x/time"
    47  )
    48  
    49  const (
    50  	numTestSeries     = 5
    51  	concurrentWorkers = 25
    52  	writesPerWorker   = 5
    53  	blockSize         = 2 * time.Hour
    54  )
    55  
    56  func TestIndexBlockOrphanedEntry(t *testing.T) {
    57  	nsOpts := namespace.NewOptions().
    58  		SetRetentionOptions(DefaultIntegrationTestRetentionOpts).
    59  		SetIndexOptions(namespace.NewIndexOptions().SetEnabled(true))
    60  
    61  	setup := generateTestSetup(t, nsOpts)
    62  	defer setup.Close()
    63  
    64  	// Start the server
    65  	log := setup.StorageOpts().InstrumentOptions().Logger()
    66  	require.NoError(t, setup.StartServer())
    67  
    68  	// Stop the server
    69  	defer func() {
    70  		assert.NoError(t, setup.StopServer())
    71  		log.Debug("server is now down")
    72  	}()
    73  
    74  	client := setup.M3DBClient()
    75  	session, err := client.DefaultSession()
    76  	require.NoError(t, err)
    77  
    78  	// Write concurrent metrics to generate multiple entries for the same series
    79  	ids := make([]ident.ID, 0, numTestSeries)
    80  	for i := 0; i < numTestSeries; i++ {
    81  		fooID := ident.StringID(fmt.Sprintf("foo.%v", i))
    82  		ids = append(ids, fooID)
    83  
    84  		writeConcurrentMetrics(t, setup, session, fooID)
    85  	}
    86  
    87  	// Write metrics for a different series to push current foreground segment
    88  	// to the background. After this, all documents for foo.X exist in background segments
    89  	barID := ident.StringID("bar")
    90  	writeConcurrentMetrics(t, setup, session, barID)
    91  
    92  	// Fast-forward to a block rotation
    93  	newBlock := xtime.Now().Truncate(blockSize).Add(blockSize)
    94  	newCurrentTime := newBlock.Add(30 * time.Minute) // Add extra to account for buffer past
    95  	setup.SetNowFn(newCurrentTime)
    96  
    97  	// Wait for flush
    98  	log.Info("waiting for block rotation to complete")
    99  	nsID := setup.Namespaces()[0].ID()
   100  	found := xclock.WaitUntil(func() bool {
   101  		filesets, err := fs.IndexFileSetsAt(setup.FilePathPrefix(), nsID, newBlock.Add(-blockSize))
   102  		require.NoError(t, err)
   103  		return len(filesets) == 1
   104  	}, 60*time.Second)
   105  	require.True(t, found)
   106  
   107  	// Do post-block rotation writes
   108  	for _, id := range ids {
   109  		writeMetric(t, session, nsID, id, newCurrentTime, 999.0)
   110  	}
   111  	writeMetric(t, session, nsID, barID, newCurrentTime, 999.0)
   112  
   113  	// Foreground segments should be in the background again which means updated index entry
   114  	// is now behind the orphaned entry so index reads should fail.
   115  	log.Info("waiting for metrics to be indexed")
   116  	var (
   117  		missing string
   118  		ok      bool
   119  	)
   120  	found = xclock.WaitUntil(func() bool {
   121  		for _, id := range ids {
   122  			ok, err = isIndexedCheckedWithTime(
   123  				t, session, nsID, id, genTags(id), newCurrentTime,
   124  			)
   125  			if !ok || err != nil {
   126  				missing = id.String()
   127  				return false
   128  			}
   129  		}
   130  		return true
   131  	}, 30*time.Second)
   132  	assert.True(t, found, fmt.Sprintf("series %s never indexed\n", missing))
   133  	assert.NoError(t, err)
   134  }
   135  
   136  func writeConcurrentMetrics(
   137  	t *testing.T,
   138  	setup TestSetup,
   139  	session client.Session,
   140  	seriesID ident.ID,
   141  ) {
   142  	var wg sync.WaitGroup
   143  	nowFn := setup.DB().Options().ClockOptions().NowFn()
   144  
   145  	workerPool := xsync.NewWorkerPool(concurrentWorkers)
   146  	workerPool.Init()
   147  
   148  	mdID := setup.Namespaces()[0].ID()
   149  	for i := 0; i < concurrentWorkers; i++ {
   150  		wg.Add(1)
   151  		go func() {
   152  			defer wg.Done()
   153  
   154  			for j := 0; j < writesPerWorker; j++ {
   155  				j := j
   156  				wg.Add(1)
   157  				workerPool.Go(func() {
   158  					defer wg.Done()
   159  					writeMetric(t, session, mdID, seriesID, xtime.ToUnixNano(nowFn()), float64(j))
   160  				})
   161  			}
   162  		}()
   163  	}
   164  
   165  	wg.Wait()
   166  }
   167  
   168  func genTags(seriesID ident.ID) ident.TagsIterator {
   169  	return ident.NewTagsIterator(ident.NewTags(ident.StringTag("tagName", seriesID.String())))
   170  }
   171  
   172  func writeMetric(
   173  	t *testing.T,
   174  	session client.Session,
   175  	nsID ident.ID,
   176  	seriesID ident.ID,
   177  	timestamp xtime.UnixNano,
   178  	value float64,
   179  ) {
   180  	err := session.WriteTagged(nsID, seriesID, genTags(seriesID),
   181  		timestamp, value, xtime.Second, nil)
   182  	require.NoError(t, err)
   183  }
   184  
   185  func generateTestSetup(t *testing.T, nsOpts namespace.Options) TestSetup {
   186  	md, err := namespace.NewMetadata(testNamespaces[0], nsOpts)
   187  	require.NoError(t, err)
   188  
   189  	testOpts := NewTestOptions(t).
   190  		SetNamespaces([]namespace.Metadata{md}).
   191  		SetWriteNewSeriesAsync(true)
   192  	testSetup, err := NewTestSetup(t, testOpts, nil,
   193  		func(s storage.Options) storage.Options {
   194  			s = s.SetCoreFn(func() int {
   195  				return rand.Intn(4) //nolint:gosec
   196  			})
   197  			compactionOpts := s.IndexOptions().ForegroundCompactionPlannerOptions()
   198  			compactionOpts.Levels = []compaction.Level{
   199  				{
   200  					MinSizeInclusive: 0,
   201  					MaxSizeExclusive: 1,
   202  				},
   203  			}
   204  			return s.SetIndexOptions(
   205  				s.IndexOptions().SetForegroundCompactionPlannerOptions(compactionOpts))
   206  		})
   207  	require.NoError(t, err)
   208  
   209  	return testSetup
   210  }
   211  
   212  func TestIndexBlockOrphanedIndexValuesUpdatedAcrossTimes(t *testing.T) {
   213  	tests := []struct {
   214  		name     string
   215  		numIDs   int
   216  		interval time.Duration
   217  	}{
   218  		{
   219  			name:     "4 series every 100 nanos",
   220  			numIDs:   4,
   221  			interval: 100,
   222  		},
   223  		{
   224  			name:     "4 series every block",
   225  			numIDs:   4,
   226  			interval: blockSize,
   227  		},
   228  		{
   229  			name:     "12 series every 100 nanos",
   230  			numIDs:   12,
   231  			interval: 100,
   232  		},
   233  		{
   234  			name:     "12 series every block",
   235  			numIDs:   12,
   236  			interval: blockSize,
   237  		},
   238  		{
   239  			name:     "120 series every 100 nanos",
   240  			numIDs:   120,
   241  			interval: 100,
   242  		},
   243  		{
   244  			name:     "120 series every block",
   245  			numIDs:   120,
   246  			interval: blockSize,
   247  		},
   248  	}
   249  	for _, tt := range tests {
   250  		t.Run(tt.name, func(t *testing.T) {
   251  			testIndexBlockOrphanedIndexValuesUpdatedAcrossTimes(t, tt.numIDs, tt.interval)
   252  		})
   253  	}
   254  }
   255  
   256  func testIndexBlockOrphanedIndexValuesUpdatedAcrossTimes(
   257  	t *testing.T, numIDs int, writeInterval time.Duration,
   258  ) {
   259  	// Write a metric concurrently for multiple index blocks to generate
   260  	// multiple entries for the same series
   261  	var (
   262  		concurrentWriteMax = runtime.NumCPU() / 2
   263  		ids                = make([]ident.ID, 0, numIDs)
   264  		writerCh           = make(chan func(), concurrentWriteMax)
   265  
   266  		nsID = testNamespaces[0]
   267  
   268  		writesPerWorker = 5
   269  		writeTimes      = make([]xtime.UnixNano, 0, writesPerWorker)
   270  		retention       = blockSize * time.Duration(1+writesPerWorker)
   271  
   272  		seed = time.Now().UnixNano()
   273  		rng  = rand.New(rand.NewSource(seed)) // nolint:gosec
   274  	)
   275  
   276  	retOpts := DefaultIntegrationTestRetentionOpts.SetRetentionPeriod(retention)
   277  	nsOpts := namespace.NewOptions().
   278  		SetRetentionOptions(retOpts).
   279  		SetIndexOptions(namespace.NewIndexOptions().SetEnabled(true)).
   280  		SetColdWritesEnabled(true)
   281  
   282  	setup := generateTestSetup(t, nsOpts)
   283  	defer setup.Close()
   284  
   285  	// Start the server
   286  	log := setup.StorageOpts().InstrumentOptions().Logger()
   287  	log.Info("running test with seed", zap.Int("seed", int(seed)))
   288  	require.NoError(t, setup.StartServer())
   289  
   290  	// Stop the server
   291  	defer func() {
   292  		assert.NoError(t, setup.StopServer())
   293  		log.Debug("server is now down")
   294  	}()
   295  
   296  	client := setup.M3DBClient()
   297  	session, err := client.DefaultSession()
   298  	require.NoError(t, err)
   299  
   300  	var (
   301  		nowFn = setup.DB().Options().ClockOptions().NowFn()
   302  		// NB: write in the middle of a block to avoid block boundaries.
   303  		now = nowFn().Truncate(blockSize / 2)
   304  	)
   305  
   306  	for i := 0; i < writesPerWorker; i++ {
   307  		writeTime := xtime.ToUnixNano(now.Add(time.Duration(i) * -writeInterval))
   308  		writeTimes = append(writeTimes, writeTime)
   309  	}
   310  
   311  	fns := make([]func(), 0, numIDs*writesPerWorker)
   312  	for i := 0; i < numIDs; i++ {
   313  		fooID := ident.StringID(fmt.Sprintf("foo.%v", i))
   314  		ids = append(ids, fooID)
   315  		fns = append(fns, writeConcurrentMetricsAcrossTime(t, setup, session, writeTimes, fooID)...)
   316  	}
   317  
   318  	rng.Shuffle(len(fns), func(i, j int) { fns[i], fns[j] = fns[j], fns[i] })
   319  	var wg sync.WaitGroup
   320  	for i := 0; i < concurrentWriteMax; i++ {
   321  		wg.Add(1)
   322  		go func() {
   323  			for writeFn := range writerCh {
   324  				writeFn()
   325  			}
   326  
   327  			wg.Done()
   328  		}()
   329  	}
   330  
   331  	for _, fn := range fns {
   332  		writerCh <- fn
   333  	}
   334  
   335  	close(writerCh)
   336  	wg.Wait()
   337  
   338  	queryIDs := func() {
   339  		notFoundIds := make(notFoundIDs, 0, len(ids)*len(writeTimes))
   340  		for _, id := range ids {
   341  			for _, writeTime := range writeTimes {
   342  				notFoundIds = append(notFoundIds, notFoundID{id: id, runAt: writeTime})
   343  			}
   344  		}
   345  
   346  		found := xclock.WaitUntil(func() bool {
   347  			filteredIds := notFoundIds[:0]
   348  			for _, id := range notFoundIds {
   349  				ok, err := isIndexedCheckedWithTime(
   350  					t, session, nsID, id.id, genTags(id.id), id.runAt,
   351  				)
   352  				if !ok || err != nil {
   353  					filteredIds = append(filteredIds, id)
   354  				}
   355  			}
   356  
   357  			if len(filteredIds) == 0 {
   358  				return true
   359  			}
   360  
   361  			notFoundIds = filteredIds
   362  			return false
   363  		}, time.Second*30)
   364  
   365  		require.True(t, found, fmt.Sprintf("series %s never indexed\n", notFoundIds))
   366  	}
   367  
   368  	// Ensure all IDs are eventually queryable, even when only in the foreground
   369  	// segments.
   370  	queryIDs()
   371  
   372  	// Write metrics for a different series to push current foreground segment
   373  	// to the background. After this, all documents for foo.X exist in background segments
   374  	barID := ident.StringID("bar")
   375  	writeConcurrentMetrics(t, setup, session, barID)
   376  
   377  	queryIDs()
   378  
   379  	// Fast-forward to a block rotation
   380  	newBlock := xtime.Now().Truncate(blockSize).Add(blockSize)
   381  	newCurrentTime := newBlock.Add(30 * time.Minute) // Add extra to account for buffer past
   382  	setup.SetNowFn(newCurrentTime)
   383  
   384  	// Wait for flush
   385  	log.Info("waiting for block rotation to complete")
   386  	found := xclock.WaitUntil(func() bool {
   387  		filesets, err := fs.IndexFileSetsAt(setup.FilePathPrefix(), nsID, newBlock.Add(-blockSize))
   388  		require.NoError(t, err)
   389  		return len(filesets) == 1
   390  	}, 30*time.Second)
   391  	require.True(t, found)
   392  
   393  	queryIDs()
   394  }
   395  
   396  type notFoundID struct {
   397  	id    ident.ID
   398  	runAt xtime.UnixNano
   399  }
   400  
   401  func (i notFoundID) String() string {
   402  	return fmt.Sprintf("{%s: %s}", i.id.String(), i.runAt.String())
   403  }
   404  
   405  type notFoundIDs []notFoundID
   406  
   407  func (ids notFoundIDs) String() string {
   408  	strs := make([]string, 0, len(ids))
   409  	for _, id := range ids {
   410  		strs = append(strs, id.String())
   411  	}
   412  
   413  	return fmt.Sprintf("[%s]", strings.Join(strs, ", "))
   414  }
   415  
   416  // writeConcurrentMetricsAcrossTime writes a datapoint for the given series at
   417  // each `writeTime` simultaneously.
   418  func writeConcurrentMetricsAcrossTime(
   419  	t *testing.T,
   420  	setup TestSetup,
   421  	session client.Session,
   422  	writeTimes []xtime.UnixNano,
   423  	seriesID ident.ID,
   424  ) []func() {
   425  	workerPool := xsync.NewWorkerPool(concurrentWorkers)
   426  	workerPool.Init()
   427  
   428  	mdID := setup.Namespaces()[0].ID()
   429  	fns := make([]func(), 0, len(writeTimes))
   430  
   431  	for j, writeTime := range writeTimes {
   432  		j, writeTime := j, writeTime
   433  		fns = append(fns, func() {
   434  			writeMetric(t, session, mdID, seriesID, writeTime, float64(j))
   435  		})
   436  	}
   437  
   438  	return fns
   439  }