github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/persist/fs/index_lookup_test.go (about)

     1  // Copyright (c) 2017 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  package fs
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"sort"
    28  	"strconv"
    29  	"sync"
    30  	"testing"
    31  
    32  	"github.com/m3db/m3/src/dbnode/digest"
    33  	"github.com/m3db/m3/src/dbnode/persist/fs/msgpack"
    34  	"github.com/m3db/m3/src/dbnode/persist/schema"
    35  	"github.com/m3db/m3/src/x/ident"
    36  	"github.com/m3db/m3/src/x/mmap"
    37  
    38  	"github.com/stretchr/testify/require"
    39  )
    40  
    41  func TestNewNearestIndexOffsetDetectsUnsortedFiles(t *testing.T) {
    42  	// Create a slice of out-of-order index summary entries
    43  	outOfOrderSummaries := []schema.IndexSummary{
    44  		{
    45  			Index:            0,
    46  			ID:               []byte("1"),
    47  			IndexEntryOffset: 0,
    48  		},
    49  		{
    50  			Index:            1,
    51  			ID:               []byte("0"),
    52  			IndexEntryOffset: 10,
    53  		},
    54  	}
    55  
    56  	// Create a temp file
    57  	file, err := ioutil.TempFile("", "index-lookup-sort")
    58  	require.NoError(t, err)
    59  	defer os.Remove(file.Name())
    60  
    61  	// Write out the out-of-order summaries into the temp file
    62  	writeSummariesEntries(t, file, outOfOrderSummaries)
    63  
    64  	// Prepare the digest reader
    65  	summariesFdWithDigest := digest.NewFdWithDigestReader(4096)
    66  	file.Seek(0, 0)
    67  	summariesFdWithDigest.Reset(file)
    68  
    69  	// Determine the expected digest
    70  	expectedDigest := calculateExpectedDigest(t, summariesFdWithDigest)
    71  
    72  	// Reset the digest reader
    73  	file.Seek(0, 0)
    74  	summariesFdWithDigest.Reset(file)
    75  
    76  	// Try and create the index lookup and make sure it detects the file is out
    77  	// of order
    78  	_, err = newNearestIndexOffsetLookupFromSummariesFile(
    79  		summariesFdWithDigest,
    80  		expectedDigest,
    81  		msgpack.NewDecoder(nil),
    82  		msgpack.NewByteDecoderStream(nil),
    83  		len(outOfOrderSummaries),
    84  		false,
    85  		mmap.ReporterOptions{},
    86  	)
    87  	expectedErr := fmt.Errorf("summaries file is not sorted: %s", file.Name())
    88  	require.Equal(t, expectedErr, err)
    89  }
    90  
    91  func TestCloneCannotBeCloned(t *testing.T) {
    92  	indexLookup := newNearestIndexOffsetLookup(nil, mmap.Descriptor{})
    93  	clone, err := indexLookup.concurrentClone()
    94  	require.NoError(t, err)
    95  
    96  	_, err = clone.concurrentClone()
    97  	require.Error(t, err)
    98  	require.NoError(t, indexLookup.close())
    99  	require.NoError(t, clone.close())
   100  }
   101  
   102  func TestClosingCloneDoesNotAffectParent(t *testing.T) {
   103  	indexSummaries := []schema.IndexSummary{
   104  		{
   105  			Index:            0,
   106  			ID:               []byte("0"),
   107  			IndexEntryOffset: 0,
   108  		},
   109  		{
   110  			Index:            1,
   111  			ID:               []byte("1"),
   112  			IndexEntryOffset: 10,
   113  		},
   114  	}
   115  
   116  	indexLookup := newIndexLookupWithSummaries(t, indexSummaries, false)
   117  	clone, err := indexLookup.concurrentClone()
   118  	require.NoError(t, err)
   119  	require.NoError(t, clone.close())
   120  	for _, summary := range indexSummaries {
   121  		id := ident.StringID(string(summary.ID))
   122  		require.NoError(t, err)
   123  		offset, err := clone.getNearestIndexFileOffset(id, newTestReusableSeekerResources())
   124  		require.NoError(t, err)
   125  		require.Equal(t, summary.IndexEntryOffset, offset)
   126  		id.Finalize()
   127  	}
   128  	require.NoError(t, indexLookup.close())
   129  }
   130  
   131  func TestParentAndClonesSafeForConcurrentUse(t *testing.T) {
   132  	testParentAndClonesSafeForConcurrentUse(t, false)
   133  }
   134  
   135  func TestParentAndClonesSafeForConcurrentUseForceMmapMemory(t *testing.T) {
   136  	testParentAndClonesSafeForConcurrentUse(t, true)
   137  }
   138  
   139  func testParentAndClonesSafeForConcurrentUse(t *testing.T, forceMmapMemory bool) {
   140  	numSummaries := 1000
   141  	numClones := 10
   142  
   143  	// Create test summary entries
   144  	indexSummaries := []schema.IndexSummary{}
   145  	for i := 0; i < numSummaries; i++ {
   146  		indexSummaries = append(indexSummaries, schema.IndexSummary{
   147  			Index:            int64(i),
   148  			ID:               []byte(strconv.Itoa(i)),
   149  			IndexEntryOffset: int64(10 * i),
   150  		})
   151  	}
   152  	sort.Sort(sortableSummaries(indexSummaries))
   153  
   154  	// Create indexLookup and associated clones
   155  	indexLookup := newIndexLookupWithSummaries(t, indexSummaries, forceMmapMemory)
   156  	clones := []*nearestIndexOffsetLookup{}
   157  	for i := 0; i < numClones; i++ {
   158  		clone, err := indexLookup.concurrentClone()
   159  		require.NoError(t, err)
   160  		clones = append(clones, clone)
   161  	}
   162  
   163  	// Spin up a goroutine for each clone that looks up every offset. Use one waitgroup
   164  	// to make a best effort attempt to get all the goroutines active before they start
   165  	// doing work, and then another waitgroup to wait for them to all finish their work.
   166  	startWg := sync.WaitGroup{}
   167  	doneWg := sync.WaitGroup{}
   168  	startWg.Add(len(clones) + 1)
   169  	doneWg.Add(len(clones) + 1)
   170  
   171  	lookupOffsetsFunc := func(clone *nearestIndexOffsetLookup) {
   172  		startWg.Done()
   173  		startWg.Wait()
   174  		for _, summary := range indexSummaries {
   175  			id := ident.StringID(string(summary.ID))
   176  			offset, err := clone.getNearestIndexFileOffset(id, newTestReusableSeekerResources())
   177  			require.NoError(t, err)
   178  			require.Equal(t, summary.IndexEntryOffset, offset)
   179  			id.Finalize()
   180  		}
   181  		doneWg.Done()
   182  	}
   183  	go lookupOffsetsFunc(indexLookup)
   184  	for _, clone := range clones {
   185  		go lookupOffsetsFunc(clone)
   186  	}
   187  
   188  	// Wait for all workers to finish and then make sure everything can be cleaned
   189  	// up properly
   190  	doneWg.Wait()
   191  	require.NoError(t, indexLookup.close())
   192  	for _, clone := range clones {
   193  		require.NoError(t, clone.close())
   194  	}
   195  }
   196  
   197  // newIndexLookupWithSummaries will return a new index lookup that is backed by the provided
   198  // indexSummaries (in the order that they are provided).
   199  func newIndexLookupWithSummaries(
   200  	t *testing.T, indexSummaries []schema.IndexSummary, forceMmapMemory bool) *nearestIndexOffsetLookup {
   201  	// Create a temp file
   202  	file, err := ioutil.TempFile("", "index-lookup-sort")
   203  	require.NoError(t, err)
   204  	defer os.Remove(file.Name())
   205  
   206  	writeSummariesEntries(t, file, indexSummaries)
   207  
   208  	// Prepare the digest reader
   209  	summariesFdWithDigest := digest.NewFdWithDigestReader(4096)
   210  	file.Seek(0, 0)
   211  	summariesFdWithDigest.Reset(file)
   212  
   213  	// Determine the expected digest
   214  	expectedDigest := calculateExpectedDigest(t, summariesFdWithDigest)
   215  
   216  	// Reset the digest reader
   217  	file.Seek(0, 0)
   218  	summariesFdWithDigest.Reset(file)
   219  
   220  	// Try and create the index lookup and make sure it detects the file is out
   221  	// of order
   222  	indexLookup, err := newNearestIndexOffsetLookupFromSummariesFile(
   223  		summariesFdWithDigest,
   224  		expectedDigest,
   225  		msgpack.NewDecoder(nil),
   226  		msgpack.NewByteDecoderStream(nil),
   227  		len(indexSummaries),
   228  		forceMmapMemory,
   229  		mmap.ReporterOptions{},
   230  	)
   231  	require.NoError(t, err)
   232  	return indexLookup
   233  }
   234  
   235  func writeSummariesEntries(t *testing.T, fd *os.File, summaries []schema.IndexSummary) {
   236  	encoder := msgpack.NewEncoder()
   237  	for _, summary := range summaries {
   238  		encoder.Reset()
   239  		require.NoError(t, encoder.EncodeIndexSummary(summary))
   240  		_, err := fd.Write(encoder.Bytes())
   241  		require.NoError(t, err)
   242  	}
   243  }
   244  
   245  func calculateExpectedDigest(t *testing.T, digestReader digest.FdWithDigestReader) uint32 {
   246  	// Determine the size of the file
   247  	file := digestReader.Fd()
   248  	stat, err := file.Stat()
   249  	require.NoError(t, err)
   250  	fileSize := stat.Size()
   251  
   252  	// Calculate the digest
   253  	_, err = digestReader.Read(make([]byte, fileSize))
   254  	require.NoError(t, err)
   255  	return digestReader.Digest().Sum32()
   256  }
   257  
   258  type sortableSummaries []schema.IndexSummary
   259  
   260  func (s sortableSummaries) Len() int {
   261  	return len(s)
   262  }
   263  
   264  func (s sortableSummaries) Less(i, j int) bool {
   265  	return bytes.Compare(s[i].ID, s[j].ID) < 0
   266  }
   267  
   268  func (s sortableSummaries) Swap(i, j int) {
   269  	s[i], s[j] = s[j], s[i]
   270  }