github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/bootstrap/result/result_index.go (about)

     1  // Copyright (c) 2020 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 result
    22  
    23  import (
    24  	"fmt"
    25  
    26  	"github.com/m3db/m3/src/dbnode/namespace"
    27  	"github.com/m3db/m3/src/m3ninx/doc"
    28  	"github.com/m3db/m3/src/m3ninx/index"
    29  	"github.com/m3db/m3/src/m3ninx/index/segment"
    30  	"github.com/m3db/m3/src/m3ninx/index/segment/builder"
    31  	"github.com/m3db/m3/src/m3ninx/persist"
    32  	xtime "github.com/m3db/m3/src/x/time"
    33  )
    34  
    35  // NewDefaultDocumentsBuilderAllocator returns a default mutable segment
    36  // allocator.
    37  func NewDefaultDocumentsBuilderAllocator() DocumentsBuilderAllocator {
    38  	return func() (segment.DocumentsBuilder, error) {
    39  		return builder.NewBuilderFromDocuments(builder.NewOptions())
    40  	}
    41  }
    42  
    43  type indexBootstrapResult struct {
    44  	results     IndexResults
    45  	unfulfilled ShardTimeRanges
    46  }
    47  
    48  // NewIndexBootstrapResult returns a new index bootstrap result.
    49  func NewIndexBootstrapResult() IndexBootstrapResult {
    50  	return &indexBootstrapResult{
    51  		results:     make(IndexResults),
    52  		unfulfilled: NewShardTimeRanges(),
    53  	}
    54  }
    55  
    56  func (r *indexBootstrapResult) IndexResults() IndexResults {
    57  	return r.results
    58  }
    59  
    60  func (r *indexBootstrapResult) Unfulfilled() ShardTimeRanges {
    61  	return r.unfulfilled
    62  }
    63  
    64  func (r *indexBootstrapResult) SetUnfulfilled(unfulfilled ShardTimeRanges) {
    65  	r.unfulfilled = unfulfilled
    66  }
    67  
    68  func (r *indexBootstrapResult) Add(blocks IndexBlockByVolumeType, unfulfilled ShardTimeRanges) {
    69  	r.results.Add(blocks)
    70  	r.unfulfilled.AddRanges(unfulfilled)
    71  }
    72  
    73  func (r *indexBootstrapResult) NumSeries() int {
    74  	var size int64
    75  	for _, blockByVolumeType := range r.results {
    76  		for _, b := range blockByVolumeType.data {
    77  			for _, s := range b.segments {
    78  				size += s.Segment().Size()
    79  			}
    80  		}
    81  	}
    82  	return int(size)
    83  }
    84  
    85  // NewIndexBuilder creates a wrapped locakble index seg builder.
    86  func NewIndexBuilder(builder segment.DocumentsBuilder) *IndexBuilder {
    87  	return &IndexBuilder{
    88  		builder: builder,
    89  	}
    90  }
    91  
    92  // FlushBatch flushes a batch of documents to the underlying segment builder.
    93  func (b *IndexBuilder) FlushBatch(batch []doc.Metadata) ([]doc.Metadata, error) {
    94  	if len(batch) == 0 {
    95  		// Last flush might not have any docs enqueued
    96  		return batch, nil
    97  	}
    98  
    99  	// NB(bodu): Prevent concurrent writes.
   100  	// Although it seems like there's no need to lock on writes since
   101  	// each block should ONLY be getting built in a single thread.
   102  	err := b.builder.InsertBatch(index.Batch{
   103  		Docs:                batch,
   104  		AllowPartialUpdates: true,
   105  	})
   106  	if err != nil && index.IsBatchPartialError(err) {
   107  		// If after filtering out duplicate ID errors
   108  		// there are no errors, then this was a successful
   109  		// insertion.
   110  		batchErr := err.(*index.BatchPartialError)
   111  		// NB(r): FilterDuplicateIDErrors returns nil
   112  		// if no errors remain after filtering duplicate ID
   113  		// errors, this case is covered in unit tests.
   114  		err = batchErr.FilterDuplicateIDErrors()
   115  	}
   116  	if err != nil {
   117  		return batch, err
   118  	}
   119  
   120  	// Reset docs batch for reuse
   121  	var empty doc.Metadata
   122  	for i := range batch {
   123  		batch[i] = empty
   124  	}
   125  	batch = batch[:0]
   126  	return batch, nil
   127  }
   128  
   129  // Builder returns the underlying index segment docs builder.
   130  func (b *IndexBuilder) Builder() segment.DocumentsBuilder {
   131  	return b.builder
   132  }
   133  
   134  // AddBlockIfNotExists adds an index block if it does not already exist to the index results.
   135  func (r IndexResults) AddBlockIfNotExists(
   136  	t xtime.UnixNano,
   137  	idxopts namespace.IndexOptions,
   138  ) {
   139  	// NB(r): The reason we can align by the retention block size and guarantee
   140  	// there is only one entry for this time is because index blocks must be a
   141  	// positive multiple of the data block size, making it easy to map a data
   142  	// block entry to at most one index block entry.
   143  	blockStart := t.Truncate(idxopts.BlockSize())
   144  	blockStartNanos := blockStart
   145  
   146  	_, exists := r[blockStartNanos]
   147  	if !exists {
   148  		r[blockStartNanos] = NewIndexBlockByVolumeType(blockStart)
   149  	}
   150  }
   151  
   152  // Add will add an index block to the collection, merging if one already
   153  // exists.
   154  func (r IndexResults) Add(blocks IndexBlockByVolumeType) {
   155  	if blocks.BlockStart().IsZero() {
   156  		return
   157  	}
   158  
   159  	// Merge results
   160  	blockStart := blocks.BlockStart()
   161  	existing, ok := r[blockStart]
   162  	if !ok {
   163  		r[blockStart] = blocks
   164  		return
   165  	}
   166  
   167  	r[blockStart] = existing.Merged(blocks)
   168  }
   169  
   170  // AddResults will add another set of index results to the collection, merging
   171  // if index blocks already exists.
   172  func (r IndexResults) AddResults(other IndexResults) {
   173  	for _, blocks := range other {
   174  		r.Add(blocks)
   175  	}
   176  }
   177  
   178  // MarkFulfilled will mark an index block as fulfilled, either partially or
   179  // wholly as specified by the shard time ranges passed.
   180  func (r IndexResults) MarkFulfilled(
   181  	t xtime.UnixNano,
   182  	fulfilled ShardTimeRanges,
   183  	indexVolumeType persist.IndexVolumeType,
   184  	idxopts namespace.IndexOptions,
   185  ) error {
   186  	// NB(r): The reason we can align by the retention block size and guarantee
   187  	// there is only one entry for this time is because index blocks must be a
   188  	// positive multiple of the data block size, making it easy to map a data
   189  	// block entry to at most one index block entry.
   190  	blockStart := t.Truncate(idxopts.BlockSize())
   191  	blockStartNanos := blockStart
   192  
   193  	blockRange := xtime.Range{
   194  		Start: blockStart,
   195  		End:   blockStart.Add(idxopts.BlockSize()),
   196  	}
   197  
   198  	// First check fulfilled is correct
   199  	min, max := fulfilled.MinMax()
   200  	if min.Before(blockRange.Start) || max.After(blockRange.End) {
   201  		return fmt.Errorf("fulfilled range %s is outside of index block range: %s",
   202  			fulfilled.SummaryString(), blockRange.String())
   203  	}
   204  
   205  	blocks, exists := r[blockStartNanos]
   206  	if !exists {
   207  		blocks = NewIndexBlockByVolumeType(blockStart)
   208  		r[blockStartNanos] = blocks
   209  	}
   210  
   211  	block, exists := blocks.data[indexVolumeType]
   212  	if !exists {
   213  		block = NewIndexBlock(nil, nil)
   214  		blocks.data[indexVolumeType] = block
   215  	}
   216  	blocks.data[indexVolumeType] = block.Merged(NewIndexBlock(nil, fulfilled))
   217  	return nil
   218  }
   219  
   220  // MergedIndexBootstrapResult returns a merged result of two bootstrap results.
   221  // It is a mutating function that mutates the larger result by adding the
   222  // smaller result to it and then finally returns the mutated result.
   223  func MergedIndexBootstrapResult(i, j IndexBootstrapResult) IndexBootstrapResult {
   224  	if i == nil {
   225  		return j
   226  	}
   227  	if j == nil {
   228  		return i
   229  	}
   230  	sizeI, sizeJ := 0, 0
   231  	for _, ir := range i.IndexResults() {
   232  		for _, b := range ir.data {
   233  			sizeI += len(b.Segments())
   234  		}
   235  	}
   236  	for _, ir := range j.IndexResults() {
   237  		for _, b := range ir.data {
   238  			sizeJ += len(b.Segments())
   239  		}
   240  	}
   241  	if sizeI >= sizeJ {
   242  		i.IndexResults().AddResults(j.IndexResults())
   243  		i.Unfulfilled().AddRanges(j.Unfulfilled())
   244  		return i
   245  	}
   246  	j.IndexResults().AddResults(i.IndexResults())
   247  	j.Unfulfilled().AddRanges(i.Unfulfilled())
   248  	return j
   249  }
   250  
   251  // NewIndexBlock returns a new bootstrap index block result.
   252  func NewIndexBlock(
   253  	segments []Segment,
   254  	fulfilled ShardTimeRanges,
   255  ) IndexBlock {
   256  	if fulfilled == nil {
   257  		fulfilled = NewShardTimeRanges()
   258  	}
   259  	return IndexBlock{
   260  		segments:  segments,
   261  		fulfilled: fulfilled,
   262  	}
   263  }
   264  
   265  // Segments returns the segments.
   266  func (b IndexBlock) Segments() []Segment {
   267  	return b.segments
   268  }
   269  
   270  // Fulfilled returns the fulfilled time ranges by this index block.
   271  func (b IndexBlock) Fulfilled() ShardTimeRanges {
   272  	return b.fulfilled
   273  }
   274  
   275  // Merged returns a new merged index block, currently it just appends the
   276  // list of segments from the other index block and the caller merges
   277  // as they see necessary.
   278  func (b IndexBlock) Merged(other IndexBlock) IndexBlock {
   279  	r := b
   280  	if len(other.segments) > 0 {
   281  		r.segments = append(r.segments, other.segments...)
   282  	}
   283  	if !other.fulfilled.IsEmpty() {
   284  		r.fulfilled = b.fulfilled.Copy()
   285  		r.fulfilled.AddRanges(other.fulfilled)
   286  	}
   287  	return r
   288  }
   289  
   290  // NewIndexBlockByVolumeType returns a new bootstrap index blocks by volume type result.
   291  func NewIndexBlockByVolumeType(blockStart xtime.UnixNano) IndexBlockByVolumeType {
   292  	return IndexBlockByVolumeType{
   293  		blockStart: blockStart,
   294  		data:       make(map[persist.IndexVolumeType]IndexBlock),
   295  	}
   296  }
   297  
   298  // BlockStart returns the block start.
   299  func (b IndexBlockByVolumeType) BlockStart() xtime.UnixNano {
   300  	return b.blockStart
   301  }
   302  
   303  // GetBlock returns an IndexBlock for volumeType.
   304  func (b IndexBlockByVolumeType) GetBlock(volumeType persist.IndexVolumeType) (IndexBlock, bool) {
   305  	block, ok := b.data[volumeType]
   306  	return block, ok
   307  }
   308  
   309  // SetBlock sets an IndexBlock for volumeType.
   310  func (b IndexBlockByVolumeType) SetBlock(volumeType persist.IndexVolumeType, block IndexBlock) {
   311  	b.data[volumeType] = block
   312  }
   313  
   314  // Iter returns the underlying iterable map data.
   315  func (b IndexBlockByVolumeType) Iter() map[persist.IndexVolumeType]IndexBlock {
   316  	return b.data
   317  }
   318  
   319  // Merged returns a new merged index block by volume type.
   320  // It merges the underlying index blocks together by index volume type.
   321  func (b IndexBlockByVolumeType) Merged(other IndexBlockByVolumeType) IndexBlockByVolumeType {
   322  	r := b
   323  	for volumeType, otherBlock := range other.data {
   324  		existing, ok := r.data[volumeType]
   325  		if !ok {
   326  			r.data[volumeType] = otherBlock
   327  			continue
   328  		}
   329  		r.data[volumeType] = existing.Merged(otherBlock)
   330  	}
   331  	return r
   332  }