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

     1  // Copyright (c) 2016 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  	"errors"
    25  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/m3db/m3/src/dbnode/digest"
    32  	"github.com/m3db/m3/src/dbnode/persist"
    33  	"github.com/m3db/m3/src/dbnode/persist/schema"
    34  	"github.com/m3db/m3/src/x/ident"
    35  	"github.com/stretchr/testify/assert"
    36  	"github.com/stretchr/testify/require"
    37  )
    38  
    39  func newTestSeeker(filePathPrefix string) DataFileSetSeeker {
    40  	return NewSeeker(
    41  		filePathPrefix, testReaderBufferSize, testReaderBufferSize,
    42  		testBytesPool, false, testDefaultOpts)
    43  }
    44  
    45  func TestSeekEmptyIndex(t *testing.T) {
    46  	dir, err := ioutil.TempDir("", "testdb")
    47  	if err != nil {
    48  		t.Fatal(err)
    49  	}
    50  	filePathPrefix := filepath.Join(dir, "")
    51  	defer os.RemoveAll(dir)
    52  
    53  	w := newTestWriter(t, filePathPrefix)
    54  	writerOpts := DataWriterOpenOptions{
    55  		BlockSize: testBlockSize,
    56  		Identifier: FileSetFileIdentifier{
    57  			Namespace:  testNs1ID,
    58  			Shard:      0,
    59  			BlockStart: testWriterStart,
    60  		},
    61  	}
    62  	err = w.Open(writerOpts)
    63  	assert.NoError(t, err)
    64  	assert.NoError(t, w.Close())
    65  
    66  	resources := newTestReusableSeekerResources()
    67  	s := newTestSeeker(filePathPrefix)
    68  	err = s.Open(testNs1ID, 0, testWriterStart, 0, resources)
    69  	assert.NoError(t, err)
    70  	_, err = s.SeekByID(ident.StringID("foo"), resources)
    71  	assert.Error(t, err)
    72  	assert.Equal(t, errSeekIDNotFound, err)
    73  	assert.NoError(t, s.Close())
    74  }
    75  
    76  func TestSeekDataUnexpectedSize(t *testing.T) {
    77  	dir, err := ioutil.TempDir("", "testdb")
    78  	if err != nil {
    79  		t.Fatal(err)
    80  	}
    81  	filePathPrefix := filepath.Join(dir, "")
    82  	defer os.RemoveAll(dir)
    83  
    84  	w := newTestWriter(t, filePathPrefix)
    85  	writerOpts := DataWriterOpenOptions{
    86  		BlockSize: testBlockSize,
    87  		Identifier: FileSetFileIdentifier{
    88  			Namespace:  testNs1ID,
    89  			Shard:      0,
    90  			BlockStart: testWriterStart,
    91  		},
    92  	}
    93  	metadata := persist.NewMetadataFromIDAndTags(
    94  		ident.StringID("foo"),
    95  		ident.Tags{},
    96  		persist.MetadataOptions{})
    97  	err = w.Open(writerOpts)
    98  	assert.NoError(t, err)
    99  	dataFile := w.(*writer).dataFdWithDigest.Fd().Name()
   100  
   101  	assert.NoError(t, w.Write(metadata,
   102  		bytesRefd([]byte{1, 2, 3}),
   103  		digest.Checksum([]byte{1, 2, 3})))
   104  	assert.NoError(t, w.Close())
   105  
   106  	// Truncate one byte
   107  	assert.NoError(t, os.Truncate(dataFile, 1))
   108  
   109  	resources := newTestReusableSeekerResources()
   110  	s := newTestSeeker(filePathPrefix)
   111  	err = s.Open(testNs1ID, 0, testWriterStart, 0, resources)
   112  	assert.NoError(t, err)
   113  
   114  	_, err = s.SeekByID(ident.StringID("foo"), resources)
   115  	assert.Error(t, err)
   116  	assert.Equal(t, errors.New("unexpected EOF"), err)
   117  
   118  	assert.NoError(t, s.Close())
   119  }
   120  
   121  func TestSeekBadChecksum(t *testing.T) {
   122  	dir, err := ioutil.TempDir("", "testdb")
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  	filePathPrefix := filepath.Join(dir, "")
   127  	defer os.RemoveAll(dir)
   128  
   129  	w := newTestWriter(t, filePathPrefix)
   130  	writerOpts := DataWriterOpenOptions{
   131  		BlockSize: testBlockSize,
   132  		Identifier: FileSetFileIdentifier{
   133  			Namespace:  testNs1ID,
   134  			Shard:      0,
   135  			BlockStart: testWriterStart,
   136  		},
   137  	}
   138  	err = w.Open(writerOpts)
   139  	assert.NoError(t, err)
   140  
   141  	// Write data with wrong checksum
   142  	assert.NoError(t, w.Write(
   143  		persist.NewMetadataFromIDAndTags(
   144  			ident.StringID("foo"),
   145  			ident.Tags{},
   146  			persist.MetadataOptions{}),
   147  		bytesRefd([]byte{1, 2, 3}),
   148  		digest.Checksum([]byte{1, 2, 4})))
   149  	assert.NoError(t, w.Close())
   150  
   151  	resources := newTestReusableSeekerResources()
   152  	s := newTestSeeker(filePathPrefix)
   153  	err = s.Open(testNs1ID, 0, testWriterStart, 0, resources)
   154  	assert.NoError(t, err)
   155  
   156  	_, err = s.SeekByID(ident.StringID("foo"), resources)
   157  	assert.Error(t, err)
   158  	assert.Equal(t, errSeekChecksumMismatch, err)
   159  
   160  	assert.NoError(t, s.Close())
   161  }
   162  
   163  // TestSeek is a basic sanity test that we can seek IDs that have been written,
   164  // as well as received errSeekIDNotFound for IDs that were not written.
   165  func TestSeek(t *testing.T) {
   166  	dir, err := ioutil.TempDir("", "testdb")
   167  	if err != nil {
   168  		t.Fatal(err)
   169  	}
   170  	filePathPrefix := filepath.Join(dir, "")
   171  	defer os.RemoveAll(dir)
   172  
   173  	w := newTestWriter(t, filePathPrefix)
   174  	writerOpts := DataWriterOpenOptions{
   175  		BlockSize: testBlockSize,
   176  		Identifier: FileSetFileIdentifier{
   177  			Namespace:  testNs1ID,
   178  			Shard:      0,
   179  			BlockStart: testWriterStart,
   180  		},
   181  	}
   182  	err = w.Open(writerOpts)
   183  	assert.NoError(t, err)
   184  	assert.NoError(t, w.Write(
   185  		persist.NewMetadataFromIDAndTags(
   186  			ident.StringID("foo1"),
   187  			ident.NewTags(ident.StringTag("num", "1")),
   188  			persist.MetadataOptions{}),
   189  		bytesRefd([]byte{1, 2, 1}),
   190  		digest.Checksum([]byte{1, 2, 1})))
   191  	assert.NoError(t, w.Write(
   192  		persist.NewMetadataFromIDAndTags(
   193  			ident.StringID("foo2"),
   194  			ident.NewTags(ident.StringTag("num", "2")),
   195  			persist.MetadataOptions{}),
   196  		bytesRefd([]byte{1, 2, 2}),
   197  		digest.Checksum([]byte{1, 2, 2})))
   198  	assert.NoError(t, w.Write(
   199  		persist.NewMetadataFromIDAndTags(
   200  			ident.StringID("foo3"),
   201  			ident.NewTags(ident.StringTag("num", "3")),
   202  			persist.MetadataOptions{}),
   203  		bytesRefd([]byte{1, 2, 3}),
   204  		digest.Checksum([]byte{1, 2, 3})))
   205  	assert.NoError(t, w.Close())
   206  
   207  	resources := newTestReusableSeekerResources()
   208  	s := newTestSeeker(filePathPrefix)
   209  	err = s.Open(testNs1ID, 0, testWriterStart, 0, resources)
   210  	assert.NoError(t, err)
   211  
   212  	data, err := s.SeekByID(ident.StringID("foo3"), resources)
   213  	require.NoError(t, err)
   214  
   215  	data.IncRef()
   216  	defer data.DecRef()
   217  	assert.Equal(t, []byte{1, 2, 3}, data.Bytes())
   218  
   219  	data, err = s.SeekByID(ident.StringID("foo1"), resources)
   220  	require.NoError(t, err)
   221  
   222  	data.IncRef()
   223  	defer data.DecRef()
   224  	assert.Equal(t, []byte{1, 2, 1}, data.Bytes())
   225  
   226  	_, err = s.SeekByID(ident.StringID("foo"), resources)
   227  	assert.Error(t, err)
   228  	assert.Equal(t, errSeekIDNotFound, err)
   229  
   230  	data, err = s.SeekByID(ident.StringID("foo2"), resources)
   231  	require.NoError(t, err)
   232  
   233  	data.IncRef()
   234  	defer data.DecRef()
   235  	assert.Equal(t, []byte{1, 2, 2}, data.Bytes())
   236  
   237  	assert.NoError(t, s.Close())
   238  }
   239  
   240  // TestSeekIDNotExists is similar to TestSeek, but it covers more edge cases
   241  // around IDs not existing.
   242  func TestSeekIDNotExists(t *testing.T) {
   243  	dir, err := ioutil.TempDir("", "testdb")
   244  	if err != nil {
   245  		t.Fatal(err)
   246  	}
   247  	filePathPrefix := filepath.Join(dir, "")
   248  	defer os.RemoveAll(dir)
   249  
   250  	w := newTestWriter(t, filePathPrefix)
   251  	writerOpts := DataWriterOpenOptions{
   252  		BlockSize: testBlockSize,
   253  		Identifier: FileSetFileIdentifier{
   254  			Namespace:  testNs1ID,
   255  			Shard:      0,
   256  			BlockStart: testWriterStart,
   257  		},
   258  	}
   259  	err = w.Open(writerOpts)
   260  	assert.NoError(t, err)
   261  	assert.NoError(t, w.Write(
   262  		persist.NewMetadataFromIDAndTags(
   263  			ident.StringID("foo10"),
   264  			ident.Tags{},
   265  			persist.MetadataOptions{}),
   266  		bytesRefd([]byte{1, 2, 1}),
   267  		digest.Checksum([]byte{1, 2, 1})))
   268  	assert.NoError(t, w.Write(
   269  		persist.NewMetadataFromIDAndTags(
   270  			ident.StringID("foo20"),
   271  			ident.Tags{},
   272  			persist.MetadataOptions{}),
   273  		bytesRefd([]byte{1, 2, 2}),
   274  		digest.Checksum([]byte{1, 2, 2})))
   275  	assert.NoError(t, w.Write(
   276  		persist.NewMetadataFromIDAndTags(
   277  			ident.StringID("foo30"),
   278  			ident.Tags{},
   279  			persist.MetadataOptions{}),
   280  		bytesRefd([]byte{1, 2, 3}),
   281  		digest.Checksum([]byte{1, 2, 3})))
   282  	assert.NoError(t, w.Close())
   283  
   284  	resources := newTestReusableSeekerResources()
   285  	s := newTestSeeker(filePathPrefix)
   286  	err = s.Open(testNs1ID, 0, testWriterStart, 0, resources)
   287  	assert.NoError(t, err)
   288  
   289  	// Test errSeekIDNotFound when we scan far enough into the index file that
   290  	// we're sure that the ID we're looking for doesn't exist (because the index
   291  	// file is sorted). In this particular case, we would know foo21 doesn't exist
   292  	// once we've scanned all the way to foo30 (which does exist).
   293  	_, err = s.SeekByID(ident.StringID("foo21"), resources)
   294  	assert.Equal(t, errSeekIDNotFound, err)
   295  
   296  	// Test errSeekIDNotFound when we scan to the end of the index file (foo40
   297  	// would be located at the end of the index file based on the writes we've made)
   298  	_, err = s.SeekByID(ident.StringID("foo40"), resources)
   299  	assert.Equal(t, errSeekIDNotFound, err)
   300  
   301  	assert.NoError(t, s.Close())
   302  }
   303  
   304  func TestReuseSeeker(t *testing.T) {
   305  	dir, err := ioutil.TempDir("", "testdb")
   306  	if err != nil {
   307  		t.Fatal(err)
   308  	}
   309  	filePathPrefix := filepath.Join(dir, "")
   310  	defer os.RemoveAll(dir)
   311  
   312  	w := newTestWriter(t, filePathPrefix)
   313  
   314  	writerOpts := DataWriterOpenOptions{
   315  		BlockSize: testBlockSize,
   316  		Identifier: FileSetFileIdentifier{
   317  			Namespace:  testNs1ID,
   318  			Shard:      0,
   319  			BlockStart: testWriterStart.Add(-time.Hour),
   320  		},
   321  	}
   322  	err = w.Open(writerOpts)
   323  	assert.NoError(t, err)
   324  	assert.NoError(t, w.Write(
   325  		persist.NewMetadataFromIDAndTags(
   326  			ident.StringID("foo"),
   327  			ident.Tags{},
   328  			persist.MetadataOptions{}),
   329  		bytesRefd([]byte{1, 2, 1}),
   330  		digest.Checksum([]byte{1, 2, 1})))
   331  	assert.NoError(t, w.Close())
   332  
   333  	writerOpts = DataWriterOpenOptions{
   334  		BlockSize: testBlockSize,
   335  		Identifier: FileSetFileIdentifier{
   336  			Namespace:  testNs1ID,
   337  			Shard:      0,
   338  			BlockStart: testWriterStart,
   339  		},
   340  	}
   341  	err = w.Open(writerOpts)
   342  	assert.NoError(t, err)
   343  	assert.NoError(t, w.Write(
   344  		persist.NewMetadataFromIDAndTags(
   345  			ident.StringID("foo"),
   346  			ident.Tags{},
   347  			persist.MetadataOptions{}),
   348  		bytesRefd([]byte{1, 2, 3}),
   349  		digest.Checksum([]byte{1, 2, 3})))
   350  	assert.NoError(t, w.Close())
   351  
   352  	resources := newTestReusableSeekerResources()
   353  	s := newTestSeeker(filePathPrefix)
   354  	err = s.Open(testNs1ID, 0, testWriterStart.Add(-time.Hour), 0, resources)
   355  	assert.NoError(t, err)
   356  
   357  	data, err := s.SeekByID(ident.StringID("foo"), resources)
   358  	require.NoError(t, err)
   359  
   360  	data.IncRef()
   361  	defer data.DecRef()
   362  	assert.Equal(t, []byte{1, 2, 1}, data.Bytes())
   363  
   364  	err = s.Open(testNs1ID, 0, testWriterStart, 0, resources)
   365  	assert.NoError(t, err)
   366  
   367  	data, err = s.SeekByID(ident.StringID("foo"), resources)
   368  	require.NoError(t, err)
   369  
   370  	data.IncRef()
   371  	defer data.DecRef()
   372  	assert.Equal(t, []byte{1, 2, 3}, data.Bytes())
   373  }
   374  
   375  func TestCloneSeeker(t *testing.T) {
   376  	dir, err := ioutil.TempDir("", "testdb")
   377  	if err != nil {
   378  		t.Fatal(err)
   379  	}
   380  	filePathPrefix := filepath.Join(dir, "")
   381  	defer os.RemoveAll(dir)
   382  
   383  	w := newTestWriter(t, filePathPrefix)
   384  
   385  	writerOpts := DataWriterOpenOptions{
   386  		BlockSize: testBlockSize,
   387  		Identifier: FileSetFileIdentifier{
   388  			Namespace:  testNs1ID,
   389  			Shard:      0,
   390  			BlockStart: testWriterStart.Add(-time.Hour),
   391  		},
   392  	}
   393  	err = w.Open(writerOpts)
   394  	assert.NoError(t, err)
   395  	assert.NoError(t, w.Write(
   396  		persist.NewMetadataFromIDAndTags(
   397  			ident.StringID("foo"),
   398  			ident.Tags{},
   399  			persist.MetadataOptions{}),
   400  		bytesRefd([]byte{1, 2, 1}),
   401  		digest.Checksum([]byte{1, 2, 1})))
   402  	assert.NoError(t, w.Close())
   403  
   404  	writerOpts = DataWriterOpenOptions{
   405  		BlockSize: testBlockSize,
   406  		Identifier: FileSetFileIdentifier{
   407  			Namespace:  testNs1ID,
   408  			Shard:      0,
   409  			BlockStart: testWriterStart,
   410  		},
   411  	}
   412  	err = w.Open(writerOpts)
   413  	assert.NoError(t, err)
   414  	assert.NoError(t, w.Write(
   415  		persist.NewMetadataFromIDAndTags(
   416  			ident.StringID("foo"),
   417  			ident.Tags{},
   418  			persist.MetadataOptions{}),
   419  		bytesRefd([]byte{1, 2, 3}),
   420  		digest.Checksum([]byte{1, 2, 3})))
   421  	assert.NoError(t, w.Close())
   422  
   423  	resources := newTestReusableSeekerResources()
   424  	s := newTestSeeker(filePathPrefix)
   425  	err = s.Open(testNs1ID, 0, testWriterStart.Add(-time.Hour), 0, resources)
   426  	assert.NoError(t, err)
   427  
   428  	clone, err := s.ConcurrentClone()
   429  	require.NoError(t, err)
   430  
   431  	data, err := clone.SeekByID(ident.StringID("foo"), resources)
   432  	require.NoError(t, err)
   433  
   434  	data.IncRef()
   435  	defer data.DecRef()
   436  	assert.Equal(t, []byte{1, 2, 1}, data.Bytes())
   437  }
   438  
   439  func TestSeekValidateIndexEntriesFile(t *testing.T) {
   440  	dir, err := ioutil.TempDir("", "testdb")
   441  	if err != nil {
   442  		t.Fatal(err)
   443  	}
   444  	filePathPrefix := filepath.Join(dir, "")
   445  	defer os.RemoveAll(dir)
   446  
   447  	w := newTestWriter(t, filePathPrefix)
   448  	writerOpts := DataWriterOpenOptions{
   449  		BlockSize: testBlockSize,
   450  		Identifier: FileSetFileIdentifier{
   451  			Namespace:  testNs1ID,
   452  			Shard:      0,
   453  			BlockStart: testWriterStart,
   454  		},
   455  	}
   456  	err = w.Open(writerOpts)
   457  	assert.NoError(t, err)
   458  
   459  	// Write data
   460  	assert.NoError(t, w.Write(
   461  		persist.NewMetadataFromIDAndTags(
   462  			ident.StringID("foo"),
   463  			ident.Tags{},
   464  			persist.MetadataOptions{}),
   465  		bytesRefd([]byte{1, 2, 3}),
   466  		digest.Checksum([]byte{1, 2, 3})))
   467  	assert.NoError(t, w.Close())
   468  
   469  	shardDir := ShardDataDirPath(filePathPrefix, testNs1ID, 0)
   470  
   471  	// With full file validation disabled
   472  	s := seeker{opts: seekerOpts{
   473  		filePathPrefix: filePathPrefix,
   474  		dataBufferSize: testReaderBufferSize,
   475  		infoBufferSize: testReaderBufferSize,
   476  		bytesPool:      testBytesPool,
   477  		keepUnreadBuf:  false,
   478  		opts:           testDefaultOpts,
   479  	}}
   480  	s.versionChecker = schema.NewVersionChecker(1, 1)
   481  
   482  	indexFilePath := dataFilesetPathFromTimeAndIndex(shardDir, testWriterStart, 0, indexFileSuffix, false)
   483  	indexFd, err := os.Open(indexFilePath)
   484  	assert.NoError(t, err)
   485  	indexReader := digest.NewFdWithDigestReader(defaultInfoReaderBufferSize)
   486  	indexReader.Reset(indexFd)
   487  
   488  	assert.NoError(t, s.validateIndexFileDigest(indexReader, 0))
   489  
   490  	// With full file validation enabled
   491  	s.versionChecker = schema.NewVersionChecker(1, 0)
   492  	_, err = indexFd.Seek(0, 0)
   493  	assert.NoError(t, err)
   494  	indexReader.Reset(indexFd)
   495  
   496  	assert.Error(t, s.validateIndexFileDigest(indexReader, 0))
   497  
   498  	// Sanity check -- call seeker#Open and ensure VersionChecker is set correctly
   499  	err = s.Open(testNs1ID, 0, testWriterStart, 0, newTestReusableSeekerResources())
   500  	assert.NoError(t, err)
   501  	assert.True(t, s.versionChecker.IndexEntryValidationEnabled())
   502  }
   503  
   504  func newTestReusableSeekerResources() ReusableSeekerResources {
   505  	return NewReusableSeekerResources(testDefaultOpts)
   506  }