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 }