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