github.com/m3db/m3@v1.5.0/src/cluster/placement/selector/mirrored_custom_groups.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  	"fmt"
    25  
    26  	"github.com/m3db/m3/src/cluster/placement"
    27  	"github.com/m3db/m3/src/x/errors"
    28  
    29  	"go.uber.org/zap"
    30  )
    31  
    32  type mirroredCustomGroupSelector struct {
    33  	instanceIDToGroupID InstanceGroupIDFunc
    34  	logger              *zap.Logger
    35  	opts            placement.Options
    36  }
    37  
    38  // InstanceGroupIDFunc maps an instance to its mirrored group.
    39  type InstanceGroupIDFunc func(inst placement.Instance) (string, error)
    40  
    41  // NewMapInstanceGroupIDFunc creates a simple lookup function for an instances group, which
    42  // looks up the group ID for an instance by the instance ID.
    43  func NewMapInstanceGroupIDFunc(instanceToGroup map[string]string) InstanceGroupIDFunc {
    44  	return func(inst placement.Instance) (string, error) {
    45  		gid, ok := instanceToGroup[inst.ID()]
    46  		if !ok {
    47  			return "", fmt.Errorf(
    48  				"instance %s doesn't have a corresponding group in ID to group map",
    49  				inst.ID(),
    50  			)
    51  		}
    52  		return gid, nil
    53  	}
    54  }
    55  
    56  // NewMirroredCustomGroupSelector constructs a placement.InstanceSelector which assigns shardsets
    57  // according to their group ID (provided by instanceToGroupID). That is, instances with the
    58  // same group ID are assigned the same shardset ID, and will receive the same mirrored traffic.
    59  func NewMirroredCustomGroupSelector(
    60  	instanceToGroupID InstanceGroupIDFunc,
    61  	opts placement.Options,
    62  ) placement.InstanceSelector {
    63  	return &mirroredCustomGroupSelector{
    64  		logger:              opts.InstrumentOptions().Logger(),
    65  		instanceIDToGroupID: instanceToGroupID,
    66  		opts:                opts,
    67  	}
    68  }
    69  
    70  func (e *mirroredCustomGroupSelector) SelectInitialInstances(
    71  	candidates []placement.Instance,
    72  	rf int,
    73  ) ([]placement.Instance, error) {
    74  	return e.selectInstances(
    75  		candidates,
    76  		placement.NewPlacement().SetReplicaFactor(rf),
    77  		true,
    78  	)
    79  }
    80  
    81  func (e *mirroredCustomGroupSelector) SelectAddingInstances(
    82  	candidates []placement.Instance,
    83  	p placement.Placement,
    84  ) ([]placement.Instance, error) {
    85  	return e.selectInstances(candidates, p, e.opts.AddAllCandidates())
    86  }
    87  
    88  // SelectReplaceInstances attempts to find a replacement instance in the same group
    89  // for each of the leavingInstances
    90  func (e *mirroredCustomGroupSelector) SelectReplaceInstances(
    91  	candidates []placement.Instance,
    92  	leavingInstanceIDs []string,
    93  	p placement.Placement,
    94  ) ([]placement.Instance, error) {
    95  	candidates, err := getValidCandidates(p, candidates, e.opts)
    96  	if err != nil {
    97  	    return nil, err
    98  	}
    99  
   100  	// find a replacement for each leaving instance.
   101  	candidatesByGroup, err := e.groupInstancesByID(candidates)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	leavingInstances, err := getLeavingInstances(p, leavingInstanceIDs)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  
   111  	replacementGroups := make([]mirroredReplacementGroup, 0, len(leavingInstances))
   112  	for _, leavingInstance := range leavingInstances {
   113  		// try to find an instance in the same group as the leaving instance.
   114  		groupID, err := e.getGroup(leavingInstance)
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  
   119  		replacementGroup := candidatesByGroup[groupID]
   120  		if len(replacementGroup) == 0 {
   121  			return nil, newErrNoValidReplacement(leavingInstance.ID(), groupID)
   122  		}
   123  
   124  		replacementNode := replacementGroup[len(replacementGroup)-1]
   125  		candidatesByGroup[groupID] = replacementGroup[:len(replacementGroup)-1]
   126  
   127  		replacementGroups = append(
   128  			replacementGroups,
   129  			mirroredReplacementGroup{
   130  				Leaving:     leavingInstance,
   131  				Replacement: replacementNode,
   132  			})
   133  	}
   134  
   135  	return assignShardsetIDsToReplacements(leavingInstanceIDs, replacementGroups)
   136  }
   137  
   138  type mirroredReplacementGroup struct {
   139  	Leaving     placement.Instance
   140  	Replacement placement.Instance
   141  }
   142  
   143  // selectInstances does the actual work of the class. It groups candidate instances by their
   144  // group ID, and assigns them shardsets.
   145  // N.B. (amains): addAllInstances is a parameter here (instead of using e.opts) because it
   146  // only applies to the SelectAddingInstances case.
   147  func (e *mirroredCustomGroupSelector) selectInstances(
   148  	candidates []placement.Instance,
   149  	p placement.Placement,
   150  	addAllInstances bool,
   151  ) ([]placement.Instance, error) {
   152  	candidates, err := getValidCandidates(p, candidates, e.opts)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	groups, err := e.groupWithRF(candidates, p.ReplicaFactor())
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  
   162  	// no groups => no instances
   163  	if len(groups) == 0 {
   164  		return nil, nil
   165  	}
   166  
   167  	if !addAllInstances {
   168  		groups = groups[:1]
   169  	}
   170  	return assignShardsetsToGroupedInstances(groups, p), nil
   171  }
   172  
   173  func (e *mirroredCustomGroupSelector) groupWithRF(
   174  	candidates []placement.Instance,
   175  	rf int) ([][]placement.Instance, error) {
   176  	byGroupID, err := e.groupInstancesByID(candidates)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	groups := make([][]placement.Instance, 0, len(byGroupID))
   182  	// validate and convert to slice
   183  	for groupID, group := range byGroupID {
   184  		if len(group) > rf {
   185  			fullGroup := group
   186  			group = group[:rf]
   187  
   188  			var droppedIDs []string
   189  			for _, dropped := range fullGroup[rf:] {
   190  				droppedIDs = append(droppedIDs, dropped.ID())
   191  			}
   192  			e.logger.Warn(
   193  				"mirroredCustomGroupSelector: found more hosts than RF in group; "+
   194  					"using only RF hosts",
   195  				zap.Strings("droppedIDs", droppedIDs),
   196  				zap.String("groupID", groupID),
   197  			)
   198  		}
   199  		groups = append(groups, group)
   200  	}
   201  	return groups, nil
   202  }
   203  
   204  func (e *mirroredCustomGroupSelector) groupInstancesByID(candidates []placement.Instance) (map[string][]placement.Instance, error) {
   205  	byGroupID := make(map[string][]placement.Instance)
   206  	for _, candidate := range candidates {
   207  		groupID, err := e.getGroup(candidate)
   208  		if err != nil {
   209  			return nil, err
   210  		}
   211  
   212  		byGroupID[groupID] = append(byGroupID[groupID], candidate)
   213  	}
   214  	return byGroupID, nil
   215  }
   216  
   217  // small wrapper around e.instanceIDToGroupID providing context on error.
   218  func (e *mirroredCustomGroupSelector) getGroup(inst placement.Instance) (string, error) {
   219  	groupID, err := e.instanceIDToGroupID(inst)
   220  	if err != nil {
   221  		return "", errors.Wrapf(err, "finding group for %s", inst.ID())
   222  	}
   223  	return groupID, nil
   224  }
   225  
   226  func newErrNoValidReplacement(leavingInstID string, groupID string) error {
   227  	return fmt.Errorf(
   228  		"leaving instance %s has no valid replacements in the same group (%s)",
   229  		leavingInstID,
   230  		groupID,
   231  	)
   232  }