github.com/maier/nomad@v0.4.1-0.20161110003312-a9e3d0b8549d/scheduler/context.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"log"
     5  	"regexp"
     6  
     7  	"github.com/hashicorp/go-version"
     8  	"github.com/hashicorp/nomad/nomad/structs"
     9  )
    10  
    11  // Context is used to track contextual information used for placement
    12  type Context interface {
    13  	// State is used to inspect the current global state
    14  	State() State
    15  
    16  	// Plan returns the current plan
    17  	Plan() *structs.Plan
    18  
    19  	// Logger provides a way to log
    20  	Logger() *log.Logger
    21  
    22  	// Metrics returns the current metrics
    23  	Metrics() *structs.AllocMetric
    24  
    25  	// Reset is invoked after making a placement
    26  	Reset()
    27  
    28  	// ProposedAllocs returns the proposed allocations for a node
    29  	// which is the existing allocations, removing evictions, and
    30  	// adding any planned placements.
    31  	ProposedAllocs(nodeID string) ([]*structs.Allocation, error)
    32  
    33  	// RegexpCache is a cache of regular expressions
    34  	RegexpCache() map[string]*regexp.Regexp
    35  
    36  	// ConstraintCache is a cache of version constraints
    37  	ConstraintCache() map[string]version.Constraints
    38  
    39  	// Eligibility returns a tracker for node eligibility in the context of the
    40  	// eval.
    41  	Eligibility() *EvalEligibility
    42  }
    43  
    44  // EvalCache is used to cache certain things during an evaluation
    45  type EvalCache struct {
    46  	reCache         map[string]*regexp.Regexp
    47  	constraintCache map[string]version.Constraints
    48  }
    49  
    50  func (e *EvalCache) RegexpCache() map[string]*regexp.Regexp {
    51  	if e.reCache == nil {
    52  		e.reCache = make(map[string]*regexp.Regexp)
    53  	}
    54  	return e.reCache
    55  }
    56  func (e *EvalCache) ConstraintCache() map[string]version.Constraints {
    57  	if e.constraintCache == nil {
    58  		e.constraintCache = make(map[string]version.Constraints)
    59  	}
    60  	return e.constraintCache
    61  }
    62  
    63  // EvalContext is a Context used during an Evaluation
    64  type EvalContext struct {
    65  	EvalCache
    66  	state       State
    67  	plan        *structs.Plan
    68  	logger      *log.Logger
    69  	metrics     *structs.AllocMetric
    70  	eligibility *EvalEligibility
    71  }
    72  
    73  // NewEvalContext constructs a new EvalContext
    74  func NewEvalContext(s State, p *structs.Plan, log *log.Logger) *EvalContext {
    75  	ctx := &EvalContext{
    76  		state:   s,
    77  		plan:    p,
    78  		logger:  log,
    79  		metrics: new(structs.AllocMetric),
    80  	}
    81  	return ctx
    82  }
    83  
    84  func (e *EvalContext) State() State {
    85  	return e.state
    86  }
    87  
    88  func (e *EvalContext) Plan() *structs.Plan {
    89  	return e.plan
    90  }
    91  
    92  func (e *EvalContext) Logger() *log.Logger {
    93  	return e.logger
    94  }
    95  
    96  func (e *EvalContext) Metrics() *structs.AllocMetric {
    97  	return e.metrics
    98  }
    99  
   100  func (e *EvalContext) SetState(s State) {
   101  	e.state = s
   102  }
   103  
   104  func (e *EvalContext) Reset() {
   105  	e.metrics = new(structs.AllocMetric)
   106  }
   107  
   108  func (e *EvalContext) ProposedAllocs(nodeID string) ([]*structs.Allocation, error) {
   109  	// Get the existing allocations that are non-terminal
   110  	existingAlloc, err := e.state.AllocsByNodeTerminal(nodeID, false)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	// Determine the proposed allocation by first removing allocations
   116  	// that are planned evictions and adding the new allocations.
   117  	proposed := existingAlloc
   118  	if update := e.plan.NodeUpdate[nodeID]; len(update) > 0 {
   119  		proposed = structs.RemoveAllocs(existingAlloc, update)
   120  	}
   121  
   122  	// We create an index of the existing allocations so that if an inplace
   123  	// update occurs, we do not double count and we override the old allocation.
   124  	proposedIDs := make(map[string]*structs.Allocation, len(proposed))
   125  	for _, alloc := range proposed {
   126  		proposedIDs[alloc.ID] = alloc
   127  	}
   128  	for _, alloc := range e.plan.NodeAllocation[nodeID] {
   129  		proposedIDs[alloc.ID] = alloc
   130  	}
   131  
   132  	// Materialize the proposed slice
   133  	proposed = make([]*structs.Allocation, 0, len(proposedIDs))
   134  	for _, alloc := range proposedIDs {
   135  		proposed = append(proposed, alloc)
   136  	}
   137  
   138  	return proposed, nil
   139  }
   140  
   141  func (e *EvalContext) Eligibility() *EvalEligibility {
   142  	if e.eligibility == nil {
   143  		e.eligibility = NewEvalEligibility()
   144  	}
   145  
   146  	return e.eligibility
   147  }
   148  
   149  type ComputedClassFeasibility byte
   150  
   151  const (
   152  	// EvalComputedClassUnknown is the initial state until the eligibility has
   153  	// been explicitly marked to eligible/ineligible or escaped.
   154  	EvalComputedClassUnknown ComputedClassFeasibility = iota
   155  
   156  	// EvalComputedClassIneligible is used to mark the computed class as
   157  	// ineligible for the evaluation.
   158  	EvalComputedClassIneligible
   159  
   160  	// EvalComputedClassIneligible is used to mark the computed class as
   161  	// eligible for the evaluation.
   162  	EvalComputedClassEligible
   163  
   164  	// EvalComputedClassEscaped signals that computed class can not determine
   165  	// eligibility because a constraint exists that is not captured by computed
   166  	// node classes.
   167  	EvalComputedClassEscaped
   168  )
   169  
   170  // EvalEligibility tracks eligibility of nodes by computed node class over the
   171  // course of an evaluation.
   172  type EvalEligibility struct {
   173  	// job tracks the eligibility at the job level per computed node class.
   174  	job map[string]ComputedClassFeasibility
   175  
   176  	// jobEscaped marks whether constraints have escaped at the job level.
   177  	jobEscaped bool
   178  
   179  	// taskGroups tracks the eligibility at the task group level per computed
   180  	// node class.
   181  	taskGroups map[string]map[string]ComputedClassFeasibility
   182  
   183  	// tgEscapedConstraints is a map of task groups to whether constraints have
   184  	// escaped.
   185  	tgEscapedConstraints map[string]bool
   186  }
   187  
   188  // NewEvalEligibility returns an eligibility tracker for the context of an evaluation.
   189  func NewEvalEligibility() *EvalEligibility {
   190  	return &EvalEligibility{
   191  		job:                  make(map[string]ComputedClassFeasibility),
   192  		taskGroups:           make(map[string]map[string]ComputedClassFeasibility),
   193  		tgEscapedConstraints: make(map[string]bool),
   194  	}
   195  }
   196  
   197  // SetJob takes the job being evaluated and calculates the escaped constraints
   198  // at the job and task group level.
   199  func (e *EvalEligibility) SetJob(job *structs.Job) {
   200  	// Determine whether the job has escaped constraints.
   201  	e.jobEscaped = len(structs.EscapedConstraints(job.Constraints)) != 0
   202  
   203  	// Determine the escaped constraints per task group.
   204  	for _, tg := range job.TaskGroups {
   205  		constraints := tg.Constraints
   206  		for _, task := range tg.Tasks {
   207  			constraints = append(constraints, task.Constraints...)
   208  		}
   209  
   210  		e.tgEscapedConstraints[tg.Name] = len(structs.EscapedConstraints(constraints)) != 0
   211  	}
   212  }
   213  
   214  // HasEscaped returns whether any of the constraints in the passed job have
   215  // escaped computed node classes.
   216  func (e *EvalEligibility) HasEscaped() bool {
   217  	if e.jobEscaped {
   218  		return true
   219  	}
   220  
   221  	for _, escaped := range e.tgEscapedConstraints {
   222  		if escaped {
   223  			return true
   224  		}
   225  	}
   226  
   227  	return false
   228  }
   229  
   230  // GetClasses returns the tracked classes to their eligibility, across the job
   231  // and task groups.
   232  func (e *EvalEligibility) GetClasses() map[string]bool {
   233  	elig := make(map[string]bool)
   234  
   235  	// Go through the job.
   236  	for class, feas := range e.job {
   237  		switch feas {
   238  		case EvalComputedClassEligible:
   239  			elig[class] = true
   240  		case EvalComputedClassIneligible:
   241  			elig[class] = false
   242  		}
   243  	}
   244  
   245  	// Go through the task groups.
   246  	for _, classes := range e.taskGroups {
   247  		for class, feas := range classes {
   248  			switch feas {
   249  			case EvalComputedClassEligible:
   250  				elig[class] = true
   251  			case EvalComputedClassIneligible:
   252  				// Only mark as ineligible if it hasn't been marked before. This
   253  				// prevents one task group marking a class as ineligible when it
   254  				// is eligible on another task group.
   255  				if _, ok := elig[class]; !ok {
   256  					elig[class] = false
   257  				}
   258  			}
   259  		}
   260  	}
   261  
   262  	return elig
   263  }
   264  
   265  // JobStatus returns the eligibility status of the job.
   266  func (e *EvalEligibility) JobStatus(class string) ComputedClassFeasibility {
   267  	// COMPAT: Computed node class was introduced in 0.3. Clients running < 0.3
   268  	// will not have a computed class. The safest value to return is the escaped
   269  	// case, since it disables any optimization.
   270  	if e.jobEscaped || class == "" {
   271  		return EvalComputedClassEscaped
   272  	}
   273  
   274  	if status, ok := e.job[class]; ok {
   275  		return status
   276  	}
   277  	return EvalComputedClassUnknown
   278  }
   279  
   280  // SetJobEligibility sets the eligibility status of the job for the computed
   281  // node class.
   282  func (e *EvalEligibility) SetJobEligibility(eligible bool, class string) {
   283  	if eligible {
   284  		e.job[class] = EvalComputedClassEligible
   285  	} else {
   286  		e.job[class] = EvalComputedClassIneligible
   287  	}
   288  }
   289  
   290  // TaskGroupStatus returns the eligibility status of the task group.
   291  func (e *EvalEligibility) TaskGroupStatus(tg, class string) ComputedClassFeasibility {
   292  	// COMPAT: Computed node class was introduced in 0.3. Clients running < 0.3
   293  	// will not have a computed class. The safest value to return is the escaped
   294  	// case, since it disables any optimization.
   295  	if class == "" {
   296  		return EvalComputedClassEscaped
   297  	}
   298  
   299  	if escaped, ok := e.tgEscapedConstraints[tg]; ok {
   300  		if escaped {
   301  			return EvalComputedClassEscaped
   302  		}
   303  	}
   304  
   305  	if classes, ok := e.taskGroups[tg]; ok {
   306  		if status, ok := classes[class]; ok {
   307  			return status
   308  		}
   309  	}
   310  	return EvalComputedClassUnknown
   311  }
   312  
   313  // SetTaskGroupEligibility sets the eligibility status of the task group for the
   314  // computed node class.
   315  func (e *EvalEligibility) SetTaskGroupEligibility(eligible bool, tg, class string) {
   316  	var eligibility ComputedClassFeasibility
   317  	if eligible {
   318  		eligibility = EvalComputedClassEligible
   319  	} else {
   320  		eligibility = EvalComputedClassIneligible
   321  	}
   322  
   323  	if classes, ok := e.taskGroups[tg]; ok {
   324  		classes[class] = eligibility
   325  	} else {
   326  		e.taskGroups[tg] = map[string]ComputedClassFeasibility{class: eligibility}
   327  	}
   328  }