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

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