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

     1  // +build integration
     2  
     3  // Copyright (c) 2017 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  	"testing"
    28  	"time"
    29  
    30  	"github.com/m3db/m3/src/dbnode/integration/generate"
    31  	"github.com/m3db/m3/src/dbnode/namespace"
    32  	"github.com/m3db/m3/src/dbnode/retention"
    33  	"github.com/m3db/m3/src/dbnode/ts"
    34  	"github.com/m3db/m3/src/x/context"
    35  	"github.com/m3db/m3/src/x/ident"
    36  	xtime "github.com/m3db/m3/src/x/time"
    37  
    38  	"github.com/m3db/m3/src/dbnode/testdata/prototest"
    39  	"github.com/stretchr/testify/require"
    40  )
    41  
    42  type annotationGenerator interface {
    43  	Next() []byte
    44  }
    45  
    46  func TestFsCommitLogMixedModeReadWrite(t *testing.T) {
    47  	testFsCommitLogMixedModeReadWrite(t, nil, nil)
    48  }
    49  
    50  func TestProtoFsCommitLogMixedModeReadWrite(t *testing.T) {
    51  	testFsCommitLogMixedModeReadWrite(t, setProtoTestOptions, prototest.NewProtoMessageIterator(testProtoMessages))
    52  }
    53  
    54  func testFsCommitLogMixedModeReadWrite(t *testing.T, setTestOpts setTestOptions, annGen annotationGenerator) {
    55  	if testing.Short() {
    56  		t.SkipNow() // Just skip if we're doing a short run
    57  	}
    58  	// Test setup
    59  	var (
    60  		ns1BlockSize = 1 * time.Hour
    61  		ns1ROpts     = retention.NewOptions().SetRetentionPeriod(3 * time.Hour).SetBlockSize(ns1BlockSize)
    62  		nsID         = testNamespaces[0]
    63  	)
    64  
    65  	ns1Opts := namespace.NewOptions().
    66  		SetRetentionOptions(ns1ROpts)
    67  	ns1, err := namespace.NewMetadata(nsID, ns1Opts)
    68  	require.NoError(t, err)
    69  	opts := NewTestOptions(t).
    70  		SetNamespaces([]namespace.Metadata{ns1})
    71  
    72  	if setTestOpts != nil {
    73  		opts = setTestOpts(t, opts)
    74  		ns1 = opts.Namespaces()[0]
    75  	}
    76  
    77  	// Test setup
    78  	setup := newTestSetupWithCommitLogAndFilesystemBootstrapper(t, opts)
    79  	defer setup.Close()
    80  
    81  	log := setup.StorageOpts().InstrumentOptions().Logger()
    82  	log.Info("commit log & fileset files, write, read, and merge bootstrap test")
    83  
    84  	filePathPrefix := setup.StorageOpts().CommitLogOptions().FilesystemOptions().FilePathPrefix()
    85  
    86  	// setting time to 2017/02/13 15:30:10
    87  	fakeStart := xtime.ToUnixNano(time.Date(2017, time.February, 13, 15, 30, 10, 0, time.Local))
    88  	blkStart15 := fakeStart.Truncate(ns1BlockSize)
    89  	blkStart16 := blkStart15.Add(ns1BlockSize)
    90  	blkStart17 := blkStart16.Add(ns1BlockSize)
    91  	blkStart18 := blkStart17.Add(ns1BlockSize)
    92  	setup.SetNowFn(fakeStart)
    93  
    94  	// startup server
    95  	log.Debug("starting server")
    96  	startServerWithNewInspection(t, opts, setup)
    97  	log.Debug("server is now up")
    98  
    99  	// Stop the server
   100  	defer func() {
   101  		log.Debug("stopping server")
   102  		require.NoError(t, setup.StopServer())
   103  		log.Debug("server is now down")
   104  	}()
   105  
   106  	// mimic a run of 200 minutes,
   107  	// should flush data for hour 15, 16, 17
   108  	// should have 50 mins of data in hour 18
   109  	var (
   110  		total = 200
   111  		ids   = &idGen{longTestID}
   112  		db    = setup.DB()
   113  		ctx   = context.NewBackground()
   114  	)
   115  	defer ctx.Close()
   116  	log.Info("writing datapoints")
   117  	datapoints := generateDatapoints(fakeStart, total, ids, annGen)
   118  	for _, dp := range datapoints {
   119  		ts := dp.time
   120  		setup.SetNowFn(ts)
   121  		require.NoError(t, db.Write(ctx, nsID, dp.series, ts, dp.value, xtime.Second, dp.ann))
   122  	}
   123  	log.Info("wrote datapoints")
   124  
   125  	// verify in-memory data matches what we expect
   126  	expectedSeriesMap := datapoints.toSeriesMap(ns1BlockSize)
   127  	log.Info("verifying data in database equals expected data")
   128  	verifySeriesMaps(t, setup, nsID, expectedSeriesMap)
   129  	log.Info("verified data in database equals expected data")
   130  
   131  	// current time is 18:50, so we expect data for block starts [15, 18) to be written out
   132  	// to fileset files, and flushed.
   133  	expectedFlushedData := datapoints.toSeriesMap(ns1BlockSize)
   134  	delete(expectedFlushedData, blkStart18)
   135  	waitTimeout := 5 * time.Minute
   136  
   137  	log.Info("waiting till expected fileset files have been written")
   138  	require.NoError(t, waitUntilDataFilesFlushed(filePathPrefix, setup.ShardSet(), nsID, expectedFlushedData, waitTimeout))
   139  	log.Info("expected fileset files have been written")
   140  
   141  	// stopping db
   142  	log.Info("stopping database")
   143  	require.NoError(t, setup.StopServer())
   144  	log.Info("database stopped")
   145  
   146  	// the time now is 18:55
   147  	setup.SetNowFn(setup.NowFn()().Add(5 * time.Minute))
   148  
   149  	// recreate the db from the data files and commit log
   150  	// should contain data from 15:30 - 17:59 on disk and 18:00 - 18:50 in mem
   151  	log.Info("re-opening database & bootstrapping")
   152  	startServerWithNewInspection(t, opts, setup)
   153  	log.Info("verifying data in database equals expected data")
   154  	verifySeriesMaps(t, setup, nsID, expectedSeriesMap)
   155  	log.Info("verified data in database equals expected data")
   156  
   157  	// the time now is 19:15
   158  	setup.SetNowFn(setup.NowFn()().Add(20 * time.Minute))
   159  	// data from hour 15 is now outdated, ensure the file has been cleaned up
   160  	log.Info("waiting till expired fileset files have been cleanedup")
   161  	require.NoError(t, waitUntilFileSetFilesCleanedUp(setup, nsID, blkStart15, waitTimeout))
   162  	log.Info("fileset files have been cleaned up")
   163  
   164  	// stopping db
   165  	log.Info("stopping database")
   166  	require.NoError(t, setup.StopServer())
   167  	log.Info("database stopped")
   168  
   169  	// recreate the db from the data files and commit log
   170  	log.Info("re-opening database & bootstrapping")
   171  	startServerWithNewInspection(t, opts, setup)
   172  
   173  	// verify in-memory data matches what we expect
   174  	// should contain data from 16:00 - 17:59 on disk and 18:00 - 18:50 in mem
   175  	delete(expectedSeriesMap, blkStart15)
   176  	log.Info("verifying data in database equals expected data")
   177  	verifySeriesMaps(t, setup, nsID, expectedSeriesMap)
   178  	log.Info("verified data in database equals expected data")
   179  }
   180  
   181  // We use this helper method to start the server so that a new filesystem
   182  // inspection and commitlog bootstrapper are generated each time.
   183  func startServerWithNewInspection(
   184  	t *testing.T,
   185  	opts TestOptions,
   186  	setup TestSetup,
   187  ) {
   188  	setCommitLogAndFilesystemBootstrapper(t, opts, setup)
   189  	require.NoError(t, setup.StartServer())
   190  }
   191  
   192  func waitUntilFileSetFilesCleanedUp(
   193  	setup TestSetup,
   194  	namespace ident.ID,
   195  	toDelete xtime.UnixNano,
   196  	timeout time.Duration,
   197  ) error {
   198  	var (
   199  		shardSet       = setup.ShardSet()
   200  		filesetFiles   = []cleanupTimesFileSet{}
   201  		commitLogFiles = cleanupTimesCommitLog{
   202  			clOpts: setup.StorageOpts().CommitLogOptions(),
   203  		}
   204  	)
   205  	for _, id := range shardSet.AllIDs() {
   206  		filesetFiles = append(filesetFiles, cleanupTimesFileSet{
   207  			filePathPrefix: setup.FilePathPrefix(),
   208  			namespace:      namespace,
   209  			shard:          id,
   210  			times:          []xtime.UnixNano{toDelete},
   211  		})
   212  	}
   213  	return waitUntilDataCleanedUpExtended(filesetFiles, commitLogFiles, timeout)
   214  }
   215  
   216  func newTestSetupWithCommitLogAndFilesystemBootstrapper(t *testing.T, opts TestOptions) TestSetup {
   217  	setup, err := NewTestSetup(t, opts, nil)
   218  	require.NoError(t, err)
   219  
   220  	setCommitLogAndFilesystemBootstrapper(t, opts, setup)
   221  
   222  	return setup
   223  }
   224  
   225  func setCommitLogAndFilesystemBootstrapper(t *testing.T, opts TestOptions, setup TestSetup) TestSetup {
   226  	commitLogOpts := setup.StorageOpts().CommitLogOptions()
   227  
   228  	commitLogOpts = commitLogOpts.
   229  		SetFlushInterval(defaultIntegrationTestFlushInterval)
   230  	setup.SetStorageOpts(setup.StorageOpts().SetCommitLogOptions(commitLogOpts))
   231  
   232  	require.NoError(t, setup.InitializeBootstrappers(InitializeBootstrappersOptions{
   233  		CommitLogOptions: commitLogOpts,
   234  		WithCommitLog:    true,
   235  		WithFileSystem:   true,
   236  	}))
   237  
   238  	// Need to make sure we have an active m3dbAdminClient because the previous one
   239  	// may have been shutdown by StopServer().
   240  	setup.MaybeResetClients()
   241  
   242  	return setup
   243  }
   244  
   245  func generateDatapoints(
   246  	start xtime.UnixNano, numPoints int, ig *idGen, annGen annotationGenerator,
   247  ) dataPointsInTimeOrder {
   248  	var points dataPointsInTimeOrder
   249  	for i := 0; i < numPoints; i++ {
   250  		t := start.Add(time.Duration(i) * time.Minute)
   251  		if annGen == nil {
   252  			points = append(points,
   253  				seriesDatapoint{
   254  					series: ig.base(),
   255  					time:   t,
   256  					value:  float64(i),
   257  				},
   258  				seriesDatapoint{
   259  					series: ig.nth(i),
   260  					time:   t,
   261  					value:  float64(i),
   262  				},
   263  			)
   264  		} else {
   265  			annBytes := annGen.Next()
   266  			points = append(points,
   267  				seriesDatapoint{
   268  					series: ig.base(),
   269  					time:   t,
   270  					ann:    annBytes,
   271  				},
   272  				seriesDatapoint{
   273  					series: ig.nth(i),
   274  					time:   t,
   275  					ann:    annBytes,
   276  				},
   277  			)
   278  		}
   279  	}
   280  	return points
   281  }
   282  
   283  type dataPointsInTimeOrder []seriesDatapoint
   284  
   285  type seriesDatapoint struct {
   286  	series ident.ID
   287  	time   xtime.UnixNano
   288  	value  float64
   289  	ann    []byte
   290  }
   291  
   292  func (d dataPointsInTimeOrder) toSeriesMap(blockSize time.Duration) generate.SeriesBlocksByStart {
   293  	blockStartToSeriesMap := make(map[xtime.UnixNano]map[string]generate.Series)
   294  	for _, point := range d {
   295  		t := point.time
   296  		trunc := t.Truncate(blockSize)
   297  		seriesBlock, ok := blockStartToSeriesMap[trunc]
   298  		if !ok {
   299  			seriesBlock = make(map[string]generate.Series)
   300  		}
   301  		idString := point.series.String()
   302  		dp, ok := seriesBlock[idString]
   303  		if !ok {
   304  			dp = generate.Series{ID: point.series}
   305  		}
   306  		dp.Data = append(dp.Data, generate.TestValue{Datapoint: ts.Datapoint{
   307  			TimestampNanos: t,
   308  			Value:          point.value,
   309  		}, Annotation: point.ann})
   310  		seriesBlock[idString] = dp
   311  		blockStartToSeriesMap[trunc] = seriesBlock
   312  	}
   313  
   314  	seriesMap := make(generate.SeriesBlocksByStart, len(blockStartToSeriesMap))
   315  	for t, serieses := range blockStartToSeriesMap {
   316  		seriesSlice := make([]generate.Series, 0, len(serieses))
   317  		for _, series := range serieses {
   318  			seriesSlice = append(seriesSlice, series)
   319  		}
   320  		seriesMap[t] = seriesSlice
   321  	}
   322  	return seriesMap
   323  }
   324  
   325  // before returns a slice of the dataPointsInTimeOrder that are before the
   326  // specified time t.
   327  func (d dataPointsInTimeOrder) before(t xtime.UnixNano) dataPointsInTimeOrder {
   328  	var i int
   329  	for i = range d {
   330  		if !d[i].time.Before(t) {
   331  			break
   332  		}
   333  	}
   334  
   335  	return d[:i]
   336  }
   337  
   338  type idGen struct {
   339  	baseID string
   340  }
   341  
   342  func (i *idGen) base() ident.ID {
   343  	return ident.StringID(i.baseID)
   344  }
   345  
   346  func (i *idGen) nth(n int) ident.ID {
   347  	return ident.StringID(fmt.Sprintf("%s%d", i.baseID, n))
   348  }
   349  
   350  const (
   351  	longTestID = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
   352  )