github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/persist/fs/seek_manager_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  	"sync"
    25  	"testing"
    26  	"time"
    27  
    28  	"github.com/m3db/m3/src/cluster/shard"
    29  	"github.com/m3db/m3/src/dbnode/retention"
    30  	"github.com/m3db/m3/src/dbnode/sharding"
    31  	"github.com/m3db/m3/src/dbnode/storage/block"
    32  	"github.com/m3db/m3/src/x/ident"
    33  	xtest "github.com/m3db/m3/src/x/test"
    34  	xtime "github.com/m3db/m3/src/x/time"
    35  
    36  	"github.com/fortytw2/leaktest"
    37  	"github.com/golang/mock/gomock"
    38  	"github.com/stretchr/testify/require"
    39  )
    40  
    41  const (
    42  	defaultTestingFetchConcurrency = 2
    43  )
    44  
    45  var defaultTestBlockRetrieverOptions = NewBlockRetrieverOptions().
    46  	SetBlockLeaseManager(&block.NoopLeaseManager{}).
    47  	// Test with caching enabled.
    48  	SetCacheBlocksOnRetrieve(true).
    49  	// Default value is determined by available CPUs, but for testing
    50  	// we want to have this been consistent across hardware.
    51  	SetFetchConcurrency(defaultTestingFetchConcurrency)
    52  
    53  func TestSeekerManagerCacheShardIndices(t *testing.T) {
    54  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
    55  
    56  	shards := []uint32{2, 5, 9, 478, 1023}
    57  	metadata := testNs1Metadata(t)
    58  	shardSet, err := sharding.NewShardSet(
    59  		sharding.NewShards(shards, shard.Available),
    60  		sharding.DefaultHashFn(1),
    61  	)
    62  	require.NoError(t, err)
    63  	m := NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
    64  	require.NoError(t, m.Open(metadata, shardSet))
    65  	byTimes := make(map[uint32]*seekersByTime)
    66  	var mu sync.Mutex
    67  	m.openAnyUnopenSeekersFn = func(byTime *seekersByTime) error {
    68  		mu.Lock()
    69  		byTimes[byTime.shard] = byTime
    70  		mu.Unlock()
    71  		return nil
    72  	}
    73  
    74  	require.NoError(t, m.CacheShardIndices(shards))
    75  	// Assert captured byTime objects match expectations
    76  	require.Equal(t, len(shards), len(byTimes))
    77  	for _, shard := range shards {
    78  		mu.Lock()
    79  		byTimes[shard].shard = shard
    80  		mu.Unlock()
    81  	}
    82  
    83  	// Assert seeksByShardIdx match expectations
    84  	shardSetMap := make(map[uint32]struct{}, len(shards))
    85  	for _, shard := range shards {
    86  		shardSetMap[shard] = struct{}{}
    87  	}
    88  
    89  	for shard, byTime := range m.seekersByShardIdx {
    90  		_, exists := shardSetMap[uint32(shard)]
    91  		if !exists {
    92  			require.False(t, byTime.accessed)
    93  		} else {
    94  			require.True(t, byTime.accessed)
    95  			require.Equal(t, int(shard), int(byTime.shard))
    96  		}
    97  	}
    98  
    99  	require.NoError(t, m.Close())
   100  }
   101  
   102  func TestSeekerManagerUpdateOpenLease(t *testing.T) {
   103  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   104  
   105  	var (
   106  		ctrl   = xtest.NewController(t)
   107  		shards = []uint32{2, 5, 9, 478, 1023}
   108  		m      = NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
   109  	)
   110  	defer ctrl.Finish()
   111  
   112  	var (
   113  		mockSeekerStatsLock sync.Mutex
   114  		numMockSeekerCloses int
   115  	)
   116  	m.newOpenSeekerFn = func(
   117  		shard uint32,
   118  		blockStart xtime.UnixNano,
   119  		volume int,
   120  	) (DataFileSetSeeker, error) {
   121  		mock := NewMockDataFileSetSeeker(ctrl)
   122  		// ConcurrentClone() will be called fetchConcurrency-1 times because the original can be used
   123  		// as one of the clones.
   124  		for i := 0; i < defaultTestingFetchConcurrency-1; i++ {
   125  			mock.EXPECT().ConcurrentClone().Return(mock, nil)
   126  		}
   127  		for i := 0; i < defaultTestingFetchConcurrency; i++ {
   128  			mock.EXPECT().Close().DoAndReturn(func() error {
   129  				mockSeekerStatsLock.Lock()
   130  				numMockSeekerCloses++
   131  				mockSeekerStatsLock.Unlock()
   132  				return nil
   133  			})
   134  			mock.EXPECT().ConcurrentIDBloomFilter().Return(nil).AnyTimes()
   135  		}
   136  		return mock, nil
   137  	}
   138  	m.sleepFn = func(_ time.Duration) {
   139  		time.Sleep(time.Millisecond)
   140  	}
   141  
   142  	metadata := testNs1Metadata(t)
   143  	shardSet, err := sharding.NewShardSet(
   144  		sharding.NewShards(shards, shard.Available),
   145  		sharding.DefaultHashFn(1),
   146  	)
   147  	require.NoError(t, err)
   148  	// Pick a start time that's within retention so the background loop doesn't close
   149  	// the seeker.
   150  	blockStart := xtime.Now().Truncate(metadata.Options().RetentionOptions().BlockSize())
   151  	require.NoError(t, m.Open(metadata, shardSet))
   152  	for _, shard := range shards {
   153  		seeker, err := m.Borrow(shard, blockStart)
   154  		require.NoError(t, err)
   155  		byTime, ok := m.seekersByTime(shard)
   156  		require.True(t, ok)
   157  		byTime.RLock()
   158  		seekers := byTime.seekers[blockStart]
   159  		require.Equal(t, defaultTestingFetchConcurrency, len(seekers.active.seekers))
   160  		require.Equal(t, 0, seekers.active.volume)
   161  		byTime.RUnlock()
   162  		require.NoError(t, m.Return(shard, blockStart, seeker))
   163  	}
   164  
   165  	// Ensure that UpdateOpenLease() updates the volumes.
   166  	for _, shard := range shards {
   167  		updateResult, err := m.UpdateOpenLease(block.LeaseDescriptor{
   168  			Namespace:  metadata.ID(),
   169  			Shard:      shard,
   170  			BlockStart: blockStart,
   171  		}, block.LeaseState{Volume: 1})
   172  		require.NoError(t, err)
   173  		require.Equal(t, block.UpdateOpenLease, updateResult)
   174  
   175  		byTime, ok := m.seekersByTime(shard)
   176  		require.True(t, ok)
   177  		byTime.RLock()
   178  		seekers := byTime.seekers[blockStart]
   179  		require.Equal(t, defaultTestingFetchConcurrency, len(seekers.active.seekers))
   180  		require.Equal(t, 1, seekers.active.volume)
   181  		byTime.RUnlock()
   182  	}
   183  	// Ensure that the old seekers actually get closed.
   184  	mockSeekerStatsLock.Lock()
   185  	require.Equal(t, len(shards)*defaultTestingFetchConcurrency, numMockSeekerCloses)
   186  	mockSeekerStatsLock.Unlock()
   187  
   188  	// Ensure that UpdateOpenLease() ignores updates for the wrong namespace.
   189  	for _, shard := range shards {
   190  		updateResult, err := m.UpdateOpenLease(block.LeaseDescriptor{
   191  			Namespace:  ident.StringID("some-other-ns"),
   192  			Shard:      shard,
   193  			BlockStart: blockStart,
   194  		}, block.LeaseState{Volume: 2})
   195  		require.NoError(t, err)
   196  		require.Equal(t, block.NoOpenLease, updateResult)
   197  
   198  		byTime, ok := m.seekersByTime(shard)
   199  		require.True(t, ok)
   200  		byTime.RLock()
   201  		seekers := byTime.seekers[blockStart]
   202  		require.Equal(t, defaultTestingFetchConcurrency, len(seekers.active.seekers))
   203  		// Should not have increased to 2.
   204  		require.Equal(t, 1, seekers.active.volume)
   205  		byTime.RUnlock()
   206  	}
   207  
   208  	// Ensure that UpdateOpenLease() returns an error for out-of-order updates.
   209  	for _, shard := range shards {
   210  		_, err := m.UpdateOpenLease(block.LeaseDescriptor{
   211  			Namespace:  metadata.ID(),
   212  			Shard:      shard,
   213  			BlockStart: blockStart,
   214  		}, block.LeaseState{Volume: 0})
   215  		require.Equal(t, errOutOfOrderUpdateOpenLease, err)
   216  	}
   217  
   218  	require.NoError(t, m.Close())
   219  }
   220  
   221  func TestSeekerManagerUpdateOpenLeaseConcurrentNotAllowed(t *testing.T) {
   222  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   223  
   224  	var (
   225  		ctrl     = xtest.NewController(t)
   226  		shards   = []uint32{1, 2}
   227  		m        = NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
   228  		metadata = testNs1Metadata(t)
   229  		// Pick a start time that's within retention so the background loop doesn't close the seeker.
   230  		blockStart = xtime.Now().Truncate(metadata.Options().RetentionOptions().BlockSize())
   231  	)
   232  	defer ctrl.Finish()
   233  
   234  	descriptor1 := block.LeaseDescriptor{
   235  		Namespace:  metadata.ID(),
   236  		Shard:      1,
   237  		BlockStart: blockStart,
   238  	}
   239  
   240  	m.newOpenSeekerFn = func(
   241  		shard uint32,
   242  		blockStart xtime.UnixNano,
   243  		volume int,
   244  	) (DataFileSetSeeker, error) {
   245  		if volume == 1 {
   246  			var wg sync.WaitGroup
   247  			wg.Add(1)
   248  			go func() {
   249  				defer wg.Done()
   250  				// Call UpdateOpenLease while within another UpdateOpenLease call.
   251  				_, err := m.UpdateOpenLease(descriptor1, block.LeaseState{Volume: 2})
   252  				if shard == 1 {
   253  					// Concurrent call is made with the same shard id (and other values).
   254  					require.Equal(t, errConcurrentUpdateOpenLeaseNotAllowed, err)
   255  				} else {
   256  					// Concurrent call is made with a different shard id (2) and so it should pass.
   257  					require.NoError(t, err)
   258  				}
   259  			}()
   260  			wg.Wait()
   261  		}
   262  		mock := NewMockDataFileSetSeeker(ctrl)
   263  		mock.EXPECT().ConcurrentClone().Return(mock, nil).AnyTimes()
   264  		mock.EXPECT().Close().AnyTimes()
   265  		mock.EXPECT().ConcurrentIDBloomFilter().Return(nil).AnyTimes()
   266  		return mock, nil
   267  	}
   268  	m.sleepFn = func(_ time.Duration) {
   269  		time.Sleep(time.Millisecond)
   270  	}
   271  
   272  	shardSet, err := sharding.NewShardSet(
   273  		sharding.NewShards(shards, shard.Available),
   274  		sharding.DefaultHashFn(1),
   275  	)
   276  	require.NoError(t, err)
   277  	require.NoError(t, m.Open(metadata, shardSet))
   278  
   279  	for _, shardID := range shards {
   280  		seeker, err := m.Borrow(shardID, blockStart)
   281  		require.NoError(t, err)
   282  		require.NoError(t, m.Return(shardID, blockStart, seeker))
   283  	}
   284  
   285  	updateResult, err := m.UpdateOpenLease(descriptor1, block.LeaseState{Volume: 1})
   286  	require.NoError(t, err)
   287  	require.Equal(t, block.UpdateOpenLease, updateResult)
   288  
   289  	descriptor2 := descriptor1
   290  	descriptor2.Shard = 2
   291  	updateResult, err = m.UpdateOpenLease(descriptor2, block.LeaseState{Volume: 1})
   292  	require.NoError(t, err)
   293  	require.Equal(t, block.UpdateOpenLease, updateResult)
   294  
   295  	require.NoError(t, m.Close())
   296  }
   297  
   298  // TestSeekerManagerBorrowOpenSeekersLazy tests that the Borrow() method will
   299  // open seekers lazily if they're not already open.
   300  func TestSeekerManagerBorrowOpenSeekersLazy(t *testing.T) {
   301  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   302  
   303  	ctrl := xtest.NewController(t)
   304  
   305  	shards := []uint32{2, 5, 9, 478, 1023}
   306  	m := NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
   307  	m.newOpenSeekerFn = func(
   308  		shard uint32,
   309  		blockStart xtime.UnixNano,
   310  		volume int,
   311  	) (DataFileSetSeeker, error) {
   312  		mock := NewMockDataFileSetSeeker(ctrl)
   313  		mock.EXPECT().Open(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
   314  		mock.EXPECT().ConcurrentClone().Return(mock, nil)
   315  		for i := 0; i < defaultTestingFetchConcurrency; i++ {
   316  			mock.EXPECT().Close().Return(nil)
   317  			mock.EXPECT().ConcurrentIDBloomFilter().Return(nil)
   318  		}
   319  		return mock, nil
   320  	}
   321  	m.sleepFn = func(_ time.Duration) {
   322  		time.Sleep(time.Millisecond)
   323  	}
   324  
   325  	metadata := testNs1Metadata(t)
   326  	shardSet, err := sharding.NewShardSet(
   327  		sharding.NewShards(shards, shard.Available),
   328  		sharding.DefaultHashFn(1),
   329  	)
   330  	require.NoError(t, err)
   331  	require.NoError(t, m.Open(metadata, shardSet))
   332  	for _, shard := range shards {
   333  		seeker, err := m.Borrow(shard, 0)
   334  		require.NoError(t, err)
   335  		byTime, ok := m.seekersByTime(shard)
   336  		require.True(t, ok)
   337  		byTime.RLock()
   338  		seekers := byTime.seekers[0]
   339  		require.Equal(t, defaultTestingFetchConcurrency, len(seekers.active.seekers))
   340  		byTime.RUnlock()
   341  		require.NoError(t, m.Return(shard, 0, seeker))
   342  	}
   343  
   344  	require.NoError(t, m.Close())
   345  }
   346  
   347  // TestSeekerManagerOpenCloseLoop tests the openCloseLoop of the SeekerManager
   348  // by making sure that it makes the right decisions with regards to cleaning
   349  // up resources based on their state.
   350  func TestSeekerManagerOpenCloseLoop(t *testing.T) {
   351  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   352  
   353  	ctrl := xtest.NewController(t)
   354  	m := NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
   355  	clockOpts := m.opts.ClockOptions()
   356  	now := clockOpts.NowFn()()
   357  	startNano := xtime.ToUnixNano(now)
   358  
   359  	fakeTime := now
   360  	fakeTimeLock := sync.Mutex{}
   361  	// Setup a function that will allow us to dynamically modify the clock in
   362  	// a concurrency-safe way
   363  	newNowFn := func() time.Time {
   364  		fakeTimeLock.Lock()
   365  		defer fakeTimeLock.Unlock()
   366  		return fakeTime
   367  	}
   368  	clockOpts = clockOpts.SetNowFn(newNowFn)
   369  	m.opts = m.opts.SetClockOptions(clockOpts)
   370  
   371  	// Initialize some seekers for a time period
   372  	m.openAnyUnopenSeekersFn = func(byTime *seekersByTime) error {
   373  		byTime.Lock()
   374  		defer byTime.Unlock()
   375  
   376  		// Don't overwrite if called again
   377  		if len(byTime.seekers) != 0 {
   378  			return nil
   379  		}
   380  
   381  		// Don't re-open if they should have expired
   382  		fakeTimeLock.Lock()
   383  		defer fakeTimeLock.Unlock()
   384  		if !fakeTime.Equal(now) {
   385  			return nil
   386  		}
   387  
   388  		mock := NewMockDataFileSetSeeker(ctrl)
   389  		mock.EXPECT().Close().Return(nil)
   390  		mocks := []borrowableSeeker{}
   391  		mocks = append(mocks, borrowableSeeker{seeker: mock})
   392  		byTime.seekers[startNano] = rotatableSeekers{
   393  			active: seekersAndBloom{
   394  				seekers:     mocks,
   395  				bloomFilter: nil,
   396  			},
   397  		}
   398  		return nil
   399  	}
   400  
   401  	// Notified everytime the openCloseLoop ticks
   402  	tickCh := make(chan struct{})
   403  	cleanupCh := make(chan struct{})
   404  
   405  	m.sleepFn = func(_ time.Duration) {
   406  		tickCh <- struct{}{}
   407  	}
   408  
   409  	shards := []uint32{2, 5, 9, 478, 1023}
   410  	metadata := testNs1Metadata(t)
   411  	shardSet, err := sharding.NewShardSet(
   412  		sharding.NewShards(shards, shard.Available),
   413  		sharding.DefaultHashFn(1),
   414  	)
   415  	require.NoError(t, err)
   416  	require.NoError(t, m.Open(metadata, shardSet))
   417  
   418  	// Force all the seekers to be opened
   419  	require.NoError(t, m.CacheShardIndices(shards))
   420  
   421  	seekers := []ConcurrentDataFileSetSeeker{}
   422  
   423  	// Steps is a series of steps for the test. It is guaranteed that at least
   424  	// one (not exactly one!) tick of the openCloseLoop will occur between every step.
   425  	steps := []struct {
   426  		title string
   427  		step  func()
   428  	}{
   429  		{
   430  			title: "Make sure it didn't clean up the seekers which are still in retention",
   431  			step: func() {
   432  				m.RLock()
   433  				for _, shard := range shards {
   434  					byTime, ok := m.seekersByTime(shard)
   435  					require.True(t, ok)
   436  
   437  					require.Equal(t, 1, len(byTime.seekers[startNano].active.seekers))
   438  				}
   439  				m.RUnlock()
   440  			},
   441  		},
   442  		{
   443  			title: "Borrow a seeker from each shard and then modify the clock such that they're out of retention",
   444  			step: func() {
   445  				for _, shard := range shards {
   446  					seeker, err := m.Borrow(shard, startNano)
   447  					require.NoError(t, err)
   448  					require.NotNil(t, seeker)
   449  					seekers = append(seekers, seeker)
   450  				}
   451  
   452  				fakeTimeLock.Lock()
   453  				fakeTime = fakeTime.Add(10 * metadata.Options().RetentionOptions().RetentionPeriod())
   454  				fakeTimeLock.Unlock()
   455  			},
   456  		},
   457  		{
   458  			title: "Make sure the seeker manager cant be closed while seekers are borrowed",
   459  			step: func() {
   460  				require.Equal(t, errCantCloseSeekerManagerWhileSeekersAreBorrowed, m.Close())
   461  			},
   462  		},
   463  		{
   464  			title: "Make sure that none of the seekers were cleaned up during the openCloseLoop tick (because they're still borrowed)",
   465  			step: func() {
   466  				m.RLock()
   467  				for _, shard := range shards {
   468  					byTime, ok := m.seekersByTime(shard)
   469  					require.True(t, ok)
   470  					require.Equal(t, 1, len(byTime.seekers[startNano].active.seekers))
   471  				}
   472  				m.RUnlock()
   473  			},
   474  		},
   475  		{
   476  			title: "Return the borrowed seekers",
   477  			step: func() {
   478  				for i, seeker := range seekers {
   479  					require.NoError(t, m.Return(shards[i], startNano, seeker))
   480  				}
   481  			},
   482  		},
   483  		{
   484  			title: "Make sure that the returned seekers were cleaned up during the openCloseLoop tick",
   485  			step: func() {
   486  				m.RLock()
   487  				for _, shard := range shards {
   488  					byTime, ok := m.seekersByTime(shard)
   489  					require.True(t, ok)
   490  					byTime.RLock()
   491  					_, ok = byTime.seekers[startNano]
   492  					byTime.RUnlock()
   493  					require.False(t, ok)
   494  				}
   495  				m.RUnlock()
   496  			},
   497  		},
   498  	}
   499  
   500  	for _, step := range steps {
   501  		// Wait for two notifications between steps to guarantee that the entirety
   502  		// of the openCloseLoop is executed at least once
   503  		<-tickCh
   504  		<-tickCh
   505  		step.step()
   506  	}
   507  
   508  	// Background goroutine that will pull notifications off the tickCh so that
   509  	// the openCloseLoop is not blocked when we call Close()
   510  	go func() {
   511  		for {
   512  			select {
   513  			case <-tickCh:
   514  				continue
   515  			case <-cleanupCh:
   516  				return
   517  			}
   518  		}
   519  	}()
   520  
   521  	// Restore previous interval once the openCloseLoop ends
   522  	require.NoError(t, m.Close())
   523  	// Make sure there are no goroutines still trying to write into the tickCh
   524  	// to prevent the test itself from interfering with the goroutine leak test
   525  	close(cleanupCh)
   526  }
   527  
   528  func TestSeekerManagerAssignShardSet(t *testing.T) {
   529  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   530  
   531  	var (
   532  		ctrl   = xtest.NewController(t)
   533  		shards = []uint32{1, 2}
   534  		m      = NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
   535  	)
   536  	defer ctrl.Finish()
   537  
   538  	var (
   539  		wg                                      sync.WaitGroup
   540  		mockSeekerStatsLock                     sync.Mutex
   541  		numMockSeekerClosesByShardAndBlockStart = make(map[uint32]map[xtime.UnixNano]int)
   542  	)
   543  	m.newOpenSeekerFn = func(
   544  		shard uint32,
   545  		blockStart xtime.UnixNano,
   546  		volume int,
   547  	) (DataFileSetSeeker, error) {
   548  		// We expect `defaultTestingFetchConcurrency` number of calls to Close because we return this
   549  		// many numbers of clones and each clone will need to be closed.
   550  		wg.Add(defaultTestingFetchConcurrency)
   551  
   552  		mock := NewMockDataFileSetSeeker(ctrl)
   553  		// ConcurrentClone() will be called fetchConcurrency-1 times because the original can be used
   554  		// as one of the clones.
   555  		mock.EXPECT().ConcurrentClone().Times(defaultTestingFetchConcurrency-1).Return(mock, nil)
   556  		mock.EXPECT().Close().Times(defaultTestingFetchConcurrency).DoAndReturn(func() error {
   557  			mockSeekerStatsLock.Lock()
   558  			numMockSeekerClosesByBlockStart, ok := numMockSeekerClosesByShardAndBlockStart[shard]
   559  			if !ok {
   560  				numMockSeekerClosesByBlockStart = make(map[xtime.UnixNano]int)
   561  				numMockSeekerClosesByShardAndBlockStart[shard] = numMockSeekerClosesByBlockStart
   562  			}
   563  			numMockSeekerClosesByBlockStart[blockStart]++
   564  			mockSeekerStatsLock.Unlock()
   565  			wg.Done()
   566  			return nil
   567  		})
   568  		mock.EXPECT().ConcurrentIDBloomFilter().Return(nil).AnyTimes()
   569  		return mock, nil
   570  	}
   571  	m.sleepFn = func(_ time.Duration) {
   572  		time.Sleep(time.Millisecond)
   573  	}
   574  
   575  	metadata := testNs1Metadata(t)
   576  	shardSet, err := sharding.NewShardSet(
   577  		sharding.NewShards(shards, shard.Available),
   578  		sharding.DefaultHashFn(1),
   579  	)
   580  	require.NoError(t, err)
   581  	// Pick a start time thats within retention so the background loop doesn't close
   582  	// the seeker.
   583  	blockStart := xtime.Now().Truncate(metadata.Options().RetentionOptions().BlockSize())
   584  	require.NoError(t, m.Open(metadata, shardSet))
   585  
   586  	for _, shard := range shards {
   587  		seeker, err := m.Borrow(shard, blockStart)
   588  		require.NoError(t, err)
   589  		require.NoError(t, m.Return(shard, blockStart, seeker))
   590  	}
   591  
   592  	// Ensure that UpdateOpenLease() updates the volumes.
   593  	for _, shard := range shards {
   594  		updateResult, err := m.UpdateOpenLease(block.LeaseDescriptor{
   595  			Namespace:  metadata.ID(),
   596  			Shard:      shard,
   597  			BlockStart: blockStart,
   598  		}, block.LeaseState{Volume: 1})
   599  		require.NoError(t, err)
   600  		require.Equal(t, block.UpdateOpenLease, updateResult)
   601  
   602  		byTime, ok := m.seekersByTime(shard)
   603  		require.True(t, ok)
   604  		byTime.RLock()
   605  		byTime.RUnlock()
   606  	}
   607  
   608  	mockSeekerStatsLock.Lock()
   609  	for _, numMockSeekerClosesByBlockStart := range numMockSeekerClosesByShardAndBlockStart {
   610  		require.Equal(t,
   611  			defaultTestingFetchConcurrency,
   612  			numMockSeekerClosesByBlockStart[blockStart])
   613  	}
   614  	mockSeekerStatsLock.Unlock()
   615  
   616  	// Shards have moved off the node so we assign an empty shard set.
   617  	m.AssignShardSet(sharding.NewEmptyShardSet(sharding.DefaultHashFn(1)))
   618  	// Wait until the open/close loop has finished closing all the shards marked to be closed.
   619  	wg.Wait()
   620  
   621  	// Verify that shards are no longer available.
   622  	for _, shard := range shards {
   623  		ok, err := m.Test(nil, shard, blockStart)
   624  		require.Equal(t, errShardNotExists, err)
   625  		require.False(t, ok)
   626  		_, err = m.Borrow(shard, blockStart)
   627  		require.Equal(t, errShardNotExists, err)
   628  	}
   629  
   630  	// Verify that we see the expected # of closes per block start.
   631  	mockSeekerStatsLock.Lock()
   632  	for _, numMockSeekerClosesByBlockStart := range numMockSeekerClosesByShardAndBlockStart {
   633  		for start, numMockSeekerCloses := range numMockSeekerClosesByBlockStart {
   634  			if blockStart == start {
   635  				// NB(bodu): These get closed twice since they've been closed once already due to updating their block lease.
   636  				require.Equal(t, defaultTestingFetchConcurrency*2, numMockSeekerCloses)
   637  				continue
   638  			}
   639  			require.Equal(t, defaultTestingFetchConcurrency, numMockSeekerCloses)
   640  		}
   641  	}
   642  	mockSeekerStatsLock.Unlock()
   643  
   644  	// Shards have moved back to the node so we assign a populated shard set again.
   645  	m.AssignShardSet(shardSet)
   646  	// Ensure that we can (once again) borrow the shards.
   647  	for _, shard := range shards {
   648  		seeker, err := m.Borrow(shard, blockStart)
   649  		require.NoError(t, err)
   650  		require.NoError(t, m.Return(shard, blockStart, seeker))
   651  	}
   652  
   653  	require.NoError(t, m.Close())
   654  }
   655  
   656  // TestSeekerManagerCacheShardIndicesSkipNotFound tests that expired (not found) index filesets
   657  // do not return an error.
   658  func TestSeekerManagerCacheShardIndicesSkipNotFound(t *testing.T) {
   659  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   660  
   661  	m := NewSeekerManager(nil, testDefaultOpts, defaultTestBlockRetrieverOptions).(*seekerManager)
   662  
   663  	m.newOpenSeekerFn = func(
   664  		shard uint32,
   665  		blockStart xtime.UnixNano,
   666  		volume int,
   667  	) (DataFileSetSeeker, error) {
   668  		return nil, errSeekerManagerFileSetNotFound
   669  	}
   670  
   671  	shards := []uint32{2, 5, 9, 478, 1023}
   672  	metadata := testNs1Metadata(t)
   673  	shardSet, err := sharding.NewShardSet(
   674  		sharding.NewShards(shards, shard.Available),
   675  		sharding.DefaultHashFn(1),
   676  	)
   677  	require.NoError(t, err)
   678  	require.NoError(t, m.Open(metadata, shardSet))
   679  
   680  	require.NoError(t, m.CacheShardIndices(shards))
   681  
   682  	require.NoError(t, m.Close())
   683  }
   684  
   685  func TestSeekerManagerDoNotOpenSeekersForOutOfRetentionBlocks(t *testing.T) {
   686  	defer leaktest.CheckTimeout(t, 1*time.Minute)()
   687  	var (
   688  		ctrl        = xtest.NewController(t)
   689  		shards      = []uint32{0}
   690  		metadata    = testNs1Metadata(t)
   691  		rOpts       = metadata.Options().RetentionOptions()
   692  		blockSize   = rOpts.BlockSize()
   693  		signal      = make(chan struct{})
   694  		openSeekers = make(map[xtime.UnixNano]struct{})
   695  		now         = time.Now()
   696  		opts        = NewOptions()
   697  	)
   698  	shardSet, err := sharding.NewShardSet(
   699  		sharding.NewShards(shards, shard.Available),
   700  		sharding.DefaultHashFn(1),
   701  	)
   702  	require.NoError(t, err)
   703  	opts = opts.SetClockOptions(opts.ClockOptions().SetNowFn(func() time.Time {
   704  		return now
   705  	}))
   706  	m := NewSeekerManager(nil, opts, defaultTestBlockRetrieverOptions).(*seekerManager)
   707  	m.sleepFn = func(_ time.Duration) {
   708  		signal <- struct{}{} // signal once to indicate that openCloseLoop completed.
   709  		m.sleepFn = time.Sleep
   710  	}
   711  	require.NoError(t, m.Open(metadata, shardSet))
   712  	defer func() {
   713  		require.NoError(t, m.Close())
   714  	}()
   715  
   716  	m.newOpenSeekerFn = func(shard uint32, blockStart xtime.UnixNano, volume int) (DataFileSetSeeker, error) {
   717  		openSeekers[blockStart] = struct{}{}
   718  		mockSeeker := NewMockDataFileSetSeeker(ctrl)
   719  		mockConcurrentDataFileSetSeeker := NewMockConcurrentDataFileSetSeeker(ctrl)
   720  		mockConcurrentDataFileSetSeeker.EXPECT().Close().Return(nil)
   721  		mockSeeker.EXPECT().ConcurrentClone().Return(mockConcurrentDataFileSetSeeker, nil)
   722  		mockSeeker.EXPECT().ConcurrentIDBloomFilter().Return(nil)
   723  		mockSeeker.EXPECT().Close().Return(nil)
   724  		return mockSeeker, nil
   725  	}
   726  
   727  	earliestBlockStart := retention.FlushTimeStart(rOpts, xtime.ToUnixNano(now))
   728  	require.NoError(t, m.CacheShardIndices(shards))
   729  
   730  	<-signal
   731  	require.Contains(t, openSeekers, earliestBlockStart)
   732  	require.Contains(t, openSeekers, earliestBlockStart.Add(blockSize))
   733  	require.NotContains(t, openSeekers, earliestBlockStart.Add(-blockSize))
   734  	require.NotContains(t, openSeekers, earliestBlockStart.Add(-2*blockSize))
   735  }