vitess.io/vitess@v0.16.2/go/vt/topotools/split.go (about)

     1  /*
     2  Copyright 2019 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package topotools
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"sort"
    23  
    24  	"context"
    25  
    26  	"vitess.io/vitess/go/vt/key"
    27  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    28  	"vitess.io/vitess/go/vt/topo"
    29  )
    30  
    31  // ValidateForReshard returns an error if sourceShards cannot reshard into
    32  // targetShards.
    33  func ValidateForReshard(sourceShards, targetShards []*topo.ShardInfo) error {
    34  	for _, source := range sourceShards {
    35  		for _, target := range targetShards {
    36  			if key.KeyRangeEqual(source.KeyRange, target.KeyRange) {
    37  				return fmt.Errorf("same keyrange is present in source and target: %v", key.KeyRangeString(source.KeyRange))
    38  			}
    39  		}
    40  	}
    41  	sourcekr, err := combineKeyRanges(sourceShards)
    42  	if err != nil {
    43  		return err
    44  	}
    45  	targetkr, err := combineKeyRanges(targetShards)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	if !key.KeyRangeEqual(sourcekr, targetkr) {
    50  		return fmt.Errorf("source and target keyranges don't match: %v vs %v", key.KeyRangeString(sourcekr), key.KeyRangeString(targetkr))
    51  	}
    52  	return nil
    53  }
    54  
    55  func combineKeyRanges(shards []*topo.ShardInfo) (*topodatapb.KeyRange, error) {
    56  	if len(shards) == 0 {
    57  		return nil, fmt.Errorf("there are no shards to combine")
    58  	}
    59  	result := shards[0].KeyRange
    60  	krmap := make(map[string]*topodatapb.KeyRange)
    61  	for _, si := range shards[1:] {
    62  		krmap[si.ShardName()] = si.KeyRange
    63  	}
    64  	for len(krmap) != 0 {
    65  		foundOne := false
    66  		for k, kr := range krmap {
    67  			newkr, ok := key.KeyRangeAdd(result, kr)
    68  			if ok {
    69  				foundOne = true
    70  				result = newkr
    71  				delete(krmap, k)
    72  			}
    73  		}
    74  		if !foundOne {
    75  			return nil, errors.New("shards don't form a contiguous keyrange")
    76  		}
    77  	}
    78  	return result, nil
    79  }
    80  
    81  // OverlappingShards contains sets of shards that overlap which each-other.
    82  // With this library, there is no guarantee of which set will be left or right.
    83  type OverlappingShards struct {
    84  	Left  []*topo.ShardInfo
    85  	Right []*topo.ShardInfo
    86  }
    87  
    88  // ContainsShard returns true if either Left or Right lists contain
    89  // the provided Shard.
    90  func (os *OverlappingShards) ContainsShard(shardName string) bool {
    91  	for _, l := range os.Left {
    92  		if l.ShardName() == shardName {
    93  			return true
    94  		}
    95  	}
    96  	for _, r := range os.Right {
    97  		if r.ShardName() == shardName {
    98  			return true
    99  		}
   100  	}
   101  	return false
   102  }
   103  
   104  // OverlappingShardsForShard returns the OverlappingShards object
   105  // from the list that has he provided shard, or nil
   106  func OverlappingShardsForShard(os []*OverlappingShards, shardName string) *OverlappingShards {
   107  	for _, o := range os {
   108  		if o.ContainsShard(shardName) {
   109  			return o
   110  		}
   111  	}
   112  	return nil
   113  }
   114  
   115  // FindOverlappingShards will return an array of OverlappingShards
   116  // for the provided keyspace.
   117  // We do not support more than two overlapping shards (for instance,
   118  // having 40-80, 40-60 and 40-50 in the same keyspace is not supported and
   119  // will return an error).
   120  // If shards don't perfectly overlap, they are not returned.
   121  func FindOverlappingShards(ctx context.Context, ts *topo.Server, keyspace string) ([]*OverlappingShards, error) {
   122  	shardMap, err := ts.FindAllShardsInKeyspace(ctx, keyspace)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	return findOverlappingShards(shardMap)
   128  }
   129  
   130  // findOverlappingShards does the work for FindOverlappingShards but
   131  // can be called on test data too.
   132  func findOverlappingShards(shardMap map[string]*topo.ShardInfo) ([]*OverlappingShards, error) {
   133  
   134  	var result []*OverlappingShards
   135  
   136  	for len(shardMap) > 0 {
   137  		var left []*topo.ShardInfo
   138  		var right []*topo.ShardInfo
   139  
   140  		// get the first value from the map, seed our left array with it
   141  		var name string
   142  		var si *topo.ShardInfo
   143  		for name, si = range shardMap {
   144  			break
   145  		}
   146  		left = append(left, si)
   147  		delete(shardMap, name)
   148  
   149  		// keep adding entries until we have no more to add
   150  		for {
   151  			foundOne := false
   152  
   153  			// try left to right
   154  			si := findIntersectingShard(shardMap, left)
   155  			if si != nil {
   156  				if intersect(si, right) {
   157  					return nil, fmt.Errorf("shard %v intersects with more than one shard, this is not supported", si.ShardName())
   158  				}
   159  				foundOne = true
   160  				right = append(right, si)
   161  			}
   162  
   163  			// try right to left
   164  			si = findIntersectingShard(shardMap, right)
   165  			if si != nil {
   166  				if intersect(si, left) {
   167  					return nil, fmt.Errorf("shard %v intersects with more than one shard, this is not supported", si.ShardName())
   168  				}
   169  				foundOne = true
   170  				left = append(left, si)
   171  			}
   172  
   173  			// we haven't found anything new, we're done
   174  			if !foundOne {
   175  				break
   176  			}
   177  		}
   178  
   179  		// save what we found if it's good
   180  		if len(right) > 0 {
   181  			// sort both lists
   182  			sort.Sort(shardInfoList(left))
   183  			sort.Sort(shardInfoList(right))
   184  
   185  			// we should not have holes on either side
   186  			hasHoles := false
   187  			for i := 0; i < len(left)-1; i++ {
   188  				if string(left[i].KeyRange.End) != string(left[i+1].KeyRange.Start) {
   189  					hasHoles = true
   190  				}
   191  			}
   192  			for i := 0; i < len(right)-1; i++ {
   193  				if string(right[i].KeyRange.End) != string(right[i+1].KeyRange.Start) {
   194  					hasHoles = true
   195  				}
   196  			}
   197  			if hasHoles {
   198  				continue
   199  			}
   200  
   201  			// the two sides should match
   202  			if !key.KeyRangeStartEqual(left[0].KeyRange, right[0].KeyRange) {
   203  				continue
   204  			}
   205  			if !key.KeyRangeEndEqual(left[len(left)-1].KeyRange, right[len(right)-1].KeyRange) {
   206  				continue
   207  			}
   208  
   209  			// all good, we have a valid overlap
   210  			result = append(result, &OverlappingShards{
   211  				Left:  left,
   212  				Right: right,
   213  			})
   214  		}
   215  	}
   216  	return result, nil
   217  }
   218  
   219  // findIntersectingShard will go through the map and take the first
   220  // entry in there that intersect with the source array, remove it from
   221  // the map, and return it
   222  func findIntersectingShard(shardMap map[string]*topo.ShardInfo, sourceArray []*topo.ShardInfo) *topo.ShardInfo {
   223  	for name, si := range shardMap {
   224  		for _, sourceShardInfo := range sourceArray {
   225  			if si.KeyRange == nil || sourceShardInfo.KeyRange == nil || key.KeyRangesIntersect(si.KeyRange, sourceShardInfo.KeyRange) {
   226  				delete(shardMap, name)
   227  				return si
   228  			}
   229  		}
   230  	}
   231  	return nil
   232  }
   233  
   234  // intersect returns true if the provided shard intersect with any shard
   235  // in the destination array
   236  func intersect(si *topo.ShardInfo, allShards []*topo.ShardInfo) bool {
   237  	for _, shard := range allShards {
   238  		if key.KeyRangesIntersect(si.KeyRange, shard.KeyRange) {
   239  			return true
   240  		}
   241  	}
   242  	return false
   243  }
   244  
   245  // shardInfoList is a helper type to sort ShardInfo array by keyrange
   246  type shardInfoList []*topo.ShardInfo
   247  
   248  // Len is part of sort.Interface
   249  func (sil shardInfoList) Len() int {
   250  	return len(sil)
   251  }
   252  
   253  // Less is part of sort.Interface
   254  func (sil shardInfoList) Less(i, j int) bool {
   255  	return string(sil[i].KeyRange.Start) < string(sil[j].KeyRange.Start)
   256  }
   257  
   258  // Swap is part of sort.Interface
   259  func (sil shardInfoList) Swap(i, j int) {
   260  	sil[i], sil[j] = sil[j], sil[i]
   261  }