github.com/m3db/m3@v1.5.0/src/dbnode/storage/bootstrap/bootstrapper/fs/source_index_test.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package fs
    22  
    23  import (
    24  	"fmt"
    25  	"os"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/stretchr/testify/require"
    30  	"github.com/uber-go/tally"
    31  
    32  	"github.com/m3db/m3/src/dbnode/namespace"
    33  	"github.com/m3db/m3/src/dbnode/persist"
    34  	"github.com/m3db/m3/src/dbnode/persist/fs"
    35  	"github.com/m3db/m3/src/dbnode/retention"
    36  	"github.com/m3db/m3/src/dbnode/storage/bootstrap"
    37  	"github.com/m3db/m3/src/dbnode/storage/bootstrap/result"
    38  	"github.com/m3db/m3/src/dbnode/storage/index/convert"
    39  	"github.com/m3db/m3/src/m3ninx/index/segment/mem"
    40  	idxpersist "github.com/m3db/m3/src/m3ninx/persist"
    41  	"github.com/m3db/m3/src/x/ident"
    42  	xtime "github.com/m3db/m3/src/x/time"
    43  )
    44  
    45  type testTimesOptions struct {
    46  	numBlocks int
    47  }
    48  
    49  type testBootstrapIndexTimes struct {
    50  	start           xtime.UnixNano
    51  	end             xtime.UnixNano
    52  	shardTimeRanges result.ShardTimeRanges
    53  }
    54  
    55  func newTestBootstrapIndexTimes(
    56  	opts testTimesOptions,
    57  ) testBootstrapIndexTimes {
    58  	var (
    59  		start, end xtime.UnixNano
    60  		at         = xtime.Now()
    61  	)
    62  	switch opts.numBlocks {
    63  	case 2:
    64  		start = at.Truncate(testBlockSize)
    65  		indexStart := start.Truncate(testIndexBlockSize)
    66  		for !start.Equal(indexStart) {
    67  			// make sure data blocks overlap, test block size is 2h
    68  			// and test index block size is 4h
    69  			start = start.Add(testBlockSize)
    70  			indexStart = start.Truncate(testIndexBlockSize)
    71  		}
    72  		end = start.Add(time.Duration(opts.numBlocks) * testIndexBlockSize)
    73  	case 3:
    74  		end = at.Truncate(testIndexBlockSize)
    75  		start = end.Add(time.Duration(-1*opts.numBlocks) * testBlockSize)
    76  	default:
    77  		panic("unexpected")
    78  	}
    79  
    80  	shardTimeRanges := result.NewShardTimeRanges().Set(
    81  		testShard,
    82  		xtime.NewRanges(xtime.Range{
    83  			Start: start,
    84  			End:   end,
    85  		}),
    86  	)
    87  
    88  	return testBootstrapIndexTimes{
    89  		start:           start,
    90  		end:             end,
    91  		shardTimeRanges: shardTimeRanges,
    92  	}
    93  }
    94  
    95  type testSeriesBlocks [][]testSeries
    96  
    97  func testGoodTaggedSeriesDataBlocks() testSeriesBlocks {
    98  	fooSeries := struct {
    99  		id   string
   100  		tags map[string]string
   101  	}{
   102  		"foo",
   103  		map[string]string{"aaa": "bbb", "ccc": "ddd"},
   104  	}
   105  	dataBlocks := testSeriesBlocks{
   106  		[]testSeries{
   107  			{fooSeries.id, fooSeries.tags, []byte{0x1}},
   108  			{"bar", map[string]string{"eee": "fff", "ggg": "hhh"}, []byte{0x1}},
   109  			{"baz", map[string]string{"iii": "jjj", "kkk": "lll"}, []byte{0x1}},
   110  		},
   111  		[]testSeries{
   112  			{fooSeries.id, fooSeries.tags, []byte{0x2}},
   113  			{"qux", map[string]string{"mmm": "nnn", "ooo": "ppp"}, []byte{0x2}},
   114  			{"qaz", map[string]string{"qqq": "rrr", "sss": "ttt"}, []byte{0x2}},
   115  		},
   116  		[]testSeries{
   117  			{fooSeries.id, fooSeries.tags, []byte{0x3}},
   118  			{"qan", map[string]string{"uuu": "vvv", "www": "xxx"}, []byte{0x3}},
   119  			{"qam", map[string]string{"yyy": "zzz", "000": "111"}, []byte{0x3}},
   120  		},
   121  	}
   122  	return dataBlocks
   123  }
   124  
   125  func writeTSDBGoodTaggedSeriesDataFiles(
   126  	t require.TestingT,
   127  	dir string,
   128  	namespaceID ident.ID,
   129  	start xtime.UnixNano,
   130  ) {
   131  	dataBlocks := testGoodTaggedSeriesDataBlocks()
   132  
   133  	writeTSDBFiles(t, dir, namespaceID, testShard,
   134  		start, dataBlocks[0])
   135  	writeTSDBFiles(t, dir, namespaceID, testShard,
   136  		start.Add(testBlockSize), dataBlocks[1])
   137  	writeTSDBFiles(t, dir, namespaceID, testShard,
   138  		start.Add(2*testBlockSize), dataBlocks[2])
   139  }
   140  
   141  func writeTSDBPersistedIndexBlock(
   142  	t *testing.T,
   143  	dir string,
   144  	namespace namespace.Metadata,
   145  	start xtime.UnixNano,
   146  	shards map[uint32]struct{},
   147  	block []testSeries,
   148  ) {
   149  	seg, err := mem.NewSegment(mem.NewOptions())
   150  	require.NoError(t, err)
   151  
   152  	for _, series := range block {
   153  		d, err := convert.FromSeriesIDAndTags(series.ID(), series.Tags())
   154  		require.NoError(t, err)
   155  		exists, err := seg.ContainsID(series.ID().Bytes())
   156  		require.NoError(t, err)
   157  		if exists {
   158  			continue // duplicate
   159  		}
   160  		_, err = seg.Insert(d)
   161  		require.NoError(t, err)
   162  	}
   163  
   164  	err = seg.Seal()
   165  	require.NoError(t, err)
   166  
   167  	pm := newTestOptionsWithPersistManager(t, dir).PersistManager()
   168  	flush, err := pm.StartIndexPersist()
   169  	require.NoError(t, err)
   170  
   171  	preparedPersist, err := flush.PrepareIndex(persist.IndexPrepareOptions{
   172  		NamespaceMetadata: namespace,
   173  		BlockStart:        start,
   174  		FileSetType:       persist.FileSetFlushType,
   175  		Shards:            shards,
   176  	})
   177  	require.NoError(t, err)
   178  
   179  	err = preparedPersist.Persist(seg)
   180  	require.NoError(t, err)
   181  
   182  	result, err := preparedPersist.Close()
   183  	require.NoError(t, err)
   184  
   185  	for _, r := range result {
   186  		require.NoError(t, r.Close())
   187  	}
   188  
   189  	err = flush.DoneIndex()
   190  	require.NoError(t, err)
   191  }
   192  
   193  type expectedTaggedSeries struct {
   194  	indexBlockStart xtime.UnixNano
   195  	series          map[string]testSeries
   196  }
   197  
   198  func expectedTaggedSeriesWithOptions(
   199  	t require.TestingT,
   200  	start xtime.UnixNano,
   201  	opts testTimesOptions,
   202  ) []expectedTaggedSeries {
   203  	dataBlocks := testGoodTaggedSeriesDataBlocks()
   204  	switch opts.numBlocks {
   205  	case 2:
   206  		return []expectedTaggedSeries{
   207  			{
   208  				indexBlockStart: start,
   209  				series: map[string]testSeries{
   210  					dataBlocks[0][0].id: dataBlocks[0][0],
   211  					dataBlocks[0][1].id: dataBlocks[0][1],
   212  					dataBlocks[0][2].id: dataBlocks[0][2],
   213  					dataBlocks[1][1].id: dataBlocks[1][1],
   214  					dataBlocks[1][2].id: dataBlocks[1][2],
   215  				},
   216  			},
   217  			{
   218  				indexBlockStart: start.Add(testIndexBlockSize),
   219  				series: map[string]testSeries{
   220  					dataBlocks[2][0].id: dataBlocks[2][0],
   221  					dataBlocks[2][1].id: dataBlocks[2][1],
   222  					dataBlocks[2][2].id: dataBlocks[2][2],
   223  				},
   224  			},
   225  		}
   226  	case 3:
   227  		return []expectedTaggedSeries{
   228  			{
   229  				indexBlockStart: start,
   230  				series: map[string]testSeries{
   231  					dataBlocks[0][0].id: dataBlocks[0][0],
   232  					dataBlocks[0][1].id: dataBlocks[0][1],
   233  					dataBlocks[0][2].id: dataBlocks[0][2],
   234  				},
   235  			},
   236  			{
   237  				indexBlockStart: start.Add(testIndexBlockSize),
   238  				series: map[string]testSeries{
   239  					dataBlocks[1][1].id: dataBlocks[1][1],
   240  					dataBlocks[1][2].id: dataBlocks[1][2],
   241  					dataBlocks[2][0].id: dataBlocks[2][0],
   242  					dataBlocks[2][1].id: dataBlocks[2][1],
   243  					dataBlocks[2][2].id: dataBlocks[2][2],
   244  				},
   245  			},
   246  		}
   247  	default:
   248  		require.FailNow(t, "unsupported test times options")
   249  	}
   250  	return nil
   251  }
   252  
   253  func validateGoodTaggedSeries(
   254  	t require.TestingT,
   255  	start xtime.UnixNano,
   256  	indexResults result.IndexResults,
   257  	opts testTimesOptions,
   258  ) {
   259  	require.Equal(t, 2, len(indexResults))
   260  
   261  	expectedSeriesByBlock := expectedTaggedSeriesWithOptions(t, start, opts)
   262  	for _, expected := range expectedSeriesByBlock {
   263  		expectedAt := expected.indexBlockStart
   264  		indexBlockByVolumeType, ok := indexResults[expectedAt]
   265  		require.True(t, ok)
   266  		for _, indexBlock := range indexBlockByVolumeType.Iter() {
   267  			require.Equal(t, 1, len(indexBlock.Segments()))
   268  			for _, seg := range indexBlock.Segments() {
   269  				reader, err := seg.Segment().Reader()
   270  				require.NoError(t, err)
   271  
   272  				docs, err := reader.AllDocs()
   273  				require.NoError(t, err)
   274  
   275  				matches := map[string]struct{}{}
   276  				for docs.Next() {
   277  					curr := docs.Current()
   278  
   279  					_, ok := matches[string(curr.ID)]
   280  					require.False(t, ok)
   281  					matches[string(curr.ID)] = struct{}{}
   282  
   283  					series, ok := expected.series[string(curr.ID)]
   284  					require.True(t, ok)
   285  
   286  					matchingTags := map[string]struct{}{}
   287  					for _, tag := range curr.Fields {
   288  						_, ok := matchingTags[string(tag.Name)]
   289  						require.False(t, ok)
   290  						matchingTags[string(tag.Name)] = struct{}{}
   291  
   292  						tagValue, ok := series.tags[string(tag.Name)]
   293  						require.True(t, ok)
   294  
   295  						require.Equal(t, tagValue, string(tag.Value))
   296  					}
   297  					require.Equal(t, len(series.tags), len(matchingTags))
   298  				}
   299  				require.NoError(t, docs.Err())
   300  				require.NoError(t, docs.Close())
   301  
   302  				require.Equal(t, len(expected.series), len(matches))
   303  			}
   304  		}
   305  	}
   306  }
   307  
   308  func TestBootstrapIndex(t *testing.T) {
   309  	dir := createTempDir(t)
   310  	defer os.RemoveAll(dir)
   311  
   312  	timesOpts := testTimesOptions{
   313  		numBlocks: 2,
   314  	}
   315  	times := newTestBootstrapIndexTimes(timesOpts)
   316  
   317  	writeTSDBGoodTaggedSeriesDataFiles(t, dir, testNs1ID, times.start)
   318  
   319  	opts := newTestOptionsWithPersistManager(t, dir)
   320  	scope := tally.NewTestScope("", nil)
   321  	opts = opts.SetInstrumentOptions(opts.InstrumentOptions().SetMetricsScope(scope))
   322  
   323  	// Should always be run with persist enabled.
   324  	runOpts := testDefaultRunOpts.
   325  		SetPersistConfig(bootstrap.PersistConfig{Enabled: true})
   326  
   327  	fsSrc, err := newFileSystemSource(opts)
   328  	require.NoError(t, err)
   329  
   330  	src, ok := fsSrc.(*fileSystemSource)
   331  	require.True(t, ok)
   332  
   333  	nsMD := testNsMetadata(t)
   334  	tester := bootstrap.BuildNamespacesTesterWithFilesystemOptions(t, runOpts,
   335  		times.shardTimeRanges, opts.FilesystemOptions(), nsMD)
   336  	defer tester.Finish()
   337  
   338  	tester.TestReadWith(src)
   339  	indexResults := tester.ResultForNamespace(nsMD.ID()).IndexResult.IndexResults()
   340  
   341  	// Check that single persisted segment got written out
   342  	infoFiles := fs.ReadIndexInfoFiles(fs.ReadIndexInfoFilesOptions{
   343  		FilePathPrefix:   src.fsopts.FilePathPrefix(),
   344  		Namespace:        testNs1ID,
   345  		ReaderBufferSize: src.fsopts.InfoReaderBufferSize(),
   346  	})
   347  	require.Equal(t, 1, len(infoFiles))
   348  
   349  	for _, infoFile := range infoFiles {
   350  		require.NoError(t, infoFile.Err.Error())
   351  		require.Equal(t, times.start, xtime.UnixNano(infoFile.Info.BlockStart))
   352  		require.Equal(t, testIndexBlockSize, time.Duration(infoFile.Info.BlockSize))
   353  		require.Equal(t, persist.FileSetFlushType, persist.FileSetType(infoFile.Info.FileType))
   354  		require.Equal(t, 1, len(infoFile.Info.Segments))
   355  		require.Equal(t, 1, len(infoFile.Info.Shards))
   356  		require.Equal(t, testShard, infoFile.Info.Shards[0])
   357  	}
   358  
   359  	// Check that the segment is not a mutable segment for this block
   360  	blockByVolumeType, ok := indexResults[times.start]
   361  	require.True(t, ok)
   362  	block, ok := blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   363  	require.True(t, ok)
   364  	require.Equal(t, 1, len(block.Segments()))
   365  	segment := block.Segments()[0]
   366  	require.True(t, ok)
   367  	require.True(t, segment.IsPersisted())
   368  
   369  	// Check that the second segment is mutable and was not written out
   370  	blockByVolumeType, ok = indexResults[times.start.Add(testIndexBlockSize)]
   371  	require.True(t, ok)
   372  	block, ok = blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   373  	require.True(t, ok)
   374  	require.Equal(t, 1, len(block.Segments()))
   375  	segment = block.Segments()[0]
   376  	require.True(t, ok)
   377  	require.False(t, segment.IsPersisted())
   378  
   379  	// Validate results
   380  	validateGoodTaggedSeries(t, times.start, indexResults, timesOpts)
   381  
   382  	// Validate that wrote the block out (and no index blocks
   383  	// were read as existing index blocks on disk)
   384  	counters := scope.Snapshot().Counters()
   385  	require.Equal(t, int64(0), counters["fs-bootstrapper.persist-index-blocks-read+"].Value())
   386  	require.Equal(t, int64(1), counters["fs-bootstrapper.persist-index-blocks-write+"].Value())
   387  }
   388  
   389  func TestBootstrapIndexIgnoresPersistConfigIfSnapshotType(t *testing.T) {
   390  	dir := createTempDir(t)
   391  	defer os.RemoveAll(dir)
   392  
   393  	timesOpts := testTimesOptions{
   394  		numBlocks: 2,
   395  	}
   396  	times := newTestBootstrapIndexTimes(timesOpts)
   397  
   398  	writeTSDBGoodTaggedSeriesDataFiles(t, dir, testNs1ID, times.start)
   399  
   400  	opts := newTestOptionsWithPersistManager(t, dir)
   401  	scope := tally.NewTestScope("", nil)
   402  	opts = opts.SetInstrumentOptions(opts.InstrumentOptions().SetMetricsScope(scope))
   403  
   404  	runOpts := testDefaultRunOpts.
   405  		SetPersistConfig(bootstrap.PersistConfig{
   406  			Enabled:     true,
   407  			FileSetType: persist.FileSetSnapshotType,
   408  		})
   409  
   410  	fsSrc, err := newFileSystemSource(opts)
   411  	require.NoError(t, err)
   412  
   413  	src, ok := fsSrc.(*fileSystemSource)
   414  	require.True(t, ok)
   415  
   416  	nsMD := testNsMetadata(t)
   417  	tester := bootstrap.BuildNamespacesTesterWithFilesystemOptions(t, runOpts,
   418  		times.shardTimeRanges, opts.FilesystemOptions(), nsMD)
   419  	defer tester.Finish()
   420  
   421  	tester.TestReadWith(src)
   422  	indexResults := tester.ResultForNamespace(nsMD.ID()).IndexResult.IndexResults()
   423  
   424  	// Check that not segments were written out
   425  	infoFiles := fs.ReadIndexInfoFiles(fs.ReadIndexInfoFilesOptions{
   426  		FilePathPrefix:   src.fsopts.FilePathPrefix(),
   427  		Namespace:        testNs1ID,
   428  		ReaderBufferSize: src.fsopts.InfoReaderBufferSize(),
   429  	})
   430  	require.Equal(t, 0, len(infoFiles))
   431  
   432  	// Check that both segments are mutable
   433  	blockByVolumeType, ok := indexResults[times.start]
   434  	require.True(t, ok)
   435  	block, ok := blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   436  	require.True(t, ok)
   437  	require.Equal(t, 1, len(block.Segments()))
   438  	segment := block.Segments()[0]
   439  	require.True(t, ok)
   440  	require.False(t, segment.IsPersisted())
   441  
   442  	blockByVolumeType, ok = indexResults[times.start.Add(testIndexBlockSize)]
   443  	require.True(t, ok)
   444  	block, ok = blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   445  	require.True(t, ok)
   446  	require.Equal(t, 1, len(block.Segments()))
   447  	segment = block.Segments()[0]
   448  	require.True(t, ok)
   449  	require.False(t, segment.IsPersisted())
   450  
   451  	// Validate results
   452  	validateGoodTaggedSeries(t, times.start, indexResults, timesOpts)
   453  
   454  	// Validate that no index blocks were read from disk and that no files were written out
   455  	counters := scope.Snapshot().Counters()
   456  	require.Equal(t, int64(0), counters["fs-bootstrapper.persist-index-blocks-read+"].Value())
   457  	require.Equal(t, int64(0), counters["fs-bootstrapper.persist-index-blocks-write+"].Value())
   458  	tester.EnsureNoWrites()
   459  }
   460  
   461  func TestBootstrapIndexWithPersistPrefersPersistedIndexBlocks(t *testing.T) {
   462  	dir := createTempDir(t)
   463  	defer os.RemoveAll(dir)
   464  
   465  	timesOpts := testTimesOptions{
   466  		numBlocks: 2,
   467  	}
   468  	times := newTestBootstrapIndexTimes(timesOpts)
   469  
   470  	// Write data files
   471  	writeTSDBGoodTaggedSeriesDataFiles(t, dir, testNs1ID, times.start)
   472  
   473  	// Now write index block segment from first two data blocks
   474  	testData := testGoodTaggedSeriesDataBlocks()
   475  	shards := map[uint32]struct{}{testShard: {}}
   476  	writeTSDBPersistedIndexBlock(t, dir, testNsMetadata(t), times.start, shards,
   477  		append(testData[0], testData[1]...))
   478  
   479  	opts := newTestOptionsWithPersistManager(t, dir)
   480  	scope := tally.NewTestScope("", nil)
   481  	opts = opts.SetInstrumentOptions(opts.InstrumentOptions().SetMetricsScope(scope))
   482  
   483  	runOpts := testDefaultRunOpts.
   484  		SetPersistConfig(bootstrap.PersistConfig{Enabled: true})
   485  
   486  	fsSrc, err := newFileSystemSource(opts)
   487  	require.NoError(t, err)
   488  
   489  	src, ok := fsSrc.(*fileSystemSource)
   490  	require.True(t, ok)
   491  
   492  	nsMD := testNsMetadata(t)
   493  	tester := bootstrap.BuildNamespacesTesterWithFilesystemOptions(t, runOpts,
   494  		times.shardTimeRanges, opts.FilesystemOptions(), nsMD)
   495  	defer tester.Finish()
   496  
   497  	tester.TestReadWith(src)
   498  	indexResults := tester.ResultForNamespace(nsMD.ID()).IndexResult.IndexResults()
   499  
   500  	// Check that the segment is not a mutable segment for this block
   501  	// and came from disk
   502  	blockByVolumeType, ok := indexResults[times.start]
   503  	require.True(t, ok)
   504  	block, ok := blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   505  	require.True(t, ok)
   506  	require.Equal(t, 1, len(block.Segments()))
   507  	segment := block.Segments()[0]
   508  	require.True(t, ok)
   509  	require.True(t, segment.IsPersisted())
   510  
   511  	// Check that the second segment is mutable
   512  	blockByVolumeType, ok = indexResults[times.start.Add(testIndexBlockSize)]
   513  	require.True(t, ok)
   514  	block, ok = blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   515  	require.True(t, ok)
   516  	require.Equal(t, 1, len(block.Segments()))
   517  	segment = block.Segments()[0]
   518  	require.True(t, ok)
   519  	require.False(t, segment.IsPersisted())
   520  
   521  	// Validate results
   522  	validateGoodTaggedSeries(t, times.start, indexResults, timesOpts)
   523  
   524  	// Validate that read the block and no blocks were written
   525  	// (ensure persist config didn't write it back out again)
   526  	counters := scope.Snapshot().Counters()
   527  	require.Equal(t, int64(1), counters["fs-bootstrapper.persist-index-blocks-read+"].Value())
   528  	require.Equal(t, int64(0), counters["fs-bootstrapper.persist-index-blocks-write+"].Value())
   529  	tester.EnsureNoWrites()
   530  }
   531  
   532  // TODO: Make this test actually exercise the case at the retention edge,
   533  // right now it only builds a partial segment for the second of three index
   534  // blocks it is trying to build.
   535  func TestBootstrapIndexWithPersistForIndexBlockAtRetentionEdge(t *testing.T) {
   536  	tests := []testOptions{
   537  		{
   538  			name:                         "now",
   539  			now:                          time.Now(),
   540  			expectedInfoFiles:            2,
   541  			expectedSegmentsFirstBlock:   1,
   542  			expectedSegmentsSecondBlock:  1,
   543  			expectedOutOfRetentionBlocks: 0,
   544  		},
   545  		{
   546  			name:                         "now + 4h",
   547  			now:                          time.Now().Add(time.Hour * 4),
   548  			expectedInfoFiles:            1,
   549  			expectedSegmentsFirstBlock:   0,
   550  			expectedSegmentsSecondBlock:  1,
   551  			expectedOutOfRetentionBlocks: 1,
   552  		},
   553  		{
   554  			name:                         "now + 8h",
   555  			now:                          time.Now().Add(time.Hour * 8),
   556  			expectedInfoFiles:            0,
   557  			expectedSegmentsFirstBlock:   0,
   558  			expectedSegmentsSecondBlock:  0,
   559  			expectedOutOfRetentionBlocks: 2,
   560  		},
   561  	}
   562  	for _, test := range tests {
   563  		// t.Run(test.name, func(t *testing.T) {
   564  		testBootstrapIndexWithPersistForIndexBlockAtRetentionEdge(t, test)
   565  		// })
   566  	}
   567  }
   568  
   569  type testOptions struct {
   570  	name                         string
   571  	now                          time.Time
   572  	expectedInfoFiles            int
   573  	expectedSegmentsFirstBlock   int
   574  	expectedSegmentsSecondBlock  int
   575  	expectedOutOfRetentionBlocks int64
   576  }
   577  
   578  func testBootstrapIndexWithPersistForIndexBlockAtRetentionEdge(t *testing.T, test testOptions) {
   579  	dir := createTempDir(t)
   580  	defer func() {
   581  		require.NoError(t, os.RemoveAll(dir))
   582  	}()
   583  
   584  	timesOpts := testTimesOptions{
   585  		numBlocks: 3,
   586  	}
   587  	times := newTestBootstrapIndexTimes(timesOpts)
   588  	firstIndexBlockStart := times.start.Truncate(testIndexBlockSize)
   589  
   590  	writeTSDBGoodTaggedSeriesDataFiles(t, dir, testNs1ID, times.start)
   591  
   592  	opts := newTestOptionsWithPersistManager(t, dir)
   593  
   594  	scope := tally.NewTestScope("", nil)
   595  	opts = opts.
   596  		SetInstrumentOptions(opts.InstrumentOptions().SetMetricsScope(scope))
   597  
   598  	at := test.now
   599  	resultOpts := opts.ResultOptions()
   600  	clockOpts := resultOpts.ClockOptions().
   601  		SetNowFn(func() time.Time {
   602  			return at
   603  		})
   604  	opts = opts.SetResultOptions(resultOpts.SetClockOptions(clockOpts))
   605  
   606  	runOpts := testDefaultRunOpts.
   607  		SetPersistConfig(bootstrap.PersistConfig{Enabled: true})
   608  
   609  	fsSrc, err := newFileSystemSource(opts)
   610  	require.NoError(t, err)
   611  
   612  	src, ok := fsSrc.(*fileSystemSource)
   613  	require.True(t, ok)
   614  
   615  	retentionPeriod := testBlockSize
   616  	for {
   617  		// Make sure that retention is set to end half way through the first block
   618  		flushStart := retention.FlushTimeStartForRetentionPeriod(
   619  			retentionPeriod, testBlockSize, xtime.Now())
   620  		if flushStart.Before(firstIndexBlockStart.Add(testIndexBlockSize)) {
   621  			break
   622  		}
   623  		retentionPeriod += testBlockSize
   624  	}
   625  	ropts := testRetentionOptions.
   626  		SetBlockSize(testBlockSize).
   627  		SetRetentionPeriod(retentionPeriod)
   628  	ns, err := namespace.NewMetadata(testNs1ID, testNamespaceOptions.
   629  		SetRetentionOptions(ropts).
   630  		SetIndexOptions(testNamespaceIndexOptions.
   631  			SetEnabled(true).
   632  			SetBlockSize(testIndexBlockSize)))
   633  	require.NoError(t, err)
   634  
   635  	// NB(bodu): Simulate requesting bootstrapping of two whole index blocks instead of 3 data blocks (1.5 index blocks).
   636  	times.shardTimeRanges = result.NewShardTimeRanges().Set(
   637  		testShard,
   638  		xtime.NewRanges(xtime.Range{
   639  			Start: firstIndexBlockStart,
   640  			End:   times.end,
   641  		}),
   642  	)
   643  	tester := bootstrap.BuildNamespacesTesterWithFilesystemOptions(t, runOpts,
   644  		times.shardTimeRanges, opts.FilesystemOptions(), ns)
   645  	defer tester.Finish()
   646  
   647  	tester.TestReadWith(src)
   648  	indexResults := tester.ResultForNamespace(ns.ID()).IndexResult.IndexResults()
   649  
   650  	// Check that single persisted segment got written out
   651  	infoFiles := fs.ReadIndexInfoFiles(fs.ReadIndexInfoFilesOptions{
   652  		FilePathPrefix:   src.fsopts.FilePathPrefix(),
   653  		Namespace:        testNs1ID,
   654  		ReaderBufferSize: src.fsopts.InfoReaderBufferSize(),
   655  	})
   656  	require.Equal(t, test.expectedInfoFiles, len(infoFiles), "index info files")
   657  
   658  	for _, infoFile := range infoFiles {
   659  		require.NoError(t, infoFile.Err.Error())
   660  
   661  		if infoFile.Info.BlockStart == int64(firstIndexBlockStart) {
   662  			expectedStart := times.end.Add(-2 * testIndexBlockSize)
   663  			require.Equal(t, int64(expectedStart), infoFile.Info.BlockStart,
   664  				fmt.Sprintf("expected=%s, actual=%v",
   665  					expectedStart, xtime.UnixNano(infoFile.Info.BlockStart)))
   666  		} else {
   667  			expectedStart := times.end.Add(-1 * testIndexBlockSize)
   668  			require.Equal(t, int64(expectedStart), infoFile.Info.BlockStart,
   669  				fmt.Sprintf("expected=%s, actual=%v",
   670  					expectedStart, xtime.UnixNano(infoFile.Info.BlockStart)))
   671  		}
   672  
   673  		require.Equal(t, testIndexBlockSize, time.Duration(infoFile.Info.BlockSize))
   674  		require.Equal(t, persist.FileSetFlushType, persist.FileSetType(infoFile.Info.FileType))
   675  		require.Equal(t, 1, len(infoFile.Info.Segments))
   676  		require.Equal(t, 1, len(infoFile.Info.Shards))
   677  		require.Equal(t, testShard, infoFile.Info.Shards[0])
   678  	}
   679  
   680  	// Check that the segment is not a mutable segment
   681  	blockByVolumeType, ok := indexResults[firstIndexBlockStart]
   682  	require.True(t, ok)
   683  	block, ok := blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   684  	require.True(t, ok)
   685  	require.Equal(t, test.expectedSegmentsFirstBlock, len(block.Segments()), "first block segment count")
   686  	if len(block.Segments()) > 0 {
   687  		segment := block.Segments()[0]
   688  		require.True(t, segment.IsPersisted(), "should be persisted")
   689  	}
   690  
   691  	// Check that the second is not a mutable segment
   692  	blockByVolumeType, ok = indexResults[firstIndexBlockStart.Add(testIndexBlockSize)]
   693  	require.True(t, ok)
   694  	block, ok = blockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   695  	require.True(t, ok)
   696  	require.Equal(t, test.expectedSegmentsSecondBlock, len(block.Segments()), "second block segment count")
   697  	if len(block.Segments()) > 0 {
   698  		segment := block.Segments()[0]
   699  		require.True(t, segment.IsPersisted(), "should be persisted")
   700  	}
   701  
   702  	// Validate results
   703  	if test.expectedSegmentsFirstBlock > 0 {
   704  		validateGoodTaggedSeries(t, firstIndexBlockStart, indexResults, timesOpts)
   705  	}
   706  
   707  	// Validate that wrote the block out (and no index blocks
   708  	// were read as existing index blocks on disk)
   709  	counters := scope.Snapshot().Counters()
   710  	require.Equal(t, int64(0),
   711  		counters["fs-bootstrapper.persist-index-blocks-read+"].Value(), "index blocks read")
   712  	require.Equal(t, int64(test.expectedInfoFiles),
   713  		counters["fs-bootstrapper.persist-index-blocks-write+"].Value(), "index blocks")
   714  	require.Equal(t, test.expectedOutOfRetentionBlocks,
   715  		counters["fs-bootstrapper.persist-index-blocks-out-of-retention+"].Value(), "out of retention blocks")
   716  	tester.EnsureNoWrites()
   717  }