github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/store/nbs/conjoiner.go (about)

     1  // Copyright 2019 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // This file incorporates work covered by the following copyright and
    16  // permission notice:
    17  //
    18  // Copyright 2017 Attic Labs, Inc. All rights reserved.
    19  // Licensed under the Apache License, version 2.0:
    20  // http://www.apache.org/licenses/LICENSE-2.0
    21  
    22  package nbs
    23  
    24  import (
    25  	"context"
    26  	"errors"
    27  	"sort"
    28  	"sync"
    29  	"time"
    30  
    31  	"github.com/dolthub/dolt/go/store/atomicerr"
    32  )
    33  
    34  type conjoiner interface {
    35  	// ConjoinRequired tells the caller whether or not it's time to request a
    36  	// Conjoin, based upon the contents of |ts| and the conjoiner
    37  	// implementation's policy.
    38  	ConjoinRequired(ts tableSet) bool
    39  
    40  	// Conjoin attempts to use |p| to conjoin some number of tables referenced
    41  	// by |upstream|, allowing it to update |mm| with a new, smaller, set of tables
    42  	// that references precisely the same set of chunks. Conjoin() may not
    43  	// actually conjoin any upstream tables, usually because some out-of-
    44  	// process actor has already landed a conjoin of its own. Callers must
    45  	// handle this, likely by rebasing against upstream and re-evaluating the
    46  	// situation.
    47  	Conjoin(ctx context.Context, upstream manifestContents, mm manifestUpdater, p tablePersister, stats *Stats) (manifestContents, error)
    48  }
    49  
    50  type inlineConjoiner struct {
    51  	maxTables int
    52  }
    53  
    54  func (c inlineConjoiner) ConjoinRequired(ts tableSet) bool {
    55  	return ts.Size() > c.maxTables && len(ts.upstream) >= 2
    56  }
    57  
    58  func (c inlineConjoiner) Conjoin(ctx context.Context, upstream manifestContents, mm manifestUpdater, p tablePersister, stats *Stats) (manifestContents, error) {
    59  	return conjoin(ctx, upstream, mm, p, stats)
    60  }
    61  
    62  type noopConjoiner struct {
    63  }
    64  
    65  func (c noopConjoiner) ConjoinRequired(ts tableSet) bool {
    66  	return false
    67  }
    68  
    69  func (c noopConjoiner) Conjoin(ctx context.Context, upstream manifestContents, mm manifestUpdater, p tablePersister, stats *Stats) (manifestContents, error) {
    70  	return manifestContents{}, errors.New("unsupported conjoin operation on noopConjoiner")
    71  }
    72  
    73  func conjoin(ctx context.Context, upstream manifestContents, mm manifestUpdater, p tablePersister, stats *Stats) (manifestContents, error) {
    74  	var conjoined tableSpec
    75  	var conjoinees, keepers, appendixSpecs []tableSpec
    76  
    77  	for {
    78  		if conjoinees == nil {
    79  			// Appendix table files should never be conjoined
    80  			// so we remove them before conjoining and add them
    81  			// back after
    82  			if upstream.NumAppendixSpecs() != 0 {
    83  				upstream, appendixSpecs = upstream.removeAppendixSpecs()
    84  			}
    85  
    86  			var err error
    87  			conjoined, conjoinees, keepers, err = conjoinTables(ctx, p, upstream.specs, stats)
    88  
    89  			if err != nil {
    90  				return manifestContents{}, err
    91  			}
    92  		}
    93  
    94  		specs := append(make([]tableSpec, 0, len(keepers)+1), conjoined)
    95  		if len(appendixSpecs) > 0 {
    96  			specs = append(make([]tableSpec, 0, len(specs)+len(appendixSpecs)), appendixSpecs...)
    97  			specs = append(specs, conjoined)
    98  		}
    99  
   100  		specs = append(specs, keepers...)
   101  
   102  		newContents := manifestContents{
   103  			vers:     upstream.vers,
   104  			root:     upstream.root,
   105  			lock:     generateLockHash(upstream.root, specs),
   106  			gcGen:    upstream.gcGen,
   107  			specs:    specs,
   108  			appendix: appendixSpecs,
   109  		}
   110  
   111  		var err error
   112  		upstream, err = mm.Update(ctx, upstream.lock, newContents, stats, nil)
   113  		if err != nil {
   114  			return manifestContents{}, err
   115  		}
   116  
   117  		if newContents.lock == upstream.lock {
   118  			return upstream, nil
   119  		}
   120  
   121  		// Optimistic lock failure. Someone else moved to the root, the set of tables, or both out from under us.
   122  		// If we can re-use the conjoin we already performed, we want to try again. Currently, we will only do so if ALL conjoinees are still present upstream. If we can't re-use...then someone else almost certainly landed a conjoin upstream. In this case, bail and let clients ask again if they think they still can't proceed.
   123  
   124  		// If the appendix has changed we simply bail
   125  		// and let the client retry
   126  		if len(appendixSpecs) > 0 {
   127  			if len(upstream.appendix) != len(appendixSpecs) {
   128  				return upstream, nil
   129  			}
   130  			for i := range upstream.appendix {
   131  				if upstream.appendix[i].name != appendixSpecs[i].name {
   132  					return upstream, nil
   133  				}
   134  			}
   135  
   136  			// No appendix change occured, so we remove the appendix
   137  			// on the "latest" upstream which will be added back
   138  			// before the conjoin completes
   139  			upstream, appendixSpecs = upstream.removeAppendixSpecs()
   140  		}
   141  
   142  		conjoineeSet := map[addr]struct{}{}
   143  		upstreamNames := map[addr]struct{}{}
   144  		for _, spec := range upstream.specs {
   145  			upstreamNames[spec.name] = struct{}{}
   146  		}
   147  		for _, c := range conjoinees {
   148  			if _, present := upstreamNames[c.name]; !present {
   149  				return upstream, nil // Bail!
   150  			}
   151  			conjoineeSet[c.name] = struct{}{}
   152  		}
   153  
   154  		// Filter conjoinees out of upstream.specs to generate new set of keepers
   155  		keepers = make([]tableSpec, 0, len(upstream.specs)-len(conjoinees))
   156  		for _, spec := range upstream.specs {
   157  			if _, present := conjoineeSet[spec.name]; !present {
   158  				keepers = append(keepers, spec)
   159  			}
   160  		}
   161  	}
   162  }
   163  
   164  func conjoinTables(ctx context.Context, p tablePersister, upstream []tableSpec, stats *Stats) (conjoined tableSpec, conjoinees, keepers []tableSpec, err error) {
   165  	// Open all the upstream tables concurrently
   166  	sources := make(chunkSources, len(upstream))
   167  
   168  	ae := atomicerr.New()
   169  	wg := sync.WaitGroup{}
   170  	for i, spec := range upstream {
   171  		wg.Add(1)
   172  		go func(idx int, spec tableSpec) {
   173  			defer wg.Done()
   174  			var err error
   175  			sources[idx], err = p.Open(ctx, spec.name, spec.chunkCount, stats)
   176  
   177  			ae.SetIfError(err)
   178  		}(i, spec)
   179  		i++
   180  	}
   181  	wg.Wait()
   182  
   183  	if err := ae.Get(); err != nil {
   184  		return tableSpec{}, nil, nil, err
   185  	}
   186  
   187  	t1 := time.Now()
   188  
   189  	toConjoin, toKeep, err := chooseConjoinees(sources)
   190  
   191  	if err != nil {
   192  		return tableSpec{}, nil, nil, err
   193  	}
   194  
   195  	conjoinedSrc, err := p.ConjoinAll(ctx, toConjoin, stats)
   196  
   197  	if err != nil {
   198  		return tableSpec{}, nil, nil, err
   199  	}
   200  
   201  	stats.ConjoinLatency.SampleTimeSince(t1)
   202  	stats.TablesPerConjoin.SampleLen(len(toConjoin))
   203  
   204  	cnt, err := conjoinedSrc.count()
   205  
   206  	if err != nil {
   207  		return tableSpec{}, nil, nil, err
   208  	}
   209  
   210  	stats.ChunksPerConjoin.Sample(uint64(cnt))
   211  
   212  	conjoinees, err = toSpecs(toConjoin)
   213  
   214  	if err != nil {
   215  		return tableSpec{}, nil, nil, err
   216  	}
   217  
   218  	keepers, err = toSpecs(toKeep)
   219  
   220  	if err != nil {
   221  		return tableSpec{}, nil, nil, err
   222  	}
   223  
   224  	h, err := conjoinedSrc.hash()
   225  
   226  	if err != nil {
   227  		return tableSpec{}, nil, nil, err
   228  	}
   229  
   230  	cnt, err = conjoinedSrc.count()
   231  
   232  	if err != nil {
   233  		return tableSpec{}, nil, nil, err
   234  	}
   235  
   236  	return tableSpec{h, cnt}, conjoinees, keepers, nil
   237  }
   238  
   239  // Current approach is to choose the smallest N tables which, when removed and replaced with the conjoinment, will leave the conjoinment as the smallest table.
   240  func chooseConjoinees(upstream chunkSources) (toConjoin, toKeep chunkSources, err error) {
   241  	sortedUpstream := make(chunkSources, len(upstream))
   242  	copy(sortedUpstream, upstream)
   243  
   244  	csbac := chunkSourcesByAscendingCount{sortedUpstream, nil}
   245  	sort.Sort(csbac)
   246  
   247  	if csbac.err != nil {
   248  		return nil, nil, csbac.err
   249  	}
   250  
   251  	partition := 2
   252  	upZero, err := sortedUpstream[0].count()
   253  
   254  	if err != nil {
   255  		return nil, nil, err
   256  	}
   257  
   258  	upOne, err := sortedUpstream[1].count()
   259  
   260  	if err != nil {
   261  		return nil, nil, err
   262  	}
   263  
   264  	sum := upZero + upOne
   265  	for partition < len(sortedUpstream) {
   266  		partCnt, err := sortedUpstream[partition].count()
   267  
   268  		if err != nil {
   269  			return nil, nil, err
   270  		}
   271  
   272  		if sum <= partCnt {
   273  			break
   274  		}
   275  
   276  		sum += partCnt
   277  		partition++
   278  	}
   279  
   280  	return sortedUpstream[:partition], sortedUpstream[partition:], nil
   281  }
   282  
   283  func toSpecs(srcs chunkSources) ([]tableSpec, error) {
   284  	specs := make([]tableSpec, len(srcs))
   285  	for i, src := range srcs {
   286  		cnt, err := src.count()
   287  
   288  		if err != nil {
   289  			return nil, err
   290  		}
   291  
   292  		if cnt <= 0 {
   293  			return nil, errors.New("invalid table spec has no sources")
   294  		}
   295  
   296  		h, err := src.hash()
   297  
   298  		if err != nil {
   299  			return nil, err
   300  		}
   301  
   302  		cnt, err = src.count()
   303  
   304  		if err != nil {
   305  			return nil, err
   306  		}
   307  
   308  		specs[i] = tableSpec{h, cnt}
   309  	}
   310  
   311  	return specs, nil
   312  }