github.com/m3db/m3@v1.5.0/src/cluster/placement/selector/mirrored_custom_groups_test.go (about) 1 // Copyright (c) 2019 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 selector 22 23 import ( 24 "testing" 25 26 "github.com/m3db/m3/src/cluster/placement" 27 "github.com/m3db/m3/src/x/instrument" 28 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 "go.uber.org/zap" 32 ) 33 34 const ( 35 // format: <groupID>_<instanceID> 36 instG1I1 = "g1_i1" 37 instG1I2 = "g1_i2" 38 instG1I3 = "g1_i3" 39 40 instG2I1 = "g2_i1" 41 instG2I2 = "g2_i2" 42 43 instG3I1 = "g3_i1" 44 instG3I2 = "g3_i2" 45 ) 46 47 var ( 48 testGroups = map[string]string{ 49 instG1I1: "group1", 50 instG1I2: "group1", 51 52 // for replacement 53 instG1I3: "group1", 54 55 instG2I1: "group2", 56 instG2I2: "group2", 57 58 // additional instances 59 instG3I1: "group3", 60 instG3I2: "group3", 61 } 62 ) 63 64 var ( 65 logger = zap.NewNop() 66 // uncomment for logging 67 // logger, _ = zap.NewDevelopment() 68 ) 69 70 // testAddingNodesBehavior contains assertions common to both GroupInitialInstances and 71 // GroupAddingInstances. 72 func testAddingNodesBehavior( 73 t *testing.T, 74 doAdd func( 75 tctx *mirroredCustomGroupSelectorTestContext, 76 candidates []placement.Instance) ([]placement.Instance, error), 77 ) { 78 t.Run("RF hosts in group", func(t *testing.T) { 79 tctx := mirroredCustomGroupSelectorSetup(t) 80 81 groups, err := doAdd(tctx, tctx.Instances) 82 require.NoError(t, err) 83 84 assertGroupsCorrect( 85 t, 86 [][]string{{instG1I1, instG1I2}, {instG2I1, instG2I2}}, 87 groups, 88 ) 89 }) 90 91 t.Run("too many hosts in group shortens the group to RF", func(t *testing.T) { 92 tctx := mirroredCustomGroupSelectorSetup(t) 93 tctx.Selector = NewMirroredCustomGroupSelector( 94 NewMapInstanceGroupIDFunc(map[string]string{ 95 instG1I1: "group1", 96 instG2I1: "group1", 97 instG1I2: "group1", 98 }), 99 newTestMirroredCustomGroupOptions(), 100 ) 101 tctx.Placement = tctx.Placement.SetReplicaFactor(2) 102 103 groups, err := doAdd(tctx, []placement.Instance{ 104 newInstanceWithID(instG1I1), 105 newInstanceWithID(instG2I1), 106 newInstanceWithID(instG1I2), 107 }) 108 require.NoError(t, err) 109 assert.Len(t, groups, 2) 110 }) 111 112 t.Run("no group configured errors", func(t *testing.T) { 113 tctx := mirroredCustomGroupSelectorSetup(t) 114 _, err := doAdd(tctx, []placement.Instance{ 115 newInstanceWithID(instG1I1), 116 newInstanceWithID("nogroup")}) 117 assert.EqualError(t, 118 err, 119 "finding group for nogroup: instance nogroup "+ 120 "doesn't have a corresponding group in ID to group map") 121 }) 122 123 t.Run("insufficient hosts in group is ok", func(t *testing.T) { 124 // case should be handled at a higher level. 125 tctx := mirroredCustomGroupSelectorSetup(t) 126 _, err := doAdd(tctx, []placement.Instance{ 127 newInstanceWithID(instG1I1), 128 }) 129 assert.NoError(t, err) 130 }) 131 132 t.Run("hosts in other zones are filtered", func(t *testing.T) { 133 tctx := mirroredCustomGroupSelectorSetup(t) 134 135 tctx.Selector = NewMirroredCustomGroupSelector( 136 tctx.GroupFn, 137 newTestMirroredCustomGroupOptions(). 138 SetValidZone("zone"). 139 SetAllowAllZones(false), 140 ) 141 142 instances, err := doAdd(tctx, []placement.Instance{ 143 newInstanceWithID(instG1I1).SetZone("zone"), 144 newInstanceWithID(instG1I2).SetZone("otherZone"), 145 }) 146 147 require.NoError(t, err) 148 // We didn't achieve RF here, but selector isn't responsible for that validation. 149 assertGroupsCorrect(t, [][]string{{instG1I1}}, instances) 150 }) 151 } 152 153 func TestExplicitMirroredCustomGroupSelector_SelectAddingInstances(t *testing.T) { 154 testAddingNodesBehavior( 155 t, 156 func(tctx *mirroredCustomGroupSelectorTestContext, candidates []placement.Instance) ([]placement.Instance, error) { 157 return tctx.Selector.SelectAddingInstances(candidates, tctx.Placement) 158 }, 159 ) 160 161 t.Run("adds only RF instances without AddAllInstances", func(t *testing.T) { 162 tctx := mirroredCustomGroupSelectorSetup(t) 163 164 tctx.Selector = NewMirroredCustomGroupSelector( 165 tctx.GroupFn, 166 newTestMirroredCustomGroupOptions().SetAddAllCandidates(false), 167 ) 168 groups, err := tctx.Selector.SelectAddingInstances(tctx.Instances, tctx.Placement) 169 require.NoError(t, err) 170 171 assert.Len(t, groups, 2) 172 }) 173 } 174 175 func TestExplicitMirroredCustomGroupSelector_SelectInitialInstances(t *testing.T) { 176 testAddingNodesBehavior( 177 t, 178 func(tctx *mirroredCustomGroupSelectorTestContext, candidates []placement.Instance) ([]placement.Instance, error) { 179 return tctx.Selector.SelectInitialInstances(candidates, tctx.Placement.ReplicaFactor()) 180 }, 181 ) 182 } 183 184 func TestExplicitMirroredCustomGroupSelector_SelectReplaceInstances(t *testing.T) { 185 type testContext struct { 186 *mirroredCustomGroupSelectorTestContext 187 ToReplace placement.Instance 188 } 189 setup := func(t *testing.T) *testContext { 190 tctx := mirroredCustomGroupSelectorSetup(t) 191 192 instances, err := tctx.Selector.SelectAddingInstances(tctx.Instances, tctx.Placement) 193 require.NoError(t, err) 194 195 tctx.Placement = tctx.Placement.SetInstances(instances) 196 197 toReplace, ok := tctx.Placement.Instance(instG1I1) 198 require.True(t, ok) 199 200 return &testContext{ 201 mirroredCustomGroupSelectorTestContext: tctx, 202 ToReplace: toReplace, 203 } 204 } 205 206 t.Run("correct replacement", func(t *testing.T) { 207 tctx := setup(t) 208 209 instG := newInstanceWithID(instG1I3) 210 replaceInstances, err := tctx.Selector.SelectReplaceInstances( 211 []placement.Instance{instG, newInstanceWithID(instG3I1)}, 212 []string{tctx.ToReplace.ID()}, 213 tctx.Placement, 214 ) 215 require.NoError(t, err) 216 217 assert.Equal(t, []placement.Instance{instG}, replaceInstances) 218 219 toReplace, ok := tctx.Placement.Instance(tctx.ToReplace.ID()) 220 require.True(t, ok) 221 222 require.NotZero(t, toReplace.ShardSetID()) 223 assert.Equal(t, tctx.ToReplace.ShardSetID(), replaceInstances[0].ShardSetID()) 224 }) 225 226 t.Run("no valid replacements", func(t *testing.T) { 227 tctx := setup(t) 228 229 _, err := tctx.Selector.SelectReplaceInstances( 230 []placement.Instance{newInstanceWithID(instG3I1), newInstanceWithID(instG3I2)}, 231 []string{tctx.ToReplace.ID()}, 232 tctx.Placement, 233 ) 234 require.EqualError(t, err, newErrNoValidReplacement(instG1I1, "group1").Error()) 235 }) 236 237 t.Run("filters out invalid zone", func(t *testing.T) { 238 tctx := setup(t) 239 240 // sanity check that this is otherwise valid. 241 instG := newInstanceWithID(instG1I3) 242 _, err := tctx.Selector.SelectReplaceInstances( 243 []placement.Instance{instG, newInstanceWithID(instG3I1)}, 244 []string{tctx.ToReplace.ID()}, 245 tctx.Placement, 246 ) 247 require.NoError(t, err) 248 249 tctx.Selector = NewMirroredCustomGroupSelector( 250 tctx.GroupFn, 251 newTestMirroredCustomGroupOptions(). 252 SetValidZone("zone"). 253 SetAllowAllZones(false), 254 ) 255 256 _, err = tctx.Selector.SelectReplaceInstances( 257 []placement.Instance{newInstanceWithID(instG1I3).SetZone("someOtherZone")}, 258 []string{tctx.ToReplace.ID()}, 259 tctx.Placement, 260 ) 261 require.EqualError(t, err, errNoValidCandidateInstance.Error()) 262 }) 263 } 264 265 type mirroredCustomGroupSelectorTestContext struct { 266 Selector placement.InstanceSelector 267 Instances []placement.Instance 268 Placement placement.Placement 269 Groups map[string]string 270 GroupFn InstanceGroupIDFunc 271 } 272 273 func mirroredCustomGroupSelectorSetup(_ *testing.T) *mirroredCustomGroupSelectorTestContext { 274 tctx := &mirroredCustomGroupSelectorTestContext{} 275 276 tctx.Instances = []placement.Instance{ 277 newInstanceWithID(instG1I1), 278 newInstanceWithID(instG2I1), 279 newInstanceWithID(instG1I2), 280 newInstanceWithID(instG2I2), 281 } 282 283 tctx.Groups = testGroups 284 285 tctx.GroupFn = NewMapInstanceGroupIDFunc(tctx.Groups) 286 287 tctx.Selector = NewMirroredCustomGroupSelector( 288 tctx.GroupFn, 289 newTestMirroredCustomGroupOptions(), 290 ) 291 292 tctx.Placement = placement.NewPlacement().SetReplicaFactor(2) 293 return tctx 294 } 295 296 func newInstanceWithID(id string) placement.Instance { 297 return placement.NewInstance().SetID(id) 298 } 299 300 func newTestMirroredCustomGroupOptions() placement.Options { 301 return placement.NewOptions(). 302 SetAllowAllZones(true). 303 SetAddAllCandidates(true). 304 SetInstrumentOptions(instrument.NewOptions().SetLogger(logger)) 305 } 306 307 func assertGroupsCorrect(t *testing.T, expectedGroupIds [][]string, instances []placement.Instance) { 308 instancesByID := make(map[string]placement.Instance, len(instances)) 309 for _, inst := range instances { 310 instancesByID[inst.ID()] = inst 311 } 312 313 groups := make([][]placement.Instance, 0, len(expectedGroupIds)) 314 // convert 315 for _, group := range expectedGroupIds { 316 groupInstances := make([]placement.Instance, 0, len(group)) 317 for _, id := range group { 318 inst, ok := instancesByID[id] 319 if !ok { 320 require.True(t, ok, "instance %s not found", id) 321 } 322 groupInstances = append(groupInstances, inst) 323 } 324 } 325 326 assertShardsetIDsEqual(t, groups) 327 assertShardsetsUnique(t, groups) 328 } 329 330 func assertShardsetIDsEqual( 331 t *testing.T, 332 groups [][]placement.Instance, 333 ) { 334 // check that shardset IDs for each group are the same. 335 for _, instances := range groups { 336 require.True(t, len(instances) >= 1, "group must have at least one instance") 337 groupShardsetID := instances[0].ShardSetID() 338 for _, inst := range instances[1:] { 339 require.Equal(t, groupShardsetID, inst.ShardSetID(), 340 "instance %s has a different shardset (%d) "+ 341 "than other instances in the group (%d)", 342 inst.ID(), inst.ShardSetID(), groupShardsetID) 343 } 344 } 345 } 346 347 func assertShardsetsUnique(t *testing.T, groups [][]placement.Instance) { 348 allShardsets := map[uint32]struct{}{} 349 for _, group := range groups { 350 require.NotEmpty(t, group) 351 352 groupShardsetID := group[0].ShardSetID() 353 _, exists := allShardsets[groupShardsetID] 354 require.False(t, exists, 355 "multiple groups have the same shardset ID %d", groupShardsetID) 356 allShardsets[groupShardsetID] = struct{}{} 357 } 358 }