github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/entry_blackbox_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 storage
    22  
    23  import (
    24  	"fmt"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/m3db/m3/src/dbnode/storage/series"
    30  	"github.com/m3db/m3/src/m3ninx/doc"
    31  	"github.com/m3db/m3/src/x/ident"
    32  	"github.com/m3db/m3/src/x/tallytest"
    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/assert"
    39  	"github.com/stretchr/testify/require"
    40  	"github.com/uber-go/tally"
    41  )
    42  
    43  var (
    44  	initTime      = time.Date(2018, time.May, 12, 15, 55, 0, 0, time.UTC)
    45  	testBlockSize = 24 * time.Hour
    46  )
    47  
    48  func newTime(n int) xtime.UnixNano {
    49  	t := initTime.Truncate(testBlockSize).Add(time.Duration(n) * testBlockSize)
    50  	return xtime.ToUnixNano(t)
    51  }
    52  
    53  func newMockSeries(ctrl *gomock.Controller) series.DatabaseSeries {
    54  	return newMockSeriesWithID(ctrl, "foo")
    55  }
    56  
    57  func newMockSeriesWithID(ctrl *gomock.Controller, id string) series.DatabaseSeries {
    58  	series := series.NewMockDatabaseSeries(ctrl)
    59  	series.EXPECT().ID().Return(ident.StringID(id)).AnyTimes()
    60  	return series
    61  }
    62  
    63  func TestEntryReaderWriterCount(t *testing.T) {
    64  	ctrl := xtest.NewController(t)
    65  	defer ctrl.Finish()
    66  
    67  	e := NewEntry(NewEntryOptions{Series: newMockSeries(ctrl)})
    68  	require.Equal(t, int32(0), e.ReaderWriterCount())
    69  
    70  	e.IncrementReaderWriterCount()
    71  	require.Equal(t, int32(1), e.ReaderWriterCount())
    72  
    73  	e.DecrementReaderWriterCount()
    74  	require.Equal(t, int32(0), e.ReaderWriterCount())
    75  }
    76  
    77  func TestEntryIndexSuccessPath(t *testing.T) {
    78  	ctrl := xtest.NewController(t)
    79  	defer ctrl.Finish()
    80  
    81  	e := NewEntry(NewEntryOptions{Series: newMockSeries(ctrl)})
    82  	t0 := newTime(0)
    83  	require.False(t, e.IndexedForBlockStart(t0))
    84  
    85  	require.True(t, e.NeedsIndexUpdate(t0))
    86  	e.OnIndexPrepare(t0)
    87  	e.OnIndexSuccess(t0)
    88  	e.OnIndexFinalize(t0)
    89  
    90  	require.True(t, e.IndexedForBlockStart(t0))
    91  	require.Equal(t, int32(0), e.ReaderWriterCount())
    92  	require.False(t, e.NeedsIndexUpdate(t0))
    93  }
    94  
    95  func TestEntryIndexFailPath(t *testing.T) {
    96  	ctrl := xtest.NewController(t)
    97  	defer ctrl.Finish()
    98  
    99  	e := NewEntry(NewEntryOptions{Series: newMockSeries(ctrl)})
   100  	t0 := newTime(0)
   101  	require.False(t, e.IndexedForBlockStart(t0))
   102  
   103  	require.True(t, e.NeedsIndexUpdate(t0))
   104  	e.OnIndexPrepare(t0)
   105  	e.OnIndexFinalize(t0)
   106  
   107  	require.False(t, e.IndexedForBlockStart(t0))
   108  	require.Equal(t, int32(0), e.ReaderWriterCount())
   109  	require.True(t, e.NeedsIndexUpdate(t0))
   110  }
   111  
   112  func TestEntryMultipleGoroutinesRaceIndexUpdate(t *testing.T) {
   113  	ctrl := xtest.NewController(t)
   114  	defer ctrl.Finish()
   115  
   116  	defer leaktest.CheckTimeout(t, time.Second)()
   117  
   118  	e := NewEntry(NewEntryOptions{Series: newMockSeries(ctrl)})
   119  	t0 := newTime(0)
   120  	require.False(t, e.IndexedForBlockStart(t0))
   121  
   122  	var (
   123  		r1, r2 bool
   124  		wg     sync.WaitGroup
   125  	)
   126  	wg.Add(2)
   127  
   128  	go func() {
   129  		r1 = e.NeedsIndexUpdate(t0)
   130  		wg.Done()
   131  	}()
   132  
   133  	go func() {
   134  		r2 = e.NeedsIndexUpdate(t0)
   135  		wg.Done()
   136  	}()
   137  
   138  	wg.Wait()
   139  
   140  	require.False(t, r1 && r2)
   141  	require.True(t, r1 || r2)
   142  }
   143  
   144  func TestEntryTryMarkIndexGarbageCollectedAfterSeriesClose(t *testing.T) {
   145  	ctrl := xtest.NewController(t)
   146  	defer ctrl.Finish()
   147  
   148  	opts := DefaultTestOptions()
   149  	ctx := opts.ContextPool().Get()
   150  	defer ctx.Close()
   151  
   152  	shard := testDatabaseShard(t, opts)
   153  	defer func() {
   154  		require.NoError(t, shard.Close())
   155  	}()
   156  
   157  	id := ident.StringID("foo")
   158  
   159  	series := series.NewMockDatabaseSeries(ctrl)
   160  	series.EXPECT().ID().Return(id)
   161  
   162  	entry := NewEntry(NewEntryOptions{
   163  		Shard:        shard,
   164  		Series:       series,
   165  		EntryMetrics: NewEntryMetrics(tally.NewTestScope("test", nil)),
   166  	})
   167  
   168  	// Make sure when ID is returned nil to emulate series being closed
   169  	// and TryMarkIndexGarbageCollected calling back into shard with a nil ID.
   170  	series.EXPECT().ID().Return(nil).AnyTimes()
   171  	series.EXPECT().IsEmpty().Return(false).AnyTimes()
   172  	require.NotPanics(t, func() {
   173  		// Make sure doesn't panic.
   174  		require.False(t, entry.TryMarkIndexGarbageCollected())
   175  	})
   176  }
   177  
   178  func TestEntryIndexedRange(t *testing.T) {
   179  	ctrl := xtest.NewController(t)
   180  	defer ctrl.Finish()
   181  
   182  	opts := DefaultTestOptions()
   183  	ctx := opts.ContextPool().Get()
   184  	defer ctx.Close()
   185  
   186  	shard := testDatabaseShard(t, opts)
   187  	defer func() {
   188  		require.NoError(t, shard.Close())
   189  	}()
   190  
   191  	entry := NewEntry(NewEntryOptions{
   192  		Shard:  shard,
   193  		Series: newMockSeries(ctrl),
   194  	})
   195  
   196  	assertRange := func(expectedMin, expectedMax xtime.UnixNano) {
   197  		min, max := entry.IndexedRange()
   198  		assert.Equal(t, expectedMin, min)
   199  		assert.Equal(t, expectedMax, max)
   200  	}
   201  
   202  	assertRange(0, 0)
   203  
   204  	entry.OnIndexPrepare(2)
   205  	assertRange(0, 0)
   206  
   207  	entry.OnIndexSuccess(2)
   208  	assertRange(2, 2)
   209  
   210  	entry.OnIndexSuccess(5)
   211  	assertRange(2, 5)
   212  
   213  	entry.OnIndexSuccess(1)
   214  	assertRange(1, 5)
   215  
   216  	entry.OnIndexSuccess(3)
   217  	assertRange(1, 5)
   218  }
   219  
   220  func TestReconciledOnIndexSeries(t *testing.T) {
   221  	ctrl := xtest.NewController(t)
   222  	defer ctrl.Finish()
   223  
   224  	opts := DefaultTestOptions()
   225  	ctx := opts.ContextPool().Get()
   226  	defer ctx.Close()
   227  
   228  	shard := testDatabaseShard(t, opts)
   229  	defer func() {
   230  		require.NoError(t, shard.Close())
   231  	}()
   232  
   233  	// Create entry with index 0 that's not inserted
   234  	series := newMockSeries(ctrl)
   235  	entry := NewEntry(NewEntryOptions{
   236  		Index:  0,
   237  		Shard:  shard,
   238  		Series: series,
   239  	})
   240  
   241  	// Create entry with index 1 that gets inserted into the lookup map
   242  	_ = addMockSeries(ctrl, shard, series.ID(), ident.Tags{}, 1)
   243  
   244  	// Validate we perform the reconciliation.
   245  	e, closer, reconciled := entry.ReconciledOnIndexSeries()
   246  	require.True(t, reconciled)
   247  	require.Equal(t, uint64(1), e.(*Entry).Index)
   248  	closer.Close()
   249  
   250  	// Set the entry's insert time emulating being inserted into the shard.
   251  	// Ensure no reconciliation.
   252  	entry.SetInsertTime(time.Now())
   253  	e, closer, reconciled = entry.ReconciledOnIndexSeries()
   254  	require.False(t, reconciled)
   255  	require.Equal(t, uint64(0), e.(*Entry).Index)
   256  	closer.Close()
   257  }
   258  
   259  func TestMergeWithIndexSeries(t *testing.T) {
   260  	ctrl := xtest.NewController(t)
   261  	defer ctrl.Finish()
   262  
   263  	var (
   264  		blockSize  = time.Hour * 2
   265  		numBlocks  = 5
   266  		numEntries = 3
   267  		start      = xtime.Now().
   268  				Truncate(blockSize).
   269  				Add(blockSize * -time.Duration(numEntries*numBlocks)) //nolint: durationcheck
   270  
   271  		expectedIndexTimes = make([]xtime.UnixNano, 0, numEntries*numBlocks)
   272  		entries            = make([]*Entry, 0, numEntries)
   273  	)
   274  
   275  	for entryIdx := 0; entryIdx < numEntries; entryIdx++ {
   276  		series := newMockSeriesWithID(ctrl, fmt.Sprint("bar", entryIdx))
   277  		entry := NewEntry(NewEntryOptions{Series: series})
   278  
   279  		for blockIdx := 0; blockIdx < numBlocks; blockIdx++ {
   280  			blockStart := start.
   281  				Add(blockSize * time.Duration(blockIdx+numBlocks*entryIdx))
   282  
   283  			expectedIndexTimes = append(expectedIndexTimes, blockStart)
   284  			entry.OnIndexSuccess(blockStart)
   285  		}
   286  
   287  		entries = append(entries, entry)
   288  	}
   289  
   290  	mergedEntry := NewEntry(NewEntryOptions{Series: newMockSeries(ctrl)})
   291  	for _, entry := range entries {
   292  		mergedEntry.MergeEntryIndexBlockStates(entry.reverseIndex.states)
   293  	}
   294  
   295  	for _, start := range expectedIndexTimes {
   296  		require.True(t, mergedEntry.IndexedForBlockStart(start))
   297  	}
   298  
   299  	min, max := mergedEntry.IndexedRange()
   300  	require.Equal(t, min, start)
   301  	require.Equal(t, max, start.Add(blockSize*time.Duration(numEntries*numBlocks-1)))
   302  }
   303  
   304  func TestEntryTryMarkIndexGarbageCollected(t *testing.T) {
   305  	for _, tc := range []struct {
   306  		name           string
   307  		entry          *Entry
   308  		hasSeries      bool
   309  		indexed        bool
   310  		indexDuplicate bool
   311  		shardClosed    bool
   312  		hasReaders     bool
   313  
   314  		expectCollected                   bool
   315  		expectedNeedsReconcileCounter     int64
   316  		expectedNoNeedsReconcileCounter   int64
   317  		expectedGcShardClosedCounter      int64
   318  		expectedGcEmptyCounter            int64
   319  		expectedNoGcNil                   int64
   320  		expectedNoGcNotEmptySeriesCounter int64
   321  		expectedNoGcHasReadersCounter     int64
   322  	}{
   323  		{
   324  			name:            "not indexed entry should not be collected",
   325  			expectCollected: false,
   326  			expectedNoGcNil: 1,
   327  		},
   328  		{
   329  			name:                            "indexed entry with empty series should be collected",
   330  			indexed:                         true,
   331  			hasSeries:                       false,
   332  			hasReaders:                      false,
   333  			shardClosed:                     false,
   334  			expectCollected:                 true,
   335  			expectedNoNeedsReconcileCounter: 1,
   336  			expectedGcEmptyCounter:          1,
   337  		},
   338  		{
   339  			name:                            "indexed 2 empty entries need reconcile",
   340  			indexed:                         true,
   341  			indexDuplicate:                  true,
   342  			hasSeries:                       false,
   343  			hasReaders:                      false,
   344  			shardClosed:                     false,
   345  			expectCollected:                 true,
   346  			expectedNoNeedsReconcileCounter: 0,
   347  			expectedNeedsReconcileCounter:   1,
   348  			expectedGcEmptyCounter:          1,
   349  		},
   350  		{
   351  			name:                              "indexed 2 non empty entries",
   352  			indexed:                           true,
   353  			indexDuplicate:                    true,
   354  			hasSeries:                         true,
   355  			hasReaders:                        false,
   356  			shardClosed:                       false,
   357  			expectCollected:                   false,
   358  			expectedNoGcNotEmptySeriesCounter: 1,
   359  		},
   360  		{
   361  			name:                              "indexed entry with series should not be collected",
   362  			indexed:                           true,
   363  			hasSeries:                         true,
   364  			hasReaders:                        false,
   365  			shardClosed:                       false,
   366  			expectCollected:                   false,
   367  			expectedNoGcNotEmptySeriesCounter: 1,
   368  		},
   369  		{
   370  			name:                          "empty indexed entry with readers should not be collected",
   371  			indexed:                       true,
   372  			hasSeries:                     false,
   373  			hasReaders:                    true,
   374  			shardClosed:                   false,
   375  			expectCollected:               false,
   376  			expectedNoGcHasReadersCounter: 1,
   377  		},
   378  		{
   379  			name:                          "indexed entry with readers and series should not be collected",
   380  			indexed:                       true,
   381  			hasSeries:                     true,
   382  			hasReaders:                    true,
   383  			shardClosed:                   false,
   384  			expectCollected:               false,
   385  			expectedNoGcHasReadersCounter: 1,
   386  		},
   387  		{
   388  			name:                         "indexed entry with non empty series should be collected when the shard is closed",
   389  			indexed:                      true,
   390  			hasSeries:                    true,
   391  			hasReaders:                   false,
   392  			shardClosed:                  true,
   393  			expectCollected:              true,
   394  			expectedGcShardClosedCounter: 1,
   395  		},
   396  		{
   397  			name:                         "indexed entry with readers should be collected when the shard is closed",
   398  			indexed:                      true,
   399  			hasSeries:                    false,
   400  			hasReaders:                   true,
   401  			shardClosed:                  true,
   402  			expectCollected:              true,
   403  			expectedGcShardClosedCounter: 1,
   404  		},
   405  		{
   406  			name:                         "indexed entry with readers and series should be collected when the shard is closed",
   407  			indexed:                      true,
   408  			hasSeries:                    true,
   409  			hasReaders:                   true,
   410  			shardClosed:                  true,
   411  			expectCollected:              true,
   412  			expectedGcShardClosedCounter: 1,
   413  		},
   414  	} {
   415  		tc := tc
   416  		t.Run(tc.name, func(t *testing.T) {
   417  			ctrl := xtest.NewController(t)
   418  			defer ctrl.Finish()
   419  
   420  			opts := DefaultTestOptions()
   421  			ctx := opts.ContextPool().Get()
   422  			defer ctx.Close()
   423  
   424  			shard := testDatabaseShard(t, opts)
   425  			if !tc.shardClosed {
   426  				defer func() {
   427  					require.NoError(t, shard.Close())
   428  				}()
   429  			}
   430  
   431  			// Create entry with index 0 that's not inserted
   432  			s := series.NewMockDatabaseSeries(ctrl)
   433  			s.EXPECT().ID().Return(id).AnyTimes()
   434  			s.EXPECT().Close().Return().AnyTimes()
   435  			s.EXPECT().IsEmpty().Return(!tc.hasSeries).AnyTimes()
   436  
   437  			scope := tally.NewTestScope("test", nil)
   438  			metrics := NewEntryMetrics(scope)
   439  			entry := NewEntry(NewEntryOptions{
   440  				Index:        0,
   441  				Shard:        shard,
   442  				Series:       s,
   443  				EntryMetrics: metrics,
   444  			})
   445  			if tc.indexed {
   446  				shard.Lock()
   447  				shard.insertNewShardEntryWithLock(entry)
   448  				if tc.indexDuplicate {
   449  					shard.insertNewShardEntryWithLock(NewEntry(NewEntryOptions{
   450  						Index:        1,
   451  						Shard:        shard,
   452  						Series:       s,
   453  						EntryMetrics: metrics,
   454  					}))
   455  				}
   456  				shard.Unlock()
   457  			}
   458  
   459  			if tc.hasReaders {
   460  				entry.IncrementReaderWriterCount()
   461  			}
   462  			if tc.shardClosed {
   463  				require.NoError(t, shard.Close())
   464  			}
   465  			collected := entry.TryMarkIndexGarbageCollected()
   466  			require.Equal(t, tc.expectCollected, collected, "collected")
   467  			if tc.indexDuplicate {
   468  				assert.False(t, entry.IndexGarbageCollected.Load(), "IndexGarbageCollected")
   469  			} else {
   470  				assert.Equal(t, tc.expectCollected, entry.IndexGarbageCollected.Load(), "IndexGarbageCollected")
   471  			}
   472  			if tc.hasReaders {
   473  				entry.DecrementReaderWriterCount()
   474  			}
   475  
   476  			tallytest.AssertCounterValue(t, tc.expectedNeedsReconcileCounter, scope.Snapshot(), "test.count",
   477  				map[string]string{
   478  					"reconcile": "needs_reconcile",
   479  					"path":      "gc",
   480  				})
   481  			tallytest.AssertCounterValue(t, tc.expectedNoNeedsReconcileCounter, scope.Snapshot(), "test.count",
   482  				map[string]string{
   483  					"reconcile": "no_reconcile",
   484  					"path":      "gc",
   485  				})
   486  
   487  			tallytest.AssertCounterValue(t, tc.expectedGcShardClosedCounter, scope.Snapshot(), "test.gc_count",
   488  				map[string]string{
   489  					"reason": "shard_closed",
   490  					"path":   "gc",
   491  				})
   492  			tallytest.AssertCounterValue(t, tc.expectedGcEmptyCounter, scope.Snapshot(), "test.gc_count",
   493  				map[string]string{
   494  					"reason": "empty",
   495  					"path":   "gc",
   496  				})
   497  
   498  			tallytest.AssertCounterValue(t, tc.expectedNoGcNil, scope.Snapshot(), "test.no_gc_count",
   499  				map[string]string{
   500  					"reason": "nil",
   501  					"path":   "gc",
   502  				})
   503  			tallytest.AssertCounterValue(t, 0, scope.Snapshot(), "test.no_gc_count",
   504  				map[string]string{
   505  					"reason": "error",
   506  					"path":   "gc",
   507  				})
   508  			tallytest.AssertCounterValue(t, tc.expectedNoGcHasReadersCounter, scope.Snapshot(), "test.no_gc_count",
   509  				map[string]string{
   510  					"reason": "has_readers",
   511  					"path":   "gc",
   512  				})
   513  			tallytest.AssertCounterValue(t, tc.expectedNoGcNotEmptySeriesCounter, scope.Snapshot(), "test.no_gc_count",
   514  				map[string]string{
   515  					"reason": "not_empty_series",
   516  					"path":   "gc",
   517  				})
   518  		})
   519  	}
   520  }
   521  
   522  func TestTryReconcileDuplicates(t *testing.T) {
   523  	ctrl := xtest.NewController(t)
   524  	defer ctrl.Finish()
   525  
   526  	var (
   527  		shard  = NewMockShard(ctrl)
   528  		scope  = tally.NewTestScope("test", nil)
   529  		series = series.NewMockDatabaseSeries(ctrl)
   530  	)
   531  
   532  	series.EXPECT().ID().Return(id)
   533  	entry := NewEntry(NewEntryOptions{
   534  		Series:       series,
   535  		Shard:        shard,
   536  		EntryMetrics: NewEntryMetrics(scope),
   537  	})
   538  
   539  	shard.EXPECT().TryRetrieveSeriesAndIncrementReaderWriterCount(id).DoAndReturn(
   540  		func(ident.ID) (*Entry, WritableSeriesOptions, error) {
   541  			// NB: TryRetrieveSeriesAndIncrementReaderWriterCount increments rw count
   542  			// so emulate this here.
   543  			entry.IncrementReaderWriterCount()
   544  			return entry, WritableSeriesOptions{}, nil
   545  		})
   546  
   547  	entry.TryReconcileDuplicates()
   548  	tallytest.AssertCounterValue(t, 1, scope.Snapshot(), "test.count", map[string]string{
   549  		"reconcile": "no_reconcile",
   550  		"path":      "duplicate",
   551  	})
   552  	tallytest.AssertCounterValue(t, 0, scope.Snapshot(), "test.count", map[string]string{
   553  		"reconcile": "needs_reconcile",
   554  		"path":      "duplicate",
   555  	})
   556  
   557  	states := doc.EntryIndexBlockStates{1: doc.EntryIndexBlockState{}}
   558  	entry.reverseIndex = entryIndexState{states: states}
   559  	e := &Entry{reverseIndex: newEntryIndexState()}
   560  	shard.EXPECT().TryRetrieveSeriesAndIncrementReaderWriterCount(id).
   561  		Return(e, WritableSeriesOptions{}, nil)
   562  
   563  	entry.TryReconcileDuplicates()
   564  	require.Equal(t, states, e.reverseIndex.states)
   565  	tallytest.AssertCounterValue(t, 1, scope.Snapshot(), "test.count", map[string]string{
   566  		"reconcile": "no_reconcile",
   567  		"path":      "duplicate",
   568  	})
   569  	tallytest.AssertCounterValue(t, 1, scope.Snapshot(), "test.count", map[string]string{
   570  		"reconcile": "needs_reconcile",
   571  		"path":      "duplicate",
   572  	})
   573  }
   574  
   575  func TestMergeOnReconcile(t *testing.T) {
   576  	ctrl := xtest.NewController(t)
   577  	defer ctrl.Finish()
   578  
   579  	var (
   580  		shard  = NewMockShard(ctrl)
   581  		series = series.NewMockDatabaseSeries(ctrl)
   582  	)
   583  
   584  	series.EXPECT().ID().Return(id)
   585  	entry := NewEntry(NewEntryOptions{
   586  		Series: series,
   587  		Shard:  shard,
   588  	})
   589  
   590  	shard.EXPECT().TryRetrieveSeriesAndIncrementReaderWriterCount(id).DoAndReturn(
   591  		func(ident.ID) (*Entry, WritableSeriesOptions, error) {
   592  			// NB: TryRetrieveSeriesAndIncrementReaderWriterCount increments rw count
   593  			// so emulate this here.
   594  			entry.IncrementReaderWriterCount()
   595  			return entry, WritableSeriesOptions{}, nil
   596  		})
   597  
   598  	onIndexed, closer, needsReconcile := entry.ReconciledOnIndexSeries()
   599  	require.Equal(t, entry, onIndexed)
   600  	require.True(t, needsReconcile)
   601  	closer.Close()
   602  
   603  	states := doc.EntryIndexBlockStates{1: doc.EntryIndexBlockState{}}
   604  	entry.reverseIndex = entryIndexState{states: states}
   605  	otherEntry := &Entry{reverseIndex: newEntryIndexState()}
   606  	shard.EXPECT().TryRetrieveSeriesAndIncrementReaderWriterCount(id).
   607  		Return(otherEntry, WritableSeriesOptions{}, nil)
   608  
   609  	onIndexed, closer, needsReconcile = entry.ReconciledOnIndexSeries()
   610  	require.True(t, needsReconcile)
   611  	e, ok := onIndexed.(*Entry)
   612  	require.True(t, ok)
   613  	require.Equal(t, states, e.reverseIndex.states)
   614  	closer.Close()
   615  }