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  }