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

     1  // Copyright (c) 2018 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 peers
    22  
    23  import (
    24  	"io/ioutil"
    25  	"os"
    26  	"sort"
    27  	"testing"
    28  	"time"
    29  
    30  	"github.com/m3db/m3/src/dbnode/client"
    31  	"github.com/m3db/m3/src/dbnode/namespace"
    32  	"github.com/m3db/m3/src/dbnode/persist/fs"
    33  	"github.com/m3db/m3/src/dbnode/retention"
    34  	"github.com/m3db/m3/src/dbnode/storage/block"
    35  	"github.com/m3db/m3/src/dbnode/storage/bootstrap"
    36  	"github.com/m3db/m3/src/dbnode/storage/bootstrap/result"
    37  	"github.com/m3db/m3/src/dbnode/storage/series"
    38  	"github.com/m3db/m3/src/dbnode/ts"
    39  	idxpersist "github.com/m3db/m3/src/m3ninx/persist"
    40  	"github.com/m3db/m3/src/x/checked"
    41  	"github.com/m3db/m3/src/x/ident"
    42  	xtime "github.com/m3db/m3/src/x/time"
    43  
    44  	"github.com/golang/mock/gomock"
    45  	"github.com/stretchr/testify/require"
    46  )
    47  
    48  var (
    49  	testShard            = uint32(0)
    50  	testFileMode         = os.FileMode(0666)
    51  	testDirMode          = os.ModeDir | os.FileMode(0755)
    52  	testWriterBufferSize = 10
    53  )
    54  
    55  func newTestFsOptions(filePathPrefix string) fs.Options {
    56  	return fs.NewOptions().
    57  		SetFilePathPrefix(filePathPrefix).
    58  		SetWriterBufferSize(testWriterBufferSize).
    59  		SetNewFileMode(testFileMode).
    60  		SetNewDirectoryMode(testDirMode)
    61  }
    62  
    63  func createTempDir(t *testing.T) string {
    64  	dir, err := ioutil.TempDir("", "foo")
    65  	require.NoError(t, err)
    66  	return dir
    67  }
    68  
    69  type testSeriesMetadata struct {
    70  	id   string
    71  	tags map[string]string
    72  	data []byte
    73  }
    74  
    75  func (s testSeriesMetadata) ID() ident.ID {
    76  	return ident.StringID(s.id)
    77  }
    78  
    79  func (s testSeriesMetadata) Tags() ident.Tags {
    80  	if s.tags == nil {
    81  		return ident.Tags{}
    82  	}
    83  
    84  	// Return in sorted order for deterministic order
    85  	var keys []string
    86  	for key := range s.tags {
    87  		keys = append(keys, key)
    88  	}
    89  	sort.Strings(keys)
    90  
    91  	var tags ident.Tags
    92  	for _, key := range keys {
    93  		tags.Append(ident.StringTag(key, s.tags[key]))
    94  	}
    95  
    96  	return tags
    97  }
    98  
    99  type testOptions struct {
   100  	name                string
   101  	indexBlockStart     xtime.UnixNano
   102  	expectedIndexBlocks int
   103  	retentionPeriod     time.Duration
   104  }
   105  
   106  func TestBootstrapIndex(t *testing.T) {
   107  	tests := []testOptions{
   108  		{
   109  			name:                "now",
   110  			indexBlockStart:     xtime.Now(),
   111  			expectedIndexBlocks: 12,
   112  			retentionPeriod:     48 * time.Hour,
   113  		},
   114  		{
   115  			name:                "now - 8h (out of retention)",
   116  			indexBlockStart:     xtime.Now().Add(-8 * time.Hour),
   117  			expectedIndexBlocks: 0,
   118  			retentionPeriod:     4 * time.Hour,
   119  		},
   120  	}
   121  	for _, test := range tests {
   122  		t.Run(test.name, func(t *testing.T) {
   123  			testBootstrapIndex(t, test)
   124  		})
   125  	}
   126  }
   127  
   128  //nolint
   129  func testBootstrapIndex(t *testing.T, test testOptions) {
   130  	ctrl := gomock.NewController(t)
   131  	defer ctrl.Finish()
   132  
   133  	opts := newTestDefaultOpts(t, ctrl)
   134  	opts = opts.SetResultOptions(result.NewOptions().
   135  		SetSeriesCachePolicy(series.CacheLRU))
   136  	pm, err := fs.NewPersistManager(opts.FilesystemOptions())
   137  	require.NoError(t, err)
   138  	opts = opts.SetPersistManager(pm)
   139  
   140  	blockSize := 2 * time.Hour
   141  	indexBlockSize := 2 * blockSize
   142  
   143  	ropts := retention.NewOptions().
   144  		SetBlockSize(blockSize).
   145  		SetRetentionPeriod(test.retentionPeriod)
   146  
   147  	nsMetadata := testNamespaceMetadata(t, func(opts namespace.Options) namespace.Options {
   148  		return opts.
   149  			SetRetentionOptions(ropts).
   150  			SetIndexOptions(opts.IndexOptions().
   151  				SetEnabled(true).
   152  				SetBlockSize(indexBlockSize))
   153  	})
   154  
   155  	at := test.indexBlockStart
   156  	start := at.Add(-ropts.RetentionPeriod()).Truncate(blockSize)
   157  	indexStart := start.Truncate(indexBlockSize)
   158  	for !start.Equal(indexStart) {
   159  		// make sure data blocks overlap, test block size is 2h
   160  		// and test index block size is 4h
   161  		start = start.Add(blockSize)
   162  		indexStart = start.Truncate(indexBlockSize)
   163  	}
   164  
   165  	fooSeries := struct {
   166  		id   string
   167  		tags map[string]string
   168  	}{
   169  		"foo",
   170  		map[string]string{"aaa": "bbb", "ccc": "ddd"},
   171  	}
   172  	dataBlocks := []struct {
   173  		blockStart xtime.UnixNano
   174  		series     []testSeriesMetadata
   175  	}{
   176  		{
   177  			blockStart: start,
   178  			series: []testSeriesMetadata{
   179  				{fooSeries.id, fooSeries.tags, []byte{0x1}},
   180  				{"bar", map[string]string{"eee": "fff", "ggg": "hhh"}, []byte{0x1}},
   181  				{"baz", map[string]string{"iii": "jjj", "kkk": "lll"}, []byte{0x1}},
   182  			},
   183  		},
   184  		{
   185  			blockStart: start.Add(blockSize),
   186  			series: []testSeriesMetadata{
   187  				{fooSeries.id, fooSeries.tags, []byte{0x2}},
   188  				{"qux", map[string]string{"mmm": "nnn", "ooo": "ppp"}, []byte{0x2}},
   189  				{"qaz", map[string]string{"qqq": "rrr", "sss": "ttt"}, []byte{0x2}},
   190  			},
   191  		},
   192  		{
   193  			blockStart: start.Add(2 * blockSize),
   194  			series: []testSeriesMetadata{
   195  				{fooSeries.id, fooSeries.tags, []byte{0x3}},
   196  				{"qan", map[string]string{"uuu": "vvv", "www": "xxx"}, []byte{0x3}},
   197  				{"qam", map[string]string{"yyy": "zzz", "000": "111"}, []byte{0x3}},
   198  			},
   199  		},
   200  	}
   201  
   202  	dir := createTempDir(t)
   203  	defer os.RemoveAll(dir)
   204  	opts = opts.SetFilesystemOptions(newTestFsOptions(dir))
   205  
   206  	end := start.Add(ropts.RetentionPeriod())
   207  
   208  	shardTimeRanges := result.NewShardTimeRanges().Set(
   209  		0,
   210  		xtime.NewRanges(xtime.Range{
   211  			Start: start,
   212  			End:   end,
   213  		}),
   214  	)
   215  
   216  	// data block start at the edge of retention so return those first.
   217  	var dataBlocksIdx int
   218  	mockAdminSession := client.NewMockAdminSession(ctrl)
   219  	mockAdminSession.EXPECT().
   220  		FetchBootstrapBlocksFromPeers(gomock.Any(),
   221  			gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
   222  		func(
   223  			_ namespace.Metadata,
   224  			_ uint32,
   225  			blockStart xtime.UnixNano,
   226  			blockEnd xtime.UnixNano,
   227  			_ result.Options,
   228  		) (result.ShardResult, error) {
   229  			goodID := ident.StringID("foo")
   230  			goodResult := result.NewShardResult(opts.ResultOptions())
   231  			for ; blockStart.Before(blockEnd); blockStart = blockStart.Add(blockSize) {
   232  				if dataBlocksIdx < len(dataBlocks) {
   233  					dataBlock := dataBlocks[dataBlocksIdx]
   234  					for _, s := range dataBlock.series {
   235  						head := checked.NewBytes(s.data, nil)
   236  						head.IncRef()
   237  						block := block.NewDatabaseBlock(blockStart, ropts.BlockSize(),
   238  							ts.Segment{Head: head}, testBlockOpts, namespace.Context{})
   239  						goodResult.AddBlock(s.ID(), s.Tags(), block)
   240  					}
   241  					dataBlocksIdx++
   242  					continue
   243  				}
   244  
   245  				head := checked.NewBytes([]byte{0x1}, nil)
   246  				head.IncRef()
   247  				fooBlock := block.NewDatabaseBlock(blockStart, ropts.BlockSize(),
   248  					ts.Segment{Head: head}, testBlockOpts, namespace.Context{})
   249  				goodResult.AddBlock(goodID, ident.NewTags(
   250  					ident.StringTag("aaa", "bbb"),
   251  					ident.StringTag("ccc", "ddd"),
   252  				), fooBlock)
   253  			}
   254  			return goodResult, nil
   255  		}).AnyTimes()
   256  
   257  	mockAdminClient := client.NewMockAdminClient(ctrl)
   258  	mockAdminClient.EXPECT().DefaultAdminSession().Return(mockAdminSession, nil).AnyTimes()
   259  	opts = opts.SetAdminClient(mockAdminClient)
   260  	src, err := newPeersSource(opts)
   261  	require.NoError(t, err)
   262  	tester := bootstrap.BuildNamespacesTesterWithFilesystemOptions(t,
   263  		testRunOptsWithPersist, shardTimeRanges,
   264  		opts.FilesystemOptions(), nsMetadata)
   265  	defer tester.Finish()
   266  	tester.TestReadWith(src)
   267  
   268  	tester.TestUnfulfilledForNamespaceIsEmpty(nsMetadata)
   269  	results := tester.ResultForNamespace(nsMetadata.ID())
   270  	indexResults := results.IndexResult.IndexResults()
   271  	numIndexBlocks := 0
   272  	for _, indexBlockByVolumeType := range indexResults {
   273  		indexBlock, ok := indexBlockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   274  		require.True(t, ok)
   275  		if len(indexBlock.Segments()) != 0 {
   276  			numIndexBlocks++
   277  		}
   278  	}
   279  	require.Equal(t, test.expectedIndexBlocks, numIndexBlocks)
   280  
   281  	if numIndexBlocks > 0 {
   282  		for _, expected := range []*struct {
   283  			indexBlockStart xtime.UnixNano
   284  			series          map[string]testSeriesMetadata
   285  		}{
   286  			{
   287  				indexBlockStart: indexStart,
   288  				series: map[string]testSeriesMetadata{
   289  					dataBlocks[0].series[0].id: dataBlocks[0].series[0],
   290  					dataBlocks[0].series[1].id: dataBlocks[0].series[1],
   291  					dataBlocks[0].series[2].id: dataBlocks[0].series[2],
   292  					dataBlocks[1].series[1].id: dataBlocks[1].series[1],
   293  					dataBlocks[1].series[2].id: dataBlocks[1].series[2],
   294  				},
   295  			},
   296  			{
   297  				indexBlockStart: indexStart.Add(indexBlockSize),
   298  				series: map[string]testSeriesMetadata{
   299  					dataBlocks[2].series[0].id: dataBlocks[2].series[0],
   300  					dataBlocks[2].series[1].id: dataBlocks[2].series[1],
   301  					dataBlocks[2].series[2].id: dataBlocks[2].series[2],
   302  				},
   303  			},
   304  		} {
   305  			expectedAt := expected.indexBlockStart
   306  			indexBlockByVolumeType, ok := indexResults[expectedAt]
   307  			require.True(t, ok)
   308  			indexBlock, ok := indexBlockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   309  			require.True(t, ok)
   310  			for _, seg := range indexBlock.Segments() {
   311  				reader, err := seg.Segment().Reader()
   312  				require.NoError(t, err)
   313  
   314  				docs, err := reader.AllDocs()
   315  				require.NoError(t, err)
   316  
   317  				matches := map[string]struct{}{}
   318  				for docs.Next() {
   319  					curr := docs.Current()
   320  
   321  					_, ok := matches[string(curr.ID)]
   322  					require.False(t, ok)
   323  					matches[string(curr.ID)] = struct{}{}
   324  
   325  					series, ok := expected.series[string(curr.ID)]
   326  					require.True(t, ok)
   327  
   328  					matchingTags := map[string]struct{}{}
   329  					for _, tag := range curr.Fields {
   330  						_, ok := matchingTags[string(tag.Name)]
   331  						require.False(t, ok)
   332  						matchingTags[string(tag.Name)] = struct{}{}
   333  
   334  						tagValue, ok := series.tags[string(tag.Name)]
   335  						require.True(t, ok)
   336  
   337  						require.Equal(t, tagValue, string(tag.Value))
   338  					}
   339  					require.Equal(t, len(series.tags), len(matchingTags))
   340  				}
   341  				require.NoError(t, docs.Err())
   342  				require.NoError(t, docs.Close())
   343  
   344  				require.Equal(t, len(expected.series), len(matches))
   345  			}
   346  		}
   347  
   348  		t1 := indexStart
   349  		t2 := indexStart.Add(indexBlockSize)
   350  		t3 := t2.Add(indexBlockSize)
   351  
   352  		indexBlockByVolumeType, ok := indexResults[t1]
   353  		require.True(t, ok)
   354  		blk1, ok := indexBlockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   355  		require.True(t, ok)
   356  		assertShardRangesEqual(t, result.NewShardTimeRangesFromRange(t1, t2, 0), blk1.Fulfilled())
   357  
   358  		indexBlockByVolumeType, ok = indexResults[t2]
   359  		require.True(t, ok)
   360  		blk2, ok := indexBlockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   361  		require.True(t, ok)
   362  		assertShardRangesEqual(t, result.NewShardTimeRangesFromRange(t2, t3, 0), blk2.Fulfilled())
   363  
   364  		for _, indexBlockByVolumeType := range indexResults {
   365  			if indexBlockByVolumeType.BlockStart().Equal(t1) || indexBlockByVolumeType.BlockStart().Equal(t2) {
   366  				continue // already checked above
   367  			}
   368  			// rest should all be marked fulfilled despite no data, because we didn't see
   369  			// any errors in the response.
   370  			start := indexBlockByVolumeType.BlockStart()
   371  			end := start.Add(indexBlockSize)
   372  			blk, ok := indexBlockByVolumeType.GetBlock(idxpersist.DefaultIndexVolumeType)
   373  			require.True(t, ok)
   374  			assertShardRangesEqual(t, result.NewShardTimeRangesFromRange(start, end, 0), blk.Fulfilled())
   375  		}
   376  	}
   377  	tester.EnsureNoWrites()
   378  }
   379  
   380  func assertShardRangesEqual(t *testing.T, a, b result.ShardTimeRanges) {
   381  	ac := a.Copy()
   382  	ac.Subtract(b)
   383  	require.True(t, ac.IsEmpty())
   384  	bc := b.Copy()
   385  	bc.Subtract(a)
   386  	require.True(t, bc.IsEmpty())
   387  }