vitess.io/vitess@v0.16.2/go/vt/topotools/split_test.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  	"encoding/hex"
    21  	"testing"
    22  
    23  	"github.com/stretchr/testify/assert"
    24  
    25  	"vitess.io/vitess/go/vt/topo"
    26  
    27  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    28  )
    29  
    30  // helper methods for tests to be shorter
    31  
    32  func hki(hexValue string) []byte {
    33  	k, err := hex.DecodeString(hexValue)
    34  	if err != nil {
    35  		panic(err)
    36  	}
    37  	return k
    38  }
    39  
    40  func si(start, end string) *topo.ShardInfo {
    41  	s := hki(start)
    42  	e := hki(end)
    43  	return topo.NewShardInfo("keyspace", start+"-"+end, &topodatapb.Shard{
    44  		KeyRange: &topodatapb.KeyRange{
    45  			Start: s,
    46  			End:   e,
    47  		},
    48  	}, nil)
    49  }
    50  
    51  type expectedOverlappingShard struct {
    52  	left  []string
    53  	right []string
    54  }
    55  
    56  func overlappingShardMatch(ol []*topo.ShardInfo, or []*topo.ShardInfo, e expectedOverlappingShard) bool {
    57  	if len(ol)+1 != len(e.left) {
    58  		return false
    59  	}
    60  	if len(or)+1 != len(e.right) {
    61  		return false
    62  	}
    63  	for i, l := range ol {
    64  		if l.ShardName() != e.left[i]+"-"+e.left[i+1] {
    65  			return false
    66  		}
    67  	}
    68  	for i, r := range or {
    69  		if r.ShardName() != e.right[i]+"-"+e.right[i+1] {
    70  			return false
    71  		}
    72  	}
    73  	return true
    74  }
    75  
    76  func compareResultLists(t *testing.T, os []*OverlappingShards, expected []expectedOverlappingShard) {
    77  	if len(os) != len(expected) {
    78  		t.Errorf("Unexpected result length, got %v, want %v", len(os), len(expected))
    79  		return
    80  	}
    81  
    82  	for _, o := range os {
    83  		found := false
    84  		for _, e := range expected {
    85  			if overlappingShardMatch(o.Left, o.Right, e) {
    86  				found = true
    87  			}
    88  			if overlappingShardMatch(o.Right, o.Left, e) {
    89  				found = true
    90  			}
    91  		}
    92  		if !found {
    93  			t.Errorf("OverlappingShard %v not found in expected %v", o, expected)
    94  			return
    95  		}
    96  	}
    97  }
    98  
    99  func TestValidateForReshard(t *testing.T) {
   100  	testcases := []struct {
   101  		sources []string
   102  		targets []string
   103  		out     string
   104  	}{{
   105  		sources: []string{"-80", "80-"},
   106  		targets: []string{"-40", "40-"},
   107  		out:     "",
   108  	}, {
   109  		sources: []string{"52-53"},
   110  		targets: []string{"5200-5240", "5240-5280", "5280-52c0", "52c0-5300"},
   111  		out:     "",
   112  	}, {
   113  		sources: []string{"5200-5300"},
   114  		targets: []string{"520000-524000", "524000-528000", "528000-52c000", "52c000-530000"},
   115  		out:     "",
   116  	}, {
   117  		sources: []string{"-80", "80-"},
   118  		targets: []string{"-4000000000000000", "4000000000000000-8000000000000000", "8000000000000000-80c0000000000000", "80c0000000000000-"},
   119  		out:     "",
   120  	}, {
   121  		sources: []string{"80-", "-80"},
   122  		targets: []string{"-40", "40-"},
   123  		out:     "",
   124  	}, {
   125  		sources: []string{"-40", "40-80", "80-"},
   126  		targets: []string{"-30", "30-"},
   127  		out:     "",
   128  	}, {
   129  		sources: []string{"0"},
   130  		targets: []string{"-40", "40-"},
   131  		out:     "",
   132  	}, {
   133  		sources: []string{"-40", "40-80", "80-"},
   134  		targets: []string{"-40", "40-"},
   135  		out:     "same keyrange is present in source and target: -40",
   136  	}, {
   137  		sources: []string{"-30", "30-80"},
   138  		targets: []string{"-40", "40-"},
   139  		out:     "source and target keyranges don't match: -80 vs -",
   140  	}, {
   141  		sources: []string{"-30", "20-80"},
   142  		targets: []string{"-40", "40-"},
   143  		out:     "shards don't form a contiguous keyrange",
   144  	}}
   145  	buildShards := func(shards []string) []*topo.ShardInfo {
   146  		sis := make([]*topo.ShardInfo, 0, len(shards))
   147  		for _, shard := range shards {
   148  			_, kr, err := topo.ValidateShardName(shard)
   149  			if err != nil {
   150  				panic(err)
   151  			}
   152  			sis = append(sis, topo.NewShardInfo("", shard, &topodatapb.Shard{KeyRange: kr}, nil))
   153  		}
   154  		return sis
   155  	}
   156  
   157  	for _, tcase := range testcases {
   158  		sources := buildShards(tcase.sources)
   159  		targets := buildShards(tcase.targets)
   160  		err := ValidateForReshard(sources, targets)
   161  		if tcase.out == "" {
   162  			assert.NoError(t, err)
   163  		} else {
   164  			assert.EqualError(t, err, tcase.out)
   165  		}
   166  	}
   167  }
   168  
   169  func TestFindOverlappingShardsNoOverlap(t *testing.T) {
   170  	var shardMap map[string]*topo.ShardInfo
   171  	var os []*OverlappingShards
   172  	var err error
   173  
   174  	// no shards
   175  	shardMap = map[string]*topo.ShardInfo{}
   176  	os, err = findOverlappingShards(shardMap)
   177  	if len(os) != 0 || err != nil {
   178  		t.Errorf("empty shard map: %v %v", os, err)
   179  	}
   180  
   181  	// just one shard, full keyrange
   182  	shardMap = map[string]*topo.ShardInfo{
   183  		"0": {},
   184  	}
   185  	os, err = findOverlappingShards(shardMap)
   186  	if len(os) != 0 || err != nil {
   187  		t.Errorf("just one shard, full keyrange: %v %v", os, err)
   188  	}
   189  
   190  	// just one shard, partial keyrange
   191  	shardMap = map[string]*topo.ShardInfo{
   192  		"-80": si("", "80"),
   193  	}
   194  	os, err = findOverlappingShards(shardMap)
   195  	if len(os) != 0 || err != nil {
   196  		t.Errorf("just one shard, partial keyrange: %v %v", os, err)
   197  	}
   198  
   199  	// two non-overlapping shards
   200  	shardMap = map[string]*topo.ShardInfo{
   201  		"-80": si("", "80"),
   202  		"80":  si("80", ""),
   203  	}
   204  	os, err = findOverlappingShards(shardMap)
   205  	if len(os) != 0 || err != nil {
   206  		t.Errorf("two non-overlapping shards: %v %v", os, err)
   207  	}
   208  
   209  	// shards with holes
   210  	shardMap = map[string]*topo.ShardInfo{
   211  		"-80": si("", "80"),
   212  		"80":  si("80", ""),
   213  		"-20": si("", "20"),
   214  		// HOLE: "20-40": si("20", "40"),
   215  		"40-60": si("40", "60"),
   216  		"60-80": si("60", "80"),
   217  	}
   218  	os, err = findOverlappingShards(shardMap)
   219  	if len(os) != 0 || err != nil {
   220  		t.Errorf("shards with holes: %v %v", os, err)
   221  	}
   222  
   223  	// shards not overlapping
   224  	shardMap = map[string]*topo.ShardInfo{
   225  		"-80": si("", "80"),
   226  		"80":  si("80", ""),
   227  		// MISSING: "-20": si("", "20"),
   228  		"20-40": si("20", "40"),
   229  		"40-60": si("40", "60"),
   230  		"60-80": si("60", "80"),
   231  	}
   232  	os, err = findOverlappingShards(shardMap)
   233  	if len(os) != 0 || err != nil {
   234  		t.Errorf("shards not overlapping: %v %v", os, err)
   235  	}
   236  }
   237  
   238  func TestFindOverlappingShardsOverlap(t *testing.T) {
   239  	var shardMap map[string]*topo.ShardInfo
   240  	var os []*OverlappingShards
   241  	var err error
   242  
   243  	// split in progress
   244  	shardMap = map[string]*topo.ShardInfo{
   245  		"-80":   si("", "80"),
   246  		"80":    si("80", ""),
   247  		"-40":   si("", "40"),
   248  		"40-80": si("40", "80"),
   249  	}
   250  	os, err = findOverlappingShards(shardMap)
   251  	if len(os) != 1 || err != nil {
   252  		t.Errorf("split in progress: %v %v", os, err)
   253  	}
   254  	compareResultLists(t, os, []expectedOverlappingShard{
   255  		{
   256  			left:  []string{"", "80"},
   257  			right: []string{"", "40", "80"},
   258  		},
   259  	})
   260  
   261  	// 1 to 4 split
   262  	shardMap = map[string]*topo.ShardInfo{
   263  		"-":     si("", ""),
   264  		"-40":   si("", "40"),
   265  		"40-80": si("40", "80"),
   266  		"80-c0": si("80", "c0"),
   267  		"c0-":   si("c0", ""),
   268  	}
   269  	os, err = findOverlappingShards(shardMap)
   270  	if len(os) != 1 || err != nil {
   271  		t.Errorf("1 to 4 split: %v %v", os, err)
   272  	}
   273  	compareResultLists(t, os, []expectedOverlappingShard{
   274  		{
   275  			left:  []string{"", ""},
   276  			right: []string{"", "40", "80", "c0", ""},
   277  		},
   278  	})
   279  
   280  	// 2 to 3 split
   281  	shardMap = map[string]*topo.ShardInfo{
   282  		"-40":   si("", "40"),
   283  		"40-80": si("40", "80"),
   284  		"80-":   si("80", ""),
   285  		"-30":   si("", "30"),
   286  		"30-60": si("30", "60"),
   287  		"60-80": si("60", "80"),
   288  	}
   289  	os, err = findOverlappingShards(shardMap)
   290  	if len(os) != 1 || err != nil {
   291  		t.Errorf("2 to 3 split: %v %v", os, err)
   292  	}
   293  	compareResultLists(t, os, []expectedOverlappingShard{
   294  		{
   295  			left:  []string{"", "40", "80"},
   296  			right: []string{"", "30", "60", "80"},
   297  		},
   298  	})
   299  
   300  	// multiple concurrent splits
   301  	shardMap = map[string]*topo.ShardInfo{
   302  		"-80":   si("", "80"),
   303  		"80-":   si("80", ""),
   304  		"-40":   si("", "40"),
   305  		"40-80": si("40", "80"),
   306  		"80-c0": si("80", "c0"),
   307  		"c0-":   si("c0", ""),
   308  	}
   309  	os, err = findOverlappingShards(shardMap)
   310  	if len(os) != 2 || err != nil {
   311  		t.Errorf("2 to 3 split: %v %v", os, err)
   312  	}
   313  	compareResultLists(t, os, []expectedOverlappingShard{
   314  		{
   315  			left:  []string{"", "80"},
   316  			right: []string{"", "40", "80"},
   317  		},
   318  		{
   319  			left:  []string{"80", ""},
   320  			right: []string{"80", "c0", ""},
   321  		},
   322  	})
   323  
   324  	// find a shard in there
   325  	if o := OverlappingShardsForShard(os, "-60"); o != nil {
   326  		t.Errorf("Found a shard where I shouldn't have!")
   327  	}
   328  	if o := OverlappingShardsForShard(os, "-40"); o == nil {
   329  		t.Errorf("Found no shard where I should have!")
   330  	} else {
   331  		compareResultLists(t, []*OverlappingShards{o},
   332  			[]expectedOverlappingShard{
   333  				{
   334  					left:  []string{"", "80"},
   335  					right: []string{"", "40", "80"},
   336  				},
   337  			})
   338  	}
   339  }
   340  
   341  func TestFindOverlappingShardsErrors(t *testing.T) {
   342  	var shardMap map[string]*topo.ShardInfo
   343  	var err error
   344  
   345  	// 3 overlapping shards
   346  	shardMap = map[string]*topo.ShardInfo{
   347  		"-20": si("", "20"),
   348  		"-40": si("", "40"),
   349  		"-80": si("", "80"),
   350  	}
   351  	_, err = findOverlappingShards(shardMap)
   352  	if err == nil {
   353  		t.Errorf("3 overlapping shards with no error")
   354  	}
   355  }