github.com/m3db/m3@v1.5.0/src/dbnode/storage/entry.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  	"sync"
    25  	"sync/atomic"
    26  	"time"
    27  
    28  	"github.com/uber-go/tally"
    29  	xatomic "go.uber.org/atomic"
    30  
    31  	"github.com/m3db/m3/src/dbnode/storage/block"
    32  	"github.com/m3db/m3/src/dbnode/storage/bootstrap"
    33  	"github.com/m3db/m3/src/dbnode/storage/index"
    34  	"github.com/m3db/m3/src/dbnode/storage/series"
    35  	"github.com/m3db/m3/src/dbnode/ts/writes"
    36  	"github.com/m3db/m3/src/m3ninx/doc"
    37  	"github.com/m3db/m3/src/x/clock"
    38  	"github.com/m3db/m3/src/x/context"
    39  	m3errors "github.com/m3db/m3/src/x/errors"
    40  	"github.com/m3db/m3/src/x/ident"
    41  	"github.com/m3db/m3/src/x/resource"
    42  	xtime "github.com/m3db/m3/src/x/time"
    43  )
    44  
    45  // IndexWriter accepts index inserts.
    46  type IndexWriter interface {
    47  	// WritePending indexes the provided pending entries.
    48  	WritePending(
    49  		pending []writes.PendingIndexInsert,
    50  	) error
    51  
    52  	// BlockStartForWriteTime returns the index block start
    53  	// time for the given writeTime.
    54  	BlockStartForWriteTime(
    55  		writeTime xtime.UnixNano,
    56  	) xtime.UnixNano
    57  }
    58  
    59  // EntryMetrics are metrics for an entry.
    60  type EntryMetrics struct {
    61  	gcNoReconcile           tally.Counter
    62  	gcNeedsReconcile        tally.Counter
    63  	gcSuccessShardClosed    tally.Counter
    64  	gcSuccessEmpty          tally.Counter
    65  	noGcNil                 tally.Counter
    66  	noGcErr                 tally.Counter
    67  	noGcHasReaders          tally.Counter
    68  	noGcNotEmptySeries      tally.Counter
    69  	duplicateNoReconcile    tally.Counter
    70  	duplicateNeedsReconcile tally.Counter
    71  }
    72  
    73  // NewEntryMetrics builds an entry metrics.
    74  func NewEntryMetrics(scope tally.Scope) *EntryMetrics {
    75  	return &EntryMetrics{
    76  		gcNoReconcile: scope.Tagged(map[string]string{
    77  			"reconcile": "no_reconcile",
    78  			"path":      "gc",
    79  		}).Counter("count"),
    80  		gcNeedsReconcile: scope.Tagged(map[string]string{
    81  			"reconcile": "needs_reconcile",
    82  			"path":      "gc",
    83  		}).Counter("count"),
    84  		gcSuccessShardClosed: scope.Tagged(map[string]string{
    85  			"reason": "shard_closed",
    86  			"path":   "gc",
    87  		}).Counter("gc_count"),
    88  		gcSuccessEmpty: scope.Tagged(map[string]string{
    89  			"reason": "empty",
    90  			"path":   "gc",
    91  		}).Counter("gc_count"),
    92  		noGcNil: scope.Tagged(map[string]string{
    93  			"reason": "nil",
    94  			"path":   "gc",
    95  		}).Counter("no_gc_count"),
    96  		noGcErr: scope.Tagged(map[string]string{
    97  			"reason": "error",
    98  			"path":   "gc",
    99  		}).Counter("no_gc_count"),
   100  		noGcHasReaders: scope.Tagged(map[string]string{
   101  			"reason": "has_readers",
   102  			"path":   "gc",
   103  		}).Counter("no_gc_count"),
   104  		noGcNotEmptySeries: scope.Tagged(map[string]string{
   105  			"reason": "not_empty_series",
   106  			"path":   "gc",
   107  		}).Counter("no_gc_count"),
   108  
   109  		duplicateNoReconcile: scope.Tagged(map[string]string{
   110  			"reconcile": "no_reconcile",
   111  			"path":      "duplicate",
   112  		}).Counter("count"),
   113  		duplicateNeedsReconcile: scope.Tagged(map[string]string{
   114  			"reconcile": "needs_reconcile",
   115  			"path":      "duplicate",
   116  		}).Counter("count"),
   117  	}
   118  }
   119  
   120  // Entry is the entry in the shard ident.ID -> series map. It has additional
   121  // members to track lifecycle and minimize indexing overhead.
   122  // NB: users are expected to use `NewEntry` to construct these objects.
   123  type Entry struct {
   124  	ID                       ident.ID
   125  	Shard                    Shard
   126  	Series                   series.DatabaseSeries
   127  	Index                    uint64
   128  	IndexGarbageCollected    *xatomic.Bool
   129  	insertTime               *xatomic.Int64
   130  	indexWriter              IndexWriter
   131  	curReadWriters           int32
   132  	reverseIndex             entryIndexState
   133  	nowFn                    clock.NowFn
   134  	metrics                  *EntryMetrics
   135  	pendingIndexBatchSizeOne []writes.PendingIndexInsert
   136  }
   137  
   138  // ensure Entry satisfies the `doc.OnIndexSeries` interface.
   139  var _ doc.OnIndexSeries = &Entry{}
   140  
   141  // ensure Entry satisfies the `bootstrap.SeriesRef` interface.
   142  var _ bootstrap.SeriesRef = &Entry{}
   143  
   144  // ensure Entry satisfies the `bootstrap.SeriesRefResolver` interface.
   145  var _ bootstrap.SeriesRefResolver = &Entry{}
   146  
   147  // NewEntryOptions supplies options for a new entry.
   148  type NewEntryOptions struct {
   149  	Shard        Shard
   150  	Series       series.DatabaseSeries
   151  	Index        uint64
   152  	IndexWriter  IndexWriter
   153  	NowFn        clock.NowFn
   154  	EntryMetrics *EntryMetrics
   155  }
   156  
   157  // NewEntry returns a new Entry.
   158  func NewEntry(opts NewEntryOptions) *Entry {
   159  	nowFn := time.Now
   160  	if opts.NowFn != nil {
   161  		nowFn = opts.NowFn
   162  	}
   163  	entry := &Entry{
   164  		ID:                       opts.Series.ID(),
   165  		Shard:                    opts.Shard,
   166  		Series:                   opts.Series,
   167  		Index:                    opts.Index,
   168  		IndexGarbageCollected:    xatomic.NewBool(false),
   169  		insertTime:               xatomic.NewInt64(0),
   170  		indexWriter:              opts.IndexWriter,
   171  		nowFn:                    nowFn,
   172  		pendingIndexBatchSizeOne: make([]writes.PendingIndexInsert, 1),
   173  		reverseIndex:             newEntryIndexState(),
   174  		metrics:                  opts.EntryMetrics,
   175  	}
   176  	return entry
   177  }
   178  
   179  // StringID returns the index series ID, as a string.
   180  func (entry *Entry) StringID() string {
   181  	return entry.ID.String()
   182  }
   183  
   184  // ReaderWriterCount returns the current ref count on the Entry.
   185  func (entry *Entry) ReaderWriterCount() int32 {
   186  	return atomic.LoadInt32(&entry.curReadWriters)
   187  }
   188  
   189  // IncrementReaderWriterCount increments the ref count on the Entry.
   190  func (entry *Entry) IncrementReaderWriterCount() {
   191  	atomic.AddInt32(&entry.curReadWriters, 1)
   192  }
   193  
   194  // DecrementReaderWriterCount decrements the ref count on the Entry.
   195  func (entry *Entry) DecrementReaderWriterCount() {
   196  	atomic.AddInt32(&entry.curReadWriters, -1)
   197  }
   198  
   199  // IndexedBlockCount returns the count of indexed block states.
   200  func (entry *Entry) IndexedBlockCount() int {
   201  	entry.reverseIndex.RLock()
   202  	count := len(entry.reverseIndex.states)
   203  	entry.reverseIndex.RUnlock()
   204  	return count
   205  }
   206  
   207  // IndexedForBlockStart returns a bool to indicate if the Entry has been successfully
   208  // indexed for the given index blockStart.
   209  func (entry *Entry) IndexedForBlockStart(indexBlockStart xtime.UnixNano) bool {
   210  	entry.reverseIndex.RLock()
   211  	isIndexed := entry.reverseIndex.indexedWithRLock(indexBlockStart)
   212  	entry.reverseIndex.RUnlock()
   213  	return isIndexed
   214  }
   215  
   216  // IndexedRange returns minimum and maximum blockStart values covered by index entry.
   217  // The range is inclusive. Note that there may be uncovered gaps within the range.
   218  // Returns (0, 0) for an empty range.
   219  func (entry *Entry) IndexedRange() (xtime.UnixNano, xtime.UnixNano) {
   220  	entry.reverseIndex.RLock()
   221  	min, max := entry.reverseIndex.indexedRangeWithRLock()
   222  	entry.reverseIndex.RUnlock()
   223  	return min, max
   224  }
   225  
   226  // ReconciledOnIndexSeries attempts to retrieve the most recent index entry from the
   227  // shard if the entry this method was called on was never inserted there. If there
   228  // is an error during retrieval, simply returns the current entry. Additionally,
   229  // returns a cleanup function to run once finished using the reconciled entry and
   230  // a boolean value indicating whether the result came from reconciliation or not.
   231  func (entry *Entry) ReconciledOnIndexSeries() (doc.OnIndexSeries, resource.SimpleCloser, bool) {
   232  	if entry.insertTime.Load() > 0 {
   233  		return entry, resource.SimpleCloserFn(func() {}), false
   234  	}
   235  
   236  	e, _, err := entry.Shard.TryRetrieveSeriesAndIncrementReaderWriterCount(entry.ID)
   237  	if err != nil || e == nil {
   238  		return entry, resource.SimpleCloserFn(func() {}), false
   239  	}
   240  
   241  	// NB: attempt to merge the index series here, to ensure the returned
   242  	// reconciled series will have each index block marked from both this and the
   243  	// reconciliated series.
   244  	entry.mergeInto(e)
   245  
   246  	return e, resource.SimpleCloserFn(func() {
   247  		e.DecrementReaderWriterCount()
   248  	}), true
   249  }
   250  
   251  // MergeEntryIndexBlockStates merges the given states into the current
   252  // indexed entry.
   253  func (entry *Entry) MergeEntryIndexBlockStates(states doc.EntryIndexBlockStates) {
   254  	entry.reverseIndex.Lock()
   255  	for t, state := range states {
   256  		set := false
   257  		if state.Success {
   258  			set = true
   259  			entry.reverseIndex.setSuccessWithWLock(t)
   260  		} else {
   261  			// NB: setSuccessWithWLock(t) will perform the logic to determine if
   262  			// minIndexedT/maxIndexedT need to be updated; if this is not being called
   263  			// these should be updated.
   264  			if entry.reverseIndex.maxIndexedT < t {
   265  				entry.reverseIndex.maxIndexedT = t
   266  			}
   267  			if entry.reverseIndex.minIndexedT > t {
   268  				entry.reverseIndex.minIndexedT = t
   269  			}
   270  		}
   271  
   272  		if state.Attempt {
   273  			set = true
   274  			entry.reverseIndex.setAttemptWithWLock(t, false)
   275  		}
   276  
   277  		if !set {
   278  			// NB: if not set through the above methods, need to create an index block
   279  			// state at the given timestamp.
   280  			entry.reverseIndex.states[t] = doc.EntryIndexBlockState{}
   281  		}
   282  	}
   283  
   284  	entry.reverseIndex.Unlock()
   285  }
   286  
   287  // NeedsIndexUpdate returns a bool to indicate if the Entry needs to be indexed
   288  // for the provided blockStart. It only allows a single index attempt at a time
   289  // for a single entry.
   290  // NB(prateek): NeedsIndexUpdate is a CAS, i.e. when this method returns true, it
   291  // also sets state on the entry to indicate that a write for the given blockStart
   292  // is going to be sent to the index, and other go routines should not attempt the
   293  // same write. Callers are expected to ensure they follow this guideline.
   294  // Further, every call to NeedsIndexUpdate which returns true needs to have a corresponding
   295  // OnIndexFinalize() call. This is required for correct lifecycle maintenance.
   296  func (entry *Entry) NeedsIndexUpdate(indexBlockStartForWrite xtime.UnixNano) bool {
   297  	// first we try the low-cost path: acquire a RLock and see if the given block start
   298  	// has been marked successful or that we've attempted it.
   299  	entry.reverseIndex.RLock()
   300  	alreadyIndexedOrAttempted := entry.reverseIndex.indexedOrAttemptedWithRLock(indexBlockStartForWrite)
   301  	entry.reverseIndex.RUnlock()
   302  	if alreadyIndexedOrAttempted {
   303  		// if so, the entry does not need to be indexed.
   304  		return false
   305  	}
   306  
   307  	// now acquire a write lock and set that we're going to attempt to do this so we don't try
   308  	// multiple times.
   309  	entry.reverseIndex.Lock()
   310  	// NB(prateek): not defer-ing here, need to avoid the the extra ~150ns to minimize contention.
   311  
   312  	// but first, we have to ensure no one has done so since we released the read lock
   313  	alreadyIndexedOrAttempted = entry.reverseIndex.indexedOrAttemptedWithRLock(indexBlockStartForWrite)
   314  	if alreadyIndexedOrAttempted {
   315  		entry.reverseIndex.Unlock()
   316  		return false
   317  	}
   318  
   319  	entry.reverseIndex.setAttemptWithWLock(indexBlockStartForWrite, true)
   320  	entry.reverseIndex.Unlock()
   321  	return true
   322  }
   323  
   324  // OnIndexPrepare prepares the Entry to be handed off to the indexing sub-system.
   325  // NB(prateek): we retain the ref count on the entry while the indexing is pending,
   326  // the callback executed on the entry once the indexing is completed releases this
   327  // reference.
   328  func (entry *Entry) OnIndexPrepare(blockStartNanos xtime.UnixNano) {
   329  	entry.reverseIndex.Lock()
   330  	entry.reverseIndex.setAttemptWithWLock(blockStartNanos, true)
   331  	entry.reverseIndex.Unlock()
   332  	entry.IncrementReaderWriterCount()
   333  }
   334  
   335  // OnIndexSuccess marks the given block start as successfully indexed.
   336  func (entry *Entry) OnIndexSuccess(blockStartNanos xtime.UnixNano) {
   337  	entry.reverseIndex.Lock()
   338  	entry.reverseIndex.setSuccessWithWLock(blockStartNanos)
   339  	entry.reverseIndex.Unlock()
   340  }
   341  
   342  // OnIndexFinalize marks any attempt for the given block start as finished
   343  // and decrements the entry ref count.
   344  func (entry *Entry) OnIndexFinalize(blockStartNanos xtime.UnixNano) {
   345  	entry.reverseIndex.Lock()
   346  	entry.reverseIndex.setAttemptWithWLock(blockStartNanos, false)
   347  	entry.reverseIndex.Unlock()
   348  	// indicate the index has released held reference for provided write
   349  	entry.DecrementReaderWriterCount()
   350  }
   351  
   352  // IfAlreadyIndexedMarkIndexSuccessAndFinalize marks the entry as successfully
   353  // indexed if already indexed and returns true. Otherwise returns false.
   354  func (entry *Entry) IfAlreadyIndexedMarkIndexSuccessAndFinalize(
   355  	blockStart xtime.UnixNano,
   356  ) bool {
   357  	successAlready := false
   358  	entry.reverseIndex.Lock()
   359  	for _, state := range entry.reverseIndex.states {
   360  		if state.Success {
   361  			successAlready = true
   362  			break
   363  		}
   364  	}
   365  	if successAlready {
   366  		entry.reverseIndex.setSuccessWithWLock(blockStart)
   367  		entry.reverseIndex.setAttemptWithWLock(blockStart, false)
   368  	}
   369  	entry.reverseIndex.Unlock()
   370  	if successAlready {
   371  		// indicate the index has released held reference for provided write
   372  		entry.DecrementReaderWriterCount()
   373  	}
   374  	return successAlready
   375  }
   376  
   377  // TryMarkIndexGarbageCollected checks if the entry is eligible to be garbage collected
   378  // from the index. If so, it marks the entry as GCed and returns true. Otherwise returns false.
   379  func (entry *Entry) TryMarkIndexGarbageCollected() bool {
   380  	// Since series insertions + index insertions are done separately async, it is possible for
   381  	// a series to be in the index but not have data written yet, and so any series not in the
   382  	// lookup yet we cannot yet consider empty.
   383  	e, _, err := entry.Shard.TryRetrieveSeriesAndIncrementReaderWriterCount(entry.ID)
   384  	if m3errors.Is(err, errShardNotOpen) {
   385  		// Shard is closing, all entries which belonged to it should be gc'ed.
   386  		entry.metrics.gcSuccessShardClosed.Inc(1)
   387  		entry.IndexGarbageCollected.Store(true)
   388  		return true
   389  	}
   390  
   391  	if err != nil {
   392  		entry.metrics.noGcErr.Inc(1)
   393  		return false
   394  	}
   395  
   396  	if e == nil {
   397  		entry.metrics.noGcNil.Inc(1)
   398  		return false
   399  	}
   400  
   401  	defer e.DecrementReaderWriterCount()
   402  
   403  	// Was reconciled if the entry retrieved from the shard differs from the current.
   404  	if e != entry {
   405  		// If this entry needs further reconciliation, merge this entry's index
   406  		// states into the
   407  		entry.reverseIndex.RLock()
   408  		e.MergeEntryIndexBlockStates(entry.reverseIndex.states)
   409  		entry.reverseIndex.RUnlock()
   410  	}
   411  
   412  	// Consider non-empty if the entry is still being held since this could indicate
   413  	// another thread holding a new series prior to writing to it.
   414  	if e.ReaderWriterCount() > 1 {
   415  		entry.metrics.noGcHasReaders.Inc(1)
   416  		return false
   417  	}
   418  
   419  	// Series must be empty to be GCed. This happens when the data and index are flushed to disk and
   420  	// so the series no longer has in-mem data.
   421  	if !e.Series.IsEmpty() {
   422  		entry.metrics.noGcNotEmptySeries.Inc(1)
   423  		return false
   424  	}
   425  
   426  	// Mark as GCed from index so the entry can be safely cleaned up in the shard.
   427  	// The reference to this entry from the index is removed by the code path that
   428  	// marks this GCed bool.
   429  	e.metrics.gcSuccessEmpty.Inc(1)
   430  	e.IndexGarbageCollected.Store(true)
   431  
   432  	if e != entry {
   433  		entry.metrics.gcNeedsReconcile.Inc(1)
   434  	} else {
   435  		entry.metrics.gcNoReconcile.Inc(1)
   436  	}
   437  
   438  	return true
   439  }
   440  
   441  // mergeInto merges this entry index blocks into the provided index series.
   442  func (entry *Entry) mergeInto(indexSeries doc.OnIndexSeries) {
   443  	if entry == indexSeries {
   444  		// NB: short circuit if attempting to merge an entry into itself.
   445  		return
   446  	}
   447  
   448  	entry.reverseIndex.RLock()
   449  	indexSeries.MergeEntryIndexBlockStates(entry.reverseIndex.states)
   450  	entry.reverseIndex.RUnlock()
   451  }
   452  
   453  // TryReconcileDuplicates attempts to reconcile the index states of this entry.
   454  func (entry *Entry) TryReconcileDuplicates() {
   455  	// Since series insertions + index insertions are done separately async, it is possible for
   456  	// a series to be in the index but not have data written yet, and so any series not in the
   457  	// lookup yet we cannot yet consider empty.
   458  	e, _, err := entry.Shard.TryRetrieveSeriesAndIncrementReaderWriterCount(entry.ID)
   459  	if err != nil || e == nil {
   460  		return
   461  	}
   462  
   463  	if e != entry {
   464  		entry.mergeInto(e)
   465  		entry.metrics.duplicateNeedsReconcile.Inc(1)
   466  	} else {
   467  		entry.metrics.duplicateNoReconcile.Inc(1)
   468  	}
   469  
   470  	e.DecrementReaderWriterCount()
   471  }
   472  
   473  // NeedsIndexGarbageCollected checks if the entry is eligible to be garbage collected
   474  // from the index. Otherwise returns false.
   475  func (entry *Entry) NeedsIndexGarbageCollected() bool {
   476  	// This is a cheaper check that loading the entry from the shard again
   477  	// which makes it cheaper to run frequently.
   478  	// It may not be as accurate, but it's fine for an approximation since
   479  	// only a single series in a segment needs to return true to trigger an
   480  	// index segment to be garbage collected.
   481  	if entry.insertTime.Load() == 0 {
   482  		return false // Not inserted, does not need garbage collection.
   483  	}
   484  
   485  	// NB(antanas): Entries need to be GC'ed for closed shards.
   486  	// Orphan entries will cause problems if same shard returns to the same node.
   487  	if entry.Shard.Closed() {
   488  		return true
   489  	}
   490  	// Check that a write is not potentially pending and the series is empty.
   491  	return entry.ReaderWriterCount() == 0 && entry.Series.IsEmpty()
   492  }
   493  
   494  // SetInsertTime marks the entry as having been inserted into the shard at a given timestamp.
   495  func (entry *Entry) SetInsertTime(t time.Time) {
   496  	entry.insertTime.Store(t.UnixNano())
   497  }
   498  
   499  // Write writes a new value.
   500  func (entry *Entry) Write(
   501  	ctx context.Context,
   502  	timestamp xtime.UnixNano,
   503  	value float64,
   504  	unit xtime.Unit,
   505  	annotation []byte,
   506  	wOpts series.WriteOptions,
   507  ) (bool, series.WriteType, error) {
   508  	if err := entry.maybeIndex(timestamp); err != nil {
   509  		return false, 0, err
   510  	}
   511  	return entry.Series.Write(
   512  		ctx,
   513  		timestamp,
   514  		value,
   515  		unit,
   516  		annotation,
   517  		wOpts,
   518  	)
   519  }
   520  
   521  // LoadBlock loads a single block into the series.
   522  func (entry *Entry) LoadBlock(
   523  	block block.DatabaseBlock,
   524  	writeType series.WriteType,
   525  ) error {
   526  	// TODO(bodu): We can remove this once we have index snapshotting as index snapshots will
   527  	// contained snapshotted index segments that cover snapshotted data.
   528  	if err := entry.maybeIndex(block.StartTime()); err != nil {
   529  		return err
   530  	}
   531  	return entry.Series.LoadBlock(block, writeType)
   532  }
   533  
   534  // UniqueIndex is the unique index for the series.
   535  func (entry *Entry) UniqueIndex() uint64 {
   536  	return entry.Series.UniqueIndex()
   537  }
   538  
   539  func (entry *Entry) maybeIndex(timestamp xtime.UnixNano) error {
   540  	idx := entry.indexWriter
   541  	if idx == nil {
   542  		return nil
   543  	}
   544  	if !entry.NeedsIndexUpdate(idx.BlockStartForWriteTime(timestamp)) {
   545  		return nil
   546  	}
   547  	entry.pendingIndexBatchSizeOne[0] = writes.PendingIndexInsert{
   548  		Entry: index.WriteBatchEntry{
   549  			Timestamp:     timestamp,
   550  			OnIndexSeries: entry,
   551  			EnqueuedAt:    entry.nowFn(),
   552  		},
   553  		Document: entry.Series.Metadata(),
   554  	}
   555  	entry.OnIndexPrepare(idx.BlockStartForWriteTime(timestamp))
   556  	return idx.WritePending(entry.pendingIndexBatchSizeOne)
   557  }
   558  
   559  // SeriesRef returns the series read write ref.
   560  func (entry *Entry) SeriesRef() (bootstrap.SeriesRef, error) {
   561  	return entry, nil
   562  }
   563  
   564  // ReleaseRef must be called after using the series ref
   565  // to release the reference count to the series so it can
   566  // be expired by the owning shard eventually.
   567  func (entry *Entry) ReleaseRef() {
   568  	entry.DecrementReaderWriterCount()
   569  }
   570  
   571  // entryIndexState is used to capture the state of indexing for a single shard
   572  // entry. It's used to prevent redundant indexing operations.
   573  // NB(prateek): We need this amount of state because in the worst case, as we can have 3 active blocks being
   574  // written to. Albeit that's an edge case due to bad configuration. Even outside of that, 2 blocks can
   575  // be written to due to delayed, out of order writes. Consider an index block size of 2h, and buffer
   576  // past of 10m. Say a write comes in at 2.05p (wallclock) for 2.05p (timestamp in the write), we'd index
   577  // the entry, and update the entry to have a success for 4p. Now imagine another write
   578  // comes in at 2.06p (wallclock) for 1.57p (timestamp in the write). We need to differentiate that we don't
   579  // have a write for the 12-2p block from the 2-4p block, or we'd drop the late write.
   580  type entryIndexState struct {
   581  	sync.RWMutex
   582  	states                   doc.EntryIndexBlockStates
   583  	minIndexedT, maxIndexedT xtime.UnixNano
   584  }
   585  
   586  func newEntryIndexState() entryIndexState {
   587  	return entryIndexState{
   588  		states: make(doc.EntryIndexBlockStates, 4),
   589  	}
   590  }
   591  
   592  func (s *entryIndexState) indexedRangeWithRLock() (xtime.UnixNano, xtime.UnixNano) {
   593  	return s.minIndexedT, s.maxIndexedT
   594  }
   595  
   596  func (s *entryIndexState) indexedWithRLock(t xtime.UnixNano) bool {
   597  	v, ok := s.states[t]
   598  	if ok {
   599  		return v.Success
   600  	}
   601  	return false
   602  }
   603  
   604  func (s *entryIndexState) indexedOrAttemptedWithRLock(t xtime.UnixNano) bool {
   605  	v, ok := s.states[t]
   606  	if ok {
   607  		return v.Success || v.Attempt
   608  	}
   609  	return false
   610  }
   611  
   612  func (s *entryIndexState) setSuccessWithWLock(t xtime.UnixNano) {
   613  	if s.indexedWithRLock(t) {
   614  		return
   615  	}
   616  
   617  	// NB(r): If not inserted state yet that means we need to make an insertion,
   618  	// this will happen if synchronously indexing and we haven't called
   619  	// NeedIndexUpdate before we indexed the series.
   620  	s.states[t] = doc.EntryIndexBlockState{
   621  		Success: true,
   622  	}
   623  
   624  	if t > s.maxIndexedT {
   625  		s.maxIndexedT = t
   626  	}
   627  	if t < s.minIndexedT || s.minIndexedT == 0 {
   628  		s.minIndexedT = t
   629  	}
   630  }
   631  
   632  func (s *entryIndexState) setAttemptWithWLock(t xtime.UnixNano, attempt bool) {
   633  	v, ok := s.states[t]
   634  	if ok {
   635  		if v.Success {
   636  			return // Attempt is not relevant if success.
   637  		}
   638  		v.Attempt = attempt
   639  		s.states[t] = v
   640  		return
   641  	}
   642  
   643  	s.states[t] = doc.EntryIndexBlockState{
   644  		Attempt: attempt,
   645  	}
   646  }