github.com/hhrutter/nomad@v0.6.0-rc2.0.20170723054333-80c4b03f0705/scheduler/propertyset.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  
     6  	memdb "github.com/hashicorp/go-memdb"
     7  	"github.com/hashicorp/nomad/nomad/structs"
     8  )
     9  
    10  // propertySet is used to track the values used for a particular property.
    11  type propertySet struct {
    12  	// ctx is used to lookup the plan and state
    13  	ctx Context
    14  
    15  	// jobID is the job we are operating on
    16  	jobID string
    17  
    18  	// taskGroup is optionally set if the constraint is for a task group
    19  	taskGroup string
    20  
    21  	// constraint is the constraint this property set is checking
    22  	constraint *structs.Constraint
    23  
    24  	// errorBuilding marks whether there was an error when building the property
    25  	// set
    26  	errorBuilding error
    27  
    28  	// existingValues is the set of values for the given property that have been
    29  	// used by pre-existing allocations.
    30  	existingValues map[string]struct{}
    31  
    32  	// proposedValues is the set of values for the given property that are used
    33  	// from proposed allocations.
    34  	proposedValues map[string]struct{}
    35  
    36  	// clearedValues is the set of values that are no longer being used by
    37  	// existingValues because of proposed stops.
    38  	clearedValues map[string]struct{}
    39  }
    40  
    41  // NewPropertySet returns a new property set used to guarantee unique property
    42  // values for new allocation placements.
    43  func NewPropertySet(ctx Context, job *structs.Job) *propertySet {
    44  	p := &propertySet{
    45  		ctx:            ctx,
    46  		jobID:          job.ID,
    47  		existingValues: make(map[string]struct{}),
    48  	}
    49  
    50  	return p
    51  }
    52  
    53  // SetJobConstraint is used to parameterize the property set for a
    54  // distinct_property constraint set at the job level.
    55  func (p *propertySet) SetJobConstraint(constraint *structs.Constraint) {
    56  	// Store the constraint
    57  	p.constraint = constraint
    58  	p.populateExisting(constraint)
    59  }
    60  
    61  // SetTGConstraint is used to parameterize the property set for a
    62  // distinct_property constraint set at the task group level. The inputs are the
    63  // constraint and the task group name.
    64  func (p *propertySet) SetTGConstraint(constraint *structs.Constraint, taskGroup string) {
    65  	// Store that this is for a task group
    66  	p.taskGroup = taskGroup
    67  
    68  	// Store the constraint
    69  	p.constraint = constraint
    70  
    71  	p.populateExisting(constraint)
    72  }
    73  
    74  // populateExisting is a helper shared when setting the constraint to populate
    75  // the existing values.
    76  func (p *propertySet) populateExisting(constraint *structs.Constraint) {
    77  	// Retrieve all previously placed allocations
    78  	ws := memdb.NewWatchSet()
    79  	allocs, err := p.ctx.State().AllocsByJob(ws, p.jobID, false)
    80  	if err != nil {
    81  		p.errorBuilding = fmt.Errorf("failed to get job's allocations: %v", err)
    82  		p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", p.errorBuilding)
    83  		return
    84  	}
    85  
    86  	// Filter to the correct set of allocs
    87  	allocs = p.filterAllocs(allocs, true)
    88  
    89  	// Get all the nodes that have been used by the allocs
    90  	nodes, err := p.buildNodeMap(allocs)
    91  	if err != nil {
    92  		p.errorBuilding = err
    93  		p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err)
    94  		return
    95  	}
    96  
    97  	// Build existing properties map
    98  	p.populateProperties(allocs, nodes, p.existingValues)
    99  }
   100  
   101  // PopulateProposed populates the proposed values and recomputes any cleared
   102  // value. It should be called whenever the plan is updated to ensure correct
   103  // results when checking an option.
   104  func (p *propertySet) PopulateProposed() {
   105  
   106  	// Reset the proposed properties
   107  	p.proposedValues = make(map[string]struct{})
   108  	p.clearedValues = make(map[string]struct{})
   109  
   110  	// Gather the set of proposed stops.
   111  	var stopping []*structs.Allocation
   112  	for _, updates := range p.ctx.Plan().NodeUpdate {
   113  		stopping = append(stopping, updates...)
   114  	}
   115  	stopping = p.filterAllocs(stopping, false)
   116  
   117  	// Gather the proposed allocations
   118  	var proposed []*structs.Allocation
   119  	for _, pallocs := range p.ctx.Plan().NodeAllocation {
   120  		proposed = append(proposed, pallocs...)
   121  	}
   122  	proposed = p.filterAllocs(proposed, true)
   123  
   124  	// Get the used nodes
   125  	both := make([]*structs.Allocation, 0, len(stopping)+len(proposed))
   126  	both = append(both, stopping...)
   127  	both = append(both, proposed...)
   128  	nodes, err := p.buildNodeMap(both)
   129  	if err != nil {
   130  		p.errorBuilding = err
   131  		p.ctx.Logger().Printf("[ERR] scheduler.dynamic-constraint: %v", err)
   132  		return
   133  	}
   134  
   135  	// Populate the cleared values
   136  	p.populateProperties(stopping, nodes, p.clearedValues)
   137  
   138  	// Populate the proposed values
   139  	p.populateProperties(proposed, nodes, p.proposedValues)
   140  
   141  	// Remove any cleared value that is now being used by the proposed allocs
   142  	for value := range p.proposedValues {
   143  		delete(p.clearedValues, value)
   144  	}
   145  }
   146  
   147  // SatisfiesDistinctProperties checks if the option satisfies the
   148  // distinct_property constraints given the existing placements and proposed
   149  // placements. If the option does not satisfy the constraints an explanation is
   150  // given.
   151  func (p *propertySet) SatisfiesDistinctProperties(option *structs.Node, tg string) (bool, string) {
   152  	// Check if there was an error building
   153  	if p.errorBuilding != nil {
   154  		return false, p.errorBuilding.Error()
   155  	}
   156  
   157  	// Get the nodes property value
   158  	nValue, ok := getProperty(option, p.constraint.LTarget)
   159  	if !ok {
   160  		return false, fmt.Sprintf("missing property %q", p.constraint.LTarget)
   161  	}
   162  
   163  	// both is used to iterate over both the proposed and existing used
   164  	// properties
   165  	bothAll := []map[string]struct{}{p.existingValues, p.proposedValues}
   166  
   167  	// Check if the nodes value has already been used.
   168  	for _, usedProperties := range bothAll {
   169  		// Check if the nodes value has been used
   170  		_, used := usedProperties[nValue]
   171  		if !used {
   172  			continue
   173  		}
   174  
   175  		// Check if the value has been cleared from a proposed stop
   176  		if _, cleared := p.clearedValues[nValue]; cleared {
   177  			continue
   178  		}
   179  
   180  		return false, fmt.Sprintf("distinct_property: %s=%s already used", p.constraint.LTarget, nValue)
   181  	}
   182  
   183  	return true, ""
   184  }
   185  
   186  // filterAllocs filters a set of allocations to just be those that are running
   187  // and if the property set is operation at a task group level, for allocations
   188  // for that task group
   189  func (p *propertySet) filterAllocs(allocs []*structs.Allocation, filterTerminal bool) []*structs.Allocation {
   190  	n := len(allocs)
   191  	for i := 0; i < n; i++ {
   192  		remove := false
   193  		if filterTerminal {
   194  			remove = allocs[i].TerminalStatus()
   195  		}
   196  
   197  		// If the constraint is on the task group filter the allocations to just
   198  		// those on the task group
   199  		if p.taskGroup != "" {
   200  			remove = remove || allocs[i].TaskGroup != p.taskGroup
   201  		}
   202  
   203  		if remove {
   204  			allocs[i], allocs[n-1] = allocs[n-1], nil
   205  			i--
   206  			n--
   207  		}
   208  	}
   209  	return allocs[:n]
   210  }
   211  
   212  // buildNodeMap takes a list of allocations and returns a map of the nodes used
   213  // by those allocations
   214  func (p *propertySet) buildNodeMap(allocs []*structs.Allocation) (map[string]*structs.Node, error) {
   215  	// Get all the nodes that have been used by the allocs
   216  	nodes := make(map[string]*structs.Node)
   217  	ws := memdb.NewWatchSet()
   218  	for _, alloc := range allocs {
   219  		if _, ok := nodes[alloc.NodeID]; ok {
   220  			continue
   221  		}
   222  
   223  		node, err := p.ctx.State().NodeByID(ws, alloc.NodeID)
   224  		if err != nil {
   225  			return nil, fmt.Errorf("failed to lookup node ID %q: %v", alloc.NodeID, err)
   226  		}
   227  
   228  		nodes[alloc.NodeID] = node
   229  	}
   230  
   231  	return nodes, nil
   232  }
   233  
   234  // populateProperties goes through all allocations and builds up the used
   235  // properties from the nodes storing the results in the passed properties map.
   236  func (p *propertySet) populateProperties(allocs []*structs.Allocation, nodes map[string]*structs.Node,
   237  	properties map[string]struct{}) {
   238  
   239  	for _, alloc := range allocs {
   240  		nProperty, ok := getProperty(nodes[alloc.NodeID], p.constraint.LTarget)
   241  		if !ok {
   242  			continue
   243  		}
   244  
   245  		properties[nProperty] = struct{}{}
   246  	}
   247  }
   248  
   249  // getProperty is used to lookup the property value on the node
   250  func getProperty(n *structs.Node, property string) (string, bool) {
   251  	if n == nil || property == "" {
   252  		return "", false
   253  	}
   254  
   255  	val, ok := resolveConstraintTarget(property, n)
   256  	if !ok {
   257  		return "", false
   258  	}
   259  	nodeValue, ok := val.(string)
   260  	if !ok {
   261  		return "", false
   262  	}
   263  
   264  	return nodeValue, true
   265  }