github.com/zoomfoo/nomad@v0.8.5-0.20180907175415-f28fd3a1a056/scheduler/propertyset.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  	"strconv"
     6  
     7  	memdb "github.com/hashicorp/go-memdb"
     8  	"github.com/hashicorp/nomad/helper"
     9  	"github.com/hashicorp/nomad/nomad/structs"
    10  )
    11  
    12  // propertySet is used to track the values used for a particular property.
    13  type propertySet struct {
    14  	// ctx is used to lookup the plan and state
    15  	ctx Context
    16  
    17  	// jobID is the job we are operating on
    18  	jobID string
    19  
    20  	// namespace is the namespace of the job we are operating on
    21  	namespace string
    22  
    23  	// taskGroup is optionally set if the constraint is for a task group
    24  	taskGroup string
    25  
    26  	// targetAttribute is the attribute this property set is checking
    27  	targetAttribute string
    28  
    29  	// allowedCount is the allowed number of allocations that can have the
    30  	// distinct property
    31  	allowedCount uint64
    32  
    33  	// errorBuilding marks whether there was an error when building the property
    34  	// set
    35  	errorBuilding error
    36  
    37  	// existingValues is a mapping of the values of a property to the number of
    38  	// times the value has been used by pre-existing allocations.
    39  	existingValues map[string]uint64
    40  
    41  	// proposedValues is a mapping of the values of a property to the number of
    42  	// times the value has been used by proposed allocations.
    43  	proposedValues map[string]uint64
    44  
    45  	// clearedValues is a mapping of the values of a property to the number of
    46  	// times the value has been used by proposed stopped allocations.
    47  	clearedValues map[string]uint64
    48  }
    49  
    50  // NewPropertySet returns a new property set used to guarantee unique property
    51  // values for new allocation placements.
    52  func NewPropertySet(ctx Context, job *structs.Job) *propertySet {
    53  	p := &propertySet{
    54  		ctx:            ctx,
    55  		jobID:          job.ID,
    56  		namespace:      job.Namespace,
    57  		existingValues: make(map[string]uint64),
    58  	}
    59  
    60  	return p
    61  }
    62  
    63  // SetJobConstraint is used to parameterize the property set for a
    64  // distinct_property constraint set at the job level.
    65  func (p *propertySet) SetJobConstraint(constraint *structs.Constraint) {
    66  	p.setConstraint(constraint, "")
    67  }
    68  
    69  // SetTGConstraint is used to parameterize the property set for a
    70  // distinct_property constraint set at the task group level. The inputs are the
    71  // constraint and the task group name.
    72  func (p *propertySet) SetTGConstraint(constraint *structs.Constraint, taskGroup string) {
    73  	p.setConstraint(constraint, taskGroup)
    74  }
    75  
    76  // setConstraint is a shared helper for setting a job or task group constraint.
    77  func (p *propertySet) setConstraint(constraint *structs.Constraint, taskGroup string) {
    78  	var allowedCount uint64
    79  	// Determine the number of allowed allocations with the property.
    80  	if v := constraint.RTarget; v != "" {
    81  		c, err := strconv.ParseUint(v, 10, 64)
    82  		if err != nil {
    83  			p.errorBuilding = fmt.Errorf("failed to convert RTarget %q to uint64: %v", v, err)
    84  			p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", p.errorBuilding)
    85  			return
    86  		}
    87  
    88  		allowedCount = c
    89  	} else {
    90  		allowedCount = 1
    91  	}
    92  	p.setTargetAttributeWithCount(constraint.LTarget, allowedCount, taskGroup)
    93  }
    94  
    95  // SetTargetAttribute is used to populate this property set without also storing allowed count
    96  // This is used when evaluating spread stanzas
    97  func (p *propertySet) SetTargetAttribute(targetAttribute string, taskGroup string) {
    98  	p.setTargetAttributeWithCount(targetAttribute, 0, taskGroup)
    99  }
   100  
   101  // setTargetAttributeWithCount is a shared helper for setting a job or task group attribute and allowedCount
   102  // allowedCount can be zero when this is used in evaluating spread stanzas
   103  func (p *propertySet) setTargetAttributeWithCount(targetAttribute string, allowedCount uint64, taskGroup string) {
   104  	// Store that this is for a task group
   105  	if taskGroup != "" {
   106  		p.taskGroup = taskGroup
   107  	}
   108  
   109  	// Store the constraint
   110  	p.targetAttribute = targetAttribute
   111  
   112  	p.allowedCount = allowedCount
   113  
   114  	// Determine the number of existing allocations that are using a property
   115  	// value
   116  	p.populateExisting()
   117  
   118  	// Populate the proposed when setting the constraint. We do this because
   119  	// when detecting if we can inplace update an allocation we stage an
   120  	// eviction and then select. This means the plan has an eviction before a
   121  	// single select has finished.
   122  	p.PopulateProposed()
   123  }
   124  
   125  // populateExisting is a helper shared when setting the constraint to populate
   126  // the existing values.
   127  func (p *propertySet) populateExisting() {
   128  	// Retrieve all previously placed allocations
   129  	ws := memdb.NewWatchSet()
   130  	allocs, err := p.ctx.State().AllocsByJob(ws, p.namespace, p.jobID, false)
   131  	if err != nil {
   132  		p.errorBuilding = fmt.Errorf("failed to get job's allocations: %v", err)
   133  		p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", p.errorBuilding)
   134  		return
   135  	}
   136  
   137  	// Filter to the correct set of allocs
   138  	allocs = p.filterAllocs(allocs, true)
   139  
   140  	// Get all the nodes that have been used by the allocs
   141  	nodes, err := p.buildNodeMap(allocs)
   142  	if err != nil {
   143  		p.errorBuilding = err
   144  		p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err)
   145  		return
   146  	}
   147  
   148  	// Build existing properties map
   149  	p.populateProperties(allocs, nodes, p.existingValues)
   150  }
   151  
   152  // PopulateProposed populates the proposed values and recomputes any cleared
   153  // value. It should be called whenever the plan is updated to ensure correct
   154  // results when checking an option.
   155  func (p *propertySet) PopulateProposed() {
   156  
   157  	// Reset the proposed properties
   158  	p.proposedValues = make(map[string]uint64)
   159  	p.clearedValues = make(map[string]uint64)
   160  
   161  	// Gather the set of proposed stops.
   162  	var stopping []*structs.Allocation
   163  	for _, updates := range p.ctx.Plan().NodeUpdate {
   164  		stopping = append(stopping, updates...)
   165  	}
   166  	stopping = p.filterAllocs(stopping, false)
   167  
   168  	// Gather the proposed allocations
   169  	var proposed []*structs.Allocation
   170  	for _, pallocs := range p.ctx.Plan().NodeAllocation {
   171  		proposed = append(proposed, pallocs...)
   172  	}
   173  	proposed = p.filterAllocs(proposed, true)
   174  
   175  	// Get the used nodes
   176  	both := make([]*structs.Allocation, 0, len(stopping)+len(proposed))
   177  	both = append(both, stopping...)
   178  	both = append(both, proposed...)
   179  	nodes, err := p.buildNodeMap(both)
   180  	if err != nil {
   181  		p.errorBuilding = err
   182  		p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err)
   183  		return
   184  	}
   185  
   186  	// Populate the cleared values
   187  	p.populateProperties(stopping, nodes, p.clearedValues)
   188  
   189  	// Populate the proposed values
   190  	p.populateProperties(proposed, nodes, p.proposedValues)
   191  
   192  	// Remove any cleared value that is now being used by the proposed allocs
   193  	for value := range p.proposedValues {
   194  		current, ok := p.clearedValues[value]
   195  		if !ok {
   196  			continue
   197  		} else if current == 0 {
   198  			delete(p.clearedValues, value)
   199  		} else if current > 1 {
   200  			p.clearedValues[value]--
   201  		}
   202  	}
   203  }
   204  
   205  // SatisfiesDistinctProperties checks if the option satisfies the
   206  // distinct_property constraints given the existing placements and proposed
   207  // placements. If the option does not satisfy the constraints an explanation is
   208  // given.
   209  func (p *propertySet) SatisfiesDistinctProperties(option *structs.Node, tg string) (bool, string) {
   210  	nValue, errorMsg, usedCount := p.UsedCount(option, tg)
   211  	if errorMsg != "" {
   212  		return false, errorMsg
   213  	}
   214  	// The property value has been used but within the number of allowed
   215  	// allocations.
   216  	if usedCount < p.allowedCount {
   217  		return true, ""
   218  	}
   219  
   220  	return false, fmt.Sprintf("distinct_property: %s=%s used by %d allocs", p.targetAttribute, nValue, usedCount)
   221  }
   222  
   223  // UsedCount returns the number of times the value of the attribute being tracked by this
   224  // property set is used across current and proposed allocations. It also returns the resolved
   225  // attribute value for the node, and an error message if it couldn't be resolved correctly
   226  func (p *propertySet) UsedCount(option *structs.Node, tg string) (string, string, uint64) {
   227  	// Check if there was an error building
   228  	if p.errorBuilding != nil {
   229  		return "", p.errorBuilding.Error(), 0
   230  	}
   231  
   232  	// Get the nodes property value
   233  	nValue, ok := getProperty(option, p.targetAttribute)
   234  	if !ok {
   235  		return nValue, fmt.Sprintf("missing property %q", p.targetAttribute), 0
   236  	}
   237  	combinedUse := p.GetCombinedUseMap()
   238  	usedCount := combinedUse[nValue]
   239  	return nValue, "", usedCount
   240  }
   241  
   242  // GetCombinedUseMap counts how many times the property has been used by
   243  // existing and proposed allocations. It also takes into account any stopped
   244  // allocations
   245  func (p *propertySet) GetCombinedUseMap() map[string]uint64 {
   246  	combinedUse := make(map[string]uint64, helper.IntMax(len(p.existingValues), len(p.proposedValues)))
   247  	for _, usedValues := range []map[string]uint64{p.existingValues, p.proposedValues} {
   248  		for propertyValue, usedCount := range usedValues {
   249  			combinedUse[propertyValue] += usedCount
   250  		}
   251  	}
   252  
   253  	// Go through and discount the combined count when the value has been
   254  	// cleared by a proposed stop.
   255  	for propertyValue, clearedCount := range p.clearedValues {
   256  		combined, ok := combinedUse[propertyValue]
   257  		if !ok {
   258  			continue
   259  		}
   260  
   261  		// Don't clear below 0.
   262  		if combined >= clearedCount {
   263  			combinedUse[propertyValue] = combined - clearedCount
   264  		} else {
   265  			combinedUse[propertyValue] = 0
   266  		}
   267  	}
   268  	return combinedUse
   269  }
   270  
   271  // filterAllocs filters a set of allocations to just be those that are running
   272  // and if the property set is operation at a task group level, for allocations
   273  // for that task group
   274  func (p *propertySet) filterAllocs(allocs []*structs.Allocation, filterTerminal bool) []*structs.Allocation {
   275  	n := len(allocs)
   276  	for i := 0; i < n; i++ {
   277  		remove := false
   278  		if filterTerminal {
   279  			remove = allocs[i].TerminalStatus()
   280  		}
   281  
   282  		// If the constraint is on the task group filter the allocations to just
   283  		// those on the task group
   284  		if p.taskGroup != "" {
   285  			remove = remove || allocs[i].TaskGroup != p.taskGroup
   286  		}
   287  
   288  		if remove {
   289  			allocs[i], allocs[n-1] = allocs[n-1], nil
   290  			i--
   291  			n--
   292  		}
   293  	}
   294  	return allocs[:n]
   295  }
   296  
   297  // buildNodeMap takes a list of allocations and returns a map of the nodes used
   298  // by those allocations
   299  func (p *propertySet) buildNodeMap(allocs []*structs.Allocation) (map[string]*structs.Node, error) {
   300  	// Get all the nodes that have been used by the allocs
   301  	nodes := make(map[string]*structs.Node)
   302  	ws := memdb.NewWatchSet()
   303  	for _, alloc := range allocs {
   304  		if _, ok := nodes[alloc.NodeID]; ok {
   305  			continue
   306  		}
   307  
   308  		node, err := p.ctx.State().NodeByID(ws, alloc.NodeID)
   309  		if err != nil {
   310  			return nil, fmt.Errorf("failed to lookup node ID %q: %v", alloc.NodeID, err)
   311  		}
   312  
   313  		nodes[alloc.NodeID] = node
   314  	}
   315  
   316  	return nodes, nil
   317  }
   318  
   319  // populateProperties goes through all allocations and builds up the used
   320  // properties from the nodes storing the results in the passed properties map.
   321  func (p *propertySet) populateProperties(allocs []*structs.Allocation, nodes map[string]*structs.Node,
   322  	properties map[string]uint64) {
   323  
   324  	for _, alloc := range allocs {
   325  		nProperty, ok := getProperty(nodes[alloc.NodeID], p.targetAttribute)
   326  		if !ok {
   327  			continue
   328  		}
   329  
   330  		properties[nProperty]++
   331  	}
   332  }
   333  
   334  // getProperty is used to lookup the property value on the node
   335  func getProperty(n *structs.Node, property string) (string, bool) {
   336  	if n == nil || property == "" {
   337  		return "", false
   338  	}
   339  
   340  	val, ok := resolveTarget(property, n)
   341  	if !ok {
   342  		return "", false
   343  	}
   344  	nodeValue, ok := val.(string)
   345  	if !ok {
   346  		return "", false
   347  	}
   348  
   349  	return nodeValue, true
   350  }