github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/block/block.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 block
    22  
    23  import (
    24  	"errors"
    25  	"sync"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"github.com/m3db/m3/src/dbnode/namespace"
    30  	"github.com/m3db/m3/src/dbnode/ts"
    31  	"github.com/m3db/m3/src/dbnode/x/xio"
    32  	"github.com/m3db/m3/src/x/context"
    33  	"github.com/m3db/m3/src/x/ident"
    34  	xtime "github.com/m3db/m3/src/x/time"
    35  )
    36  
    37  var (
    38  	errReadFromClosedBlock       = errors.New("attempt to read from a closed block")
    39  	errTriedToMergeBlockFromDisk = errors.New("[invariant violated] tried to merge a block that was retrieved from disk")
    40  
    41  	timeZero = xtime.UnixNano(0)
    42  )
    43  
    44  type dbBlock struct {
    45  	sync.RWMutex
    46  
    47  	nsCtx          namespace.Context
    48  	opts           Options
    49  	startUnixNanos xtime.UnixNano
    50  	segment        ts.Segment
    51  	length         int
    52  
    53  	blockSize time.Duration
    54  
    55  	lastReadUnixNanos int64
    56  
    57  	mergeTarget DatabaseBlock
    58  
    59  	seriesID ident.ID
    60  
    61  	onEvicted OnEvictedFromWiredList
    62  
    63  	// listState contains state that the Wired List requires in order to track a block's
    64  	// position in the wired list. All the state in this struct is "owned" by the wired
    65  	// list and should only be accessed by the Wired List itself. Does not require any
    66  	// synchronization because the WiredList is not concurrent.
    67  	listState listState
    68  
    69  	checksum uint32
    70  
    71  	wasRetrievedFromDisk bool
    72  	closed               bool
    73  }
    74  
    75  type listState struct {
    76  	next                  DatabaseBlock
    77  	prev                  DatabaseBlock
    78  	enteredListAtUnixNano int64
    79  }
    80  
    81  // NewDatabaseBlock creates a new DatabaseBlock instance.
    82  func NewDatabaseBlock(
    83  	start xtime.UnixNano,
    84  	blockSize time.Duration,
    85  	segment ts.Segment,
    86  	opts Options,
    87  	nsCtx namespace.Context,
    88  ) DatabaseBlock {
    89  	b := &dbBlock{
    90  		nsCtx:          nsCtx,
    91  		opts:           opts,
    92  		startUnixNanos: start,
    93  		blockSize:      blockSize,
    94  		closed:         false,
    95  	}
    96  	if segment.Len() > 0 {
    97  		b.resetSegmentWithLock(segment)
    98  	}
    99  	return b
   100  }
   101  
   102  func (b *dbBlock) StartTime() xtime.UnixNano {
   103  	b.RLock()
   104  	start := b.startWithRLock()
   105  	b.RUnlock()
   106  	return start
   107  }
   108  
   109  func (b *dbBlock) BlockSize() time.Duration {
   110  	b.RLock()
   111  	size := b.blockSize
   112  	b.RUnlock()
   113  	return size
   114  }
   115  
   116  func (b *dbBlock) startWithRLock() xtime.UnixNano {
   117  	return b.startUnixNanos
   118  }
   119  
   120  func (b *dbBlock) SetLastReadTime(value xtime.UnixNano) {
   121  	// Use an int64 to avoid needing a write lock for
   122  	// this high frequency called method (i.e. each individual
   123  	// read needing a write lock would be excessive)
   124  	atomic.StoreInt64(&b.lastReadUnixNanos, int64(value))
   125  }
   126  
   127  func (b *dbBlock) LastReadTime() xtime.UnixNano {
   128  	return xtime.UnixNano(atomic.LoadInt64(&b.lastReadUnixNanos))
   129  }
   130  
   131  func (b *dbBlock) Empty() bool {
   132  	b.RLock()
   133  	empty := b.length == 0
   134  	b.RUnlock()
   135  	return empty
   136  }
   137  
   138  func (b *dbBlock) Len() int {
   139  	b.RLock()
   140  	length := b.length
   141  	b.RUnlock()
   142  	return length
   143  }
   144  
   145  func (b *dbBlock) Checksum() (uint32, error) {
   146  	b.RLock()
   147  	checksum := b.checksum
   148  	hasMergeTarget := b.mergeTarget != nil
   149  	b.RUnlock()
   150  
   151  	if !hasMergeTarget {
   152  		return checksum, nil
   153  	}
   154  
   155  	b.Lock()
   156  	defer b.Unlock()
   157  	// Since we released the lock temporarily we need to check again.
   158  	hasMergeTarget = b.mergeTarget != nil
   159  	if !hasMergeTarget {
   160  		return b.checksum, nil
   161  	}
   162  
   163  	tempCtx := b.opts.ContextPool().Get()
   164  
   165  	stream, err := b.streamWithRLock(tempCtx)
   166  	if err != nil {
   167  		return 0, err
   168  	}
   169  
   170  	// This will merge the existing stream with the merge target's stream,
   171  	// as well as recalculate and store the new checksum.
   172  	err = b.forceMergeWithLock(tempCtx, stream)
   173  	if err != nil {
   174  		return 0, err
   175  	}
   176  
   177  	return b.checksum, nil
   178  }
   179  
   180  func (b *dbBlock) Stream(blocker context.Context) (xio.BlockReader, error) {
   181  	lockUpgraded := false
   182  
   183  	b.RLock()
   184  	defer func() {
   185  		if lockUpgraded {
   186  			b.Unlock()
   187  		} else {
   188  			b.RUnlock()
   189  		}
   190  	}()
   191  
   192  	if b.closed {
   193  		return xio.EmptyBlockReader, errReadFromClosedBlock
   194  	}
   195  
   196  	if b.mergeTarget == nil {
   197  		return b.streamWithRLock(blocker)
   198  	}
   199  
   200  	b.RUnlock()
   201  	lockUpgraded = true
   202  	b.Lock()
   203  
   204  	// Need to re-check everything since we upgraded the lock.
   205  	if b.closed {
   206  		return xio.EmptyBlockReader, errReadFromClosedBlock
   207  	}
   208  
   209  	stream, err := b.streamWithRLock(blocker)
   210  	if err != nil {
   211  		return xio.EmptyBlockReader, err
   212  	}
   213  
   214  	if b.mergeTarget == nil {
   215  		return stream, nil
   216  	}
   217  
   218  	// This will merge the existing stream with the merge target's stream,
   219  	// as well as recalculate and store the new checksum.
   220  	err = b.forceMergeWithLock(blocker, stream)
   221  	if err != nil {
   222  		return xio.EmptyBlockReader, err
   223  	}
   224  
   225  	// This will return a copy of the data so that it is still safe to
   226  	// close the block after calling this method.
   227  	return b.streamWithRLock(blocker)
   228  }
   229  
   230  func (b *dbBlock) HasMergeTarget() bool {
   231  	b.RLock()
   232  	hasMergeTarget := b.mergeTarget != nil
   233  	b.RUnlock()
   234  	return hasMergeTarget
   235  }
   236  
   237  func (b *dbBlock) WasRetrievedFromDisk() bool {
   238  	b.RLock()
   239  	wasRetrieved := b.wasRetrievedFromDisk
   240  	b.RUnlock()
   241  	return wasRetrieved
   242  }
   243  
   244  func (b *dbBlock) Merge(other DatabaseBlock) error {
   245  	b.Lock()
   246  	if b.wasRetrievedFromDisk || other.WasRetrievedFromDisk() {
   247  		// We use Merge to lazily merge blocks that eventually need to be flushed to disk
   248  		// If we try to perform a merge on blocks that were retrieved from disk then we've
   249  		// violated an invariant and probably have a bug that is causing data loss.
   250  		b.Unlock()
   251  		return errTriedToMergeBlockFromDisk
   252  	}
   253  
   254  	if b.mergeTarget == nil {
   255  		b.mergeTarget = other
   256  	} else {
   257  		b.mergeTarget.Merge(other)
   258  	}
   259  
   260  	b.Unlock()
   261  	return nil
   262  }
   263  
   264  func (b *dbBlock) Reset(
   265  	start xtime.UnixNano,
   266  	blockSize time.Duration,
   267  	segment ts.Segment,
   268  	nsCtx namespace.Context,
   269  ) {
   270  	b.Lock()
   271  	defer b.Unlock()
   272  	b.resetNewBlockStartWithLock(start, blockSize)
   273  	b.resetSegmentWithLock(segment)
   274  	b.nsCtx = nsCtx
   275  }
   276  
   277  func (b *dbBlock) ResetFromDisk(
   278  	start xtime.UnixNano,
   279  	blockSize time.Duration,
   280  	segment ts.Segment,
   281  	id ident.ID,
   282  	nsCtx namespace.Context,
   283  ) {
   284  	b.Lock()
   285  	defer b.Unlock()
   286  	b.resetNewBlockStartWithLock(start, blockSize)
   287  	// resetSegmentWithLock sets seriesID to nil
   288  	b.resetSegmentWithLock(segment)
   289  	b.seriesID = id
   290  	b.nsCtx = nsCtx
   291  	b.wasRetrievedFromDisk = true
   292  }
   293  
   294  func (b *dbBlock) streamWithRLock(ctx context.Context) (xio.BlockReader, error) {
   295  	start := b.startWithRLock()
   296  
   297  	// Take a copy to avoid heavy depends on cycle
   298  	segmentReader := b.opts.SegmentReaderPool().Get()
   299  	data := b.opts.BytesPool().Get(b.segment.Len())
   300  	data.IncRef()
   301  	if b.segment.Head != nil {
   302  		data.AppendAll(b.segment.Head.Bytes())
   303  	}
   304  	if b.segment.Tail != nil {
   305  		data.AppendAll(b.segment.Tail.Bytes())
   306  	}
   307  	data.DecRef()
   308  	checksum := b.segment.CalculateChecksum()
   309  	segmentReader.Reset(ts.NewSegment(data, nil, checksum, ts.FinalizeHead))
   310  	ctx.RegisterFinalizer(segmentReader)
   311  
   312  	blockReader := xio.BlockReader{
   313  		SegmentReader: segmentReader,
   314  		Start:         start,
   315  		BlockSize:     b.blockSize,
   316  	}
   317  
   318  	return blockReader, nil
   319  }
   320  
   321  func (b *dbBlock) forceMergeWithLock(ctx context.Context, stream xio.SegmentReader) error {
   322  	targetStream, err := b.mergeTarget.Stream(ctx)
   323  	if err != nil {
   324  		return err
   325  	}
   326  	start := b.startWithRLock()
   327  	mergedBlockReader := newDatabaseMergedBlockReader(b.nsCtx, start, b.blockSize,
   328  		mergeableStream{stream: stream, finalize: false},       // Should have been marked for finalization by the caller
   329  		mergeableStream{stream: targetStream, finalize: false}, // Already marked for finalization by the Stream() call above
   330  		b.opts)
   331  	mergedSegment, err := mergedBlockReader.Segment()
   332  	if err != nil {
   333  		return err
   334  	}
   335  
   336  	b.resetMergeTargetWithLock()
   337  	b.resetSegmentWithLock(mergedSegment)
   338  	return nil
   339  }
   340  
   341  func (b *dbBlock) resetNewBlockStartWithLock(start xtime.UnixNano, blockSize time.Duration) {
   342  	b.startUnixNanos = start
   343  	b.blockSize = blockSize
   344  	atomic.StoreInt64(&b.lastReadUnixNanos, 0)
   345  	b.closed = false
   346  	b.resetMergeTargetWithLock()
   347  }
   348  
   349  func (b *dbBlock) resetSegmentWithLock(seg ts.Segment) {
   350  	b.segment = seg
   351  	b.length = seg.Len()
   352  	b.checksum = seg.CalculateChecksum()
   353  	b.seriesID = nil
   354  	b.wasRetrievedFromDisk = false
   355  }
   356  
   357  func (b *dbBlock) Discard() ts.Segment {
   358  	seg, _ := b.closeAndDiscardConditionally(nil)
   359  	return seg
   360  }
   361  
   362  func (b *dbBlock) Close() {
   363  	segment, _ := b.closeAndDiscardConditionally(nil)
   364  	segment.Finalize()
   365  }
   366  
   367  func (b *dbBlock) CloseIfFromDisk() bool {
   368  	segment, ok := b.closeAndDiscardConditionally(func(b *dbBlock) bool {
   369  		return b.wasRetrievedFromDisk
   370  	})
   371  	if !ok {
   372  		return false
   373  	}
   374  	segment.Finalize()
   375  	return true
   376  }
   377  
   378  func (b *dbBlock) closeAndDiscardConditionally(condition func(b *dbBlock) bool) (ts.Segment, bool) {
   379  	b.Lock()
   380  
   381  	if condition != nil && !condition(b) {
   382  		b.Unlock()
   383  		return ts.Segment{}, false
   384  	}
   385  
   386  	if b.closed {
   387  		b.Unlock()
   388  		return ts.Segment{}, true
   389  	}
   390  
   391  	segment := b.segment
   392  	b.closed = true
   393  
   394  	b.resetMergeTargetWithLock()
   395  	b.Unlock()
   396  
   397  	if pool := b.opts.DatabaseBlockPool(); pool != nil {
   398  		pool.Put(b)
   399  	}
   400  
   401  	return segment, true
   402  }
   403  
   404  func (b *dbBlock) resetMergeTargetWithLock() {
   405  	if b.mergeTarget != nil {
   406  		b.mergeTarget.Close()
   407  	}
   408  	b.mergeTarget = nil
   409  }
   410  
   411  // Should only be used by the WiredList.
   412  func (b *dbBlock) next() DatabaseBlock {
   413  	return b.listState.next
   414  }
   415  
   416  // Should only be used by the WiredList.
   417  func (b *dbBlock) setNext(value DatabaseBlock) {
   418  	b.listState.next = value
   419  }
   420  
   421  // Should only be used by the WiredList.
   422  func (b *dbBlock) prev() DatabaseBlock {
   423  	return b.listState.prev
   424  }
   425  
   426  // Should only be used by the WiredList.
   427  func (b *dbBlock) setPrev(value DatabaseBlock) {
   428  	b.listState.prev = value
   429  }
   430  
   431  // Should only be used by the WiredList.
   432  func (b *dbBlock) enteredListAtUnixNano() int64 {
   433  	return b.listState.enteredListAtUnixNano
   434  }
   435  
   436  // Should only be used by the WiredList.
   437  func (b *dbBlock) setEnteredListAtUnixNano(value int64) {
   438  	b.listState.enteredListAtUnixNano = value
   439  }
   440  
   441  // wiredListEntry is a snapshot of a subset of the block's state that the WiredList
   442  // uses to determine if a block is eligible for inclusion in the WiredList.
   443  type wiredListEntry struct {
   444  	seriesID             ident.ID
   445  	startTime            xtime.UnixNano
   446  	closed               bool
   447  	wasRetrievedFromDisk bool
   448  }
   449  
   450  // wiredListEntry generates a wiredListEntry for the block, and should only
   451  // be used by the WiredList.
   452  func (b *dbBlock) wiredListEntry() wiredListEntry {
   453  	b.RLock()
   454  	result := wiredListEntry{
   455  		closed:               b.closed,
   456  		seriesID:             b.seriesID,
   457  		wasRetrievedFromDisk: b.wasRetrievedFromDisk,
   458  		startTime:            b.startWithRLock(),
   459  	}
   460  	b.RUnlock()
   461  	return result
   462  }
   463  
   464  func (b *dbBlock) SetOnEvictedFromWiredList(onEvicted OnEvictedFromWiredList) {
   465  	b.Lock()
   466  	b.onEvicted = onEvicted
   467  	b.Unlock()
   468  }
   469  
   470  func (b *dbBlock) OnEvictedFromWiredList() OnEvictedFromWiredList {
   471  	b.RLock()
   472  	onEvicted := b.onEvicted
   473  	b.RUnlock()
   474  	return onEvicted
   475  }
   476  
   477  type databaseSeriesBlocks struct {
   478  	elems map[xtime.UnixNano]DatabaseBlock
   479  	min   xtime.UnixNano
   480  	max   xtime.UnixNano
   481  }
   482  
   483  // NewDatabaseSeriesBlocks creates a databaseSeriesBlocks instance.
   484  func NewDatabaseSeriesBlocks(capacity int) DatabaseSeriesBlocks {
   485  	return &databaseSeriesBlocks{
   486  		elems: make(map[xtime.UnixNano]DatabaseBlock, capacity),
   487  	}
   488  }
   489  
   490  func (dbb *databaseSeriesBlocks) Len() int {
   491  	return len(dbb.elems)
   492  }
   493  
   494  func (dbb *databaseSeriesBlocks) AddBlock(block DatabaseBlock) {
   495  	start := block.StartTime()
   496  	if dbb.min.Equal(timeZero) || start.Before(dbb.min) {
   497  		dbb.min = start
   498  	}
   499  	if dbb.max.Equal(timeZero) || start.After(dbb.max) {
   500  		dbb.max = start
   501  	}
   502  	dbb.elems[start] = block
   503  }
   504  
   505  func (dbb *databaseSeriesBlocks) AddSeries(other DatabaseSeriesBlocks) {
   506  	if other == nil {
   507  		return
   508  	}
   509  	blocks := other.AllBlocks()
   510  	for _, b := range blocks {
   511  		dbb.AddBlock(b)
   512  	}
   513  }
   514  
   515  // MinTime returns the min time of the blocks contained.
   516  func (dbb *databaseSeriesBlocks) MinTime() xtime.UnixNano {
   517  	return dbb.min
   518  }
   519  
   520  // MaxTime returns the max time of the blocks contained.
   521  func (dbb *databaseSeriesBlocks) MaxTime() xtime.UnixNano {
   522  	return dbb.max
   523  }
   524  
   525  func (dbb *databaseSeriesBlocks) BlockAt(t xtime.UnixNano) (DatabaseBlock, bool) {
   526  	b, ok := dbb.elems[t]
   527  	return b, ok
   528  }
   529  
   530  func (dbb *databaseSeriesBlocks) AllBlocks() map[xtime.UnixNano]DatabaseBlock {
   531  	return dbb.elems
   532  }
   533  
   534  func (dbb *databaseSeriesBlocks) RemoveBlockAt(t xtime.UnixNano) {
   535  	if _, exists := dbb.elems[t]; !exists {
   536  		return
   537  	}
   538  	delete(dbb.elems, t)
   539  	if dbb.min != t && dbb.max != t {
   540  		return
   541  	}
   542  	dbb.min, dbb.max = timeZero, timeZero
   543  	if len(dbb.elems) == 0 {
   544  		return
   545  	}
   546  	for key := range dbb.elems {
   547  		if dbb.min == timeZero || dbb.min > key {
   548  			dbb.min = key
   549  		}
   550  		if dbb.max == timeZero || dbb.max < key {
   551  			dbb.max = key
   552  		}
   553  	}
   554  }
   555  
   556  func (dbb *databaseSeriesBlocks) RemoveAll() {
   557  	for t, block := range dbb.elems {
   558  		block.Close()
   559  		delete(dbb.elems, t)
   560  	}
   561  }
   562  
   563  func (dbb *databaseSeriesBlocks) Reset() {
   564  	// Ensure the old, possibly large map is GC'd
   565  	dbb.elems = nil
   566  	dbb.elems = make(map[xtime.UnixNano]DatabaseBlock)
   567  	dbb.min = 0
   568  	dbb.max = 0
   569  }
   570  
   571  func (dbb *databaseSeriesBlocks) Close() {
   572  	dbb.RemoveAll()
   573  	// Mark the map as nil to prevent maps that have grown large from wasting
   574  	// space in the pool (Deleting elements from a large map will not cause
   575  	// the underlying resources to shrink)
   576  	dbb.elems = nil
   577  }