github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/scheduler/task_prioritizer.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/evergreen-ci/evergreen"
     8  	"github.com/evergreen-ci/evergreen/model/task"
     9  	"github.com/mongodb/grip"
    10  	"github.com/pkg/errors"
    11  )
    12  
    13  // TaskPrioritizer is responsible for taking in a slice of tasks, and ordering them
    14  // according to which should be run first.
    15  type TaskPrioritizer interface {
    16  	// Takes in a slice of tasks and the current MCI settings.
    17  	// Returns the slice of tasks, sorted in the order in which they should
    18  	// be run, as well as an error if appropriate.
    19  	PrioritizeTasks(settings *evergreen.Settings, tasks []task.Task) (
    20  		[]task.Task, error)
    21  }
    22  
    23  // CmpBasedTaskComparator runs the tasks through a slice of comparator functions
    24  // determining which is more important.
    25  type CmpBasedTaskComparator struct {
    26  	tasks          []task.Task
    27  	errsDuringSort []error
    28  	setupFuncs     []sortSetupFunc
    29  	comparators    []taskPriorityCmp
    30  
    31  	// caches for sorting
    32  	previousTasksCache map[string]task.Task
    33  
    34  	// cache the number of tasks that have failed in other buildvariants; tasks
    35  	// with the same revision, project, display name and requester
    36  	similarFailingCount map[string]int
    37  }
    38  
    39  // CmpBasedTaskQueues represents the three types of queues that are created for merging together into one queue.
    40  // The HighPriorityTasks list represent the tasks that are always placed at the front of the queue
    41  // PatchTasks and RepotrackerTasks are interleaved after the high priority tasks.
    42  type CmpBasedTaskQueues struct {
    43  	HighPriorityTasks []task.Task
    44  	PatchTasks        []task.Task
    45  	RepotrackerTasks  []task.Task
    46  }
    47  
    48  // NewCmpBasedTaskComparator returns a new task prioritizer, using the default set of comparators
    49  // as well as the setup functions necessary for those comparators.
    50  func NewCmpBasedTaskComparator() *CmpBasedTaskComparator {
    51  	return &CmpBasedTaskComparator{
    52  		setupFuncs: []sortSetupFunc{
    53  			cachePreviousTasks,
    54  			cacheSimilarFailing,
    55  		},
    56  		comparators: []taskPriorityCmp{
    57  			byPriority,
    58  			byNumDeps,
    59  			byRevisionOrderNumber,
    60  			byCreateTime,
    61  			bySimilarFailing,
    62  			byRecentlyFailing,
    63  		},
    64  	}
    65  }
    66  
    67  type CmpBasedTaskPrioritizer struct{}
    68  
    69  // PrioritizeTask prioritizes the tasks to run. First splits the tasks into slices based on
    70  // whether they are part of patch versions or automatically created versions.
    71  // Then prioritizes each slice, and merges them.
    72  // Returns a full slice of the prioritized tasks, and an error if one occurs.
    73  func (prioritizer *CmpBasedTaskPrioritizer) PrioritizeTasks(
    74  	settings *evergreen.Settings, tasks []task.Task) ([]task.Task, error) {
    75  
    76  	comparator := NewCmpBasedTaskComparator()
    77  	// split the tasks into repotracker tasks and patch tasks, then prioritize
    78  	// individually and merge
    79  	taskQueues := comparator.splitTasksByRequester(tasks)
    80  	prioritizedTaskLists := make([][]task.Task, 0, 3)
    81  	for _, taskList := range [][]task.Task{taskQueues.RepotrackerTasks, taskQueues.PatchTasks, taskQueues.HighPriorityTasks} {
    82  
    83  		comparator.tasks = taskList
    84  
    85  		err := comparator.setupForSortingTasks()
    86  		if err != nil {
    87  			return nil, errors.Wrap(err, "Error running setup for sorting tasks")
    88  		}
    89  
    90  		sort.Sort(comparator)
    91  
    92  		if len(comparator.errsDuringSort) > 0 {
    93  			errString := "The following errors were thrown while sorting:"
    94  			for _, e := range comparator.errsDuringSort {
    95  				errString += fmt.Sprintf("\n    %v", e)
    96  			}
    97  			return nil, errors.New(errString)
    98  		}
    99  
   100  		prioritizedTaskLists = append(prioritizedTaskLists, comparator.tasks)
   101  	}
   102  	prioritizedTaskQueues := CmpBasedTaskQueues{
   103  		RepotrackerTasks:  prioritizedTaskLists[0],
   104  		PatchTasks:        prioritizedTaskLists[1],
   105  		HighPriorityTasks: prioritizedTaskLists[2],
   106  	}
   107  
   108  	comparator.tasks = comparator.mergeTasks(settings, &prioritizedTaskQueues)
   109  
   110  	return comparator.tasks, nil
   111  }
   112  
   113  // Run all of the setup functions necessary for prioritizing the tasks.
   114  // Returns an error if any of the setup funcs return an error.
   115  func (self *CmpBasedTaskComparator) setupForSortingTasks() error {
   116  	for _, setupFunc := range self.setupFuncs {
   117  		if err := setupFunc(self); err != nil {
   118  			return errors.Wrap(err, "Error running setup for sorting")
   119  		}
   120  	}
   121  	return nil
   122  }
   123  
   124  // Determine which of two tasks is more important, by running the tasks through
   125  // the comparator functions and returning the first definitive decision on which
   126  // is more important.
   127  func (self *CmpBasedTaskComparator) taskMoreImportantThan(task1,
   128  	task2 task.Task) (bool, error) {
   129  
   130  	// run through the comparators, and return the first definitive decision on
   131  	// which task is more important
   132  	for _, cmp := range self.comparators {
   133  		ret, err := cmp(task1, task2, self)
   134  		if err != nil {
   135  			return false, errors.WithStack(err)
   136  		}
   137  		switch ret {
   138  		case -1:
   139  			return false, nil
   140  		case 0:
   141  			continue
   142  		case 1:
   143  			return true, nil
   144  		default:
   145  			panic("Unexpected return value from task comparator")
   146  		}
   147  	}
   148  
   149  	// none of the comparators reached a definitive decision, so the return val
   150  	// doesn't matter
   151  	return false, nil
   152  }
   153  
   154  // Functions that ensure the CmdBasedTaskPrioritizer implements sort.Interface
   155  
   156  func (self *CmpBasedTaskComparator) Len() int {
   157  	return len(self.tasks)
   158  }
   159  
   160  func (self *CmpBasedTaskComparator) Less(i, j int) bool {
   161  	moreImportant, err := self.taskMoreImportantThan(self.tasks[i],
   162  		self.tasks[j])
   163  	if err != nil {
   164  		self.errsDuringSort = append(self.errsDuringSort, err)
   165  	}
   166  	return moreImportant
   167  }
   168  
   169  func (self *CmpBasedTaskComparator) Swap(i, j int) {
   170  	self.tasks[i], self.tasks[j] = self.tasks[j], self.tasks[i]
   171  }
   172  
   173  // Split the tasks, based on the requester field.
   174  // Returns two slices - the tasks requested by the repotracker, and the tasks
   175  // requested in a patch.
   176  func (self *CmpBasedTaskComparator) splitTasksByRequester(
   177  	allTasks []task.Task) *CmpBasedTaskQueues {
   178  
   179  	repoTrackerTasks := make([]task.Task, 0, len(allTasks))
   180  	patchTasks := make([]task.Task, 0, len(allTasks))
   181  	priorityTasks := make([]task.Task, 0, len(allTasks))
   182  
   183  	for _, task := range allTasks {
   184  		switch {
   185  		case task.Priority > evergreen.MaxTaskPriority:
   186  			priorityTasks = append(priorityTasks, task)
   187  		case task.Requester == evergreen.RepotrackerVersionRequester:
   188  			repoTrackerTasks = append(repoTrackerTasks, task)
   189  		case task.Requester == evergreen.PatchVersionRequester:
   190  			patchTasks = append(patchTasks, task)
   191  		default:
   192  			grip.Errorf("Unrecognized requester '%s' for task %s", task.Requester, task.Id)
   193  		}
   194  	}
   195  
   196  	return &CmpBasedTaskQueues{
   197  		HighPriorityTasks: priorityTasks,
   198  		RepotrackerTasks:  repoTrackerTasks,
   199  		PatchTasks:        patchTasks,
   200  	}
   201  }
   202  
   203  // Merge the slices of tasks requested by the repotracker and in patches.
   204  // Returns a slice of the merged tasks.
   205  func (self *CmpBasedTaskComparator) mergeTasks(settings *evergreen.Settings,
   206  	tq *CmpBasedTaskQueues) []task.Task {
   207  
   208  	mergedTasks := make([]task.Task, 0, len(tq.RepotrackerTasks)+
   209  		len(tq.PatchTasks)+len(tq.HighPriorityTasks))
   210  
   211  	toggle := settings.Scheduler.MergeToggle
   212  	if toggle == 0 {
   213  		toggle = 2 // defaults to interleaving evenly
   214  	}
   215  
   216  	rIdx := 0
   217  	pIdx := 0
   218  	lenRepoTrackerTasks := len(tq.RepotrackerTasks)
   219  	lenPatchTasks := len(tq.PatchTasks)
   220  
   221  	// add the high priority tasks to the start of the queue
   222  	mergedTasks = append(mergedTasks, tq.HighPriorityTasks...)
   223  	for idx := 0; idx < len(tq.RepotrackerTasks)+len(tq.PatchTasks); idx++ {
   224  		if pIdx >= lenPatchTasks { // overruns patch tasks
   225  			mergedTasks = append(mergedTasks, tq.RepotrackerTasks[rIdx])
   226  			rIdx++
   227  		} else if rIdx >= lenRepoTrackerTasks { // overruns repotracker tasks
   228  			mergedTasks = append(mergedTasks, tq.PatchTasks[pIdx])
   229  			pIdx++
   230  		} else if idx > 0 && (idx+1)%toggle == 0 { // turn for a repotracker task
   231  			mergedTasks = append(mergedTasks, tq.RepotrackerTasks[rIdx])
   232  			rIdx++
   233  		} else { // turn for a patch task
   234  			mergedTasks = append(mergedTasks, tq.PatchTasks[pIdx])
   235  			pIdx++
   236  		}
   237  	}
   238  	return mergedTasks
   239  }