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