github.com/grafana/pyroscope@v1.18.0/pkg/metastore/compaction/scheduler/scheduler_queue.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"container/heap"
     5  	"slices"
     6  	"strings"
     7  
     8  	"github.com/grafana/pyroscope/api/gen/proto/go/metastore/v1/raft_log"
     9  )
    10  
    11  type schedulerQueue struct {
    12  	jobs map[string]*jobEntry
    13  	// Sparse array of job queues, indexed by compaction level.
    14  	levels []*jobQueue
    15  }
    16  
    17  func newJobQueue() *schedulerQueue {
    18  	return &schedulerQueue{
    19  		jobs: make(map[string]*jobEntry),
    20  	}
    21  }
    22  
    23  func (q *schedulerQueue) reset() {
    24  	clear(q.jobs)
    25  	clear(q.levels)
    26  	q.levels = q.levels[:0]
    27  }
    28  
    29  func (q *schedulerQueue) put(state *raft_log.CompactionJobState) {
    30  	job, exists := q.jobs[state.Name]
    31  	level := q.level(state.CompactionLevel)
    32  	if exists {
    33  		level.update(job, state)
    34  		return
    35  	}
    36  	e := &jobEntry{CompactionJobState: state}
    37  	q.jobs[state.Name] = e
    38  	level.add(e)
    39  }
    40  
    41  func (q *schedulerQueue) delete(name string) *raft_log.CompactionJobState {
    42  	if e, exists := q.jobs[name]; exists {
    43  		delete(q.jobs, name)
    44  		level := q.level(e.CompactionLevel)
    45  		level.delete(e)
    46  		level.stats.completedTotal++
    47  		return e.CompactionJobState
    48  	}
    49  	return nil
    50  }
    51  
    52  // evict is identical to delete, but it updates the eviction stats.
    53  func (q *schedulerQueue) evict(name string) {
    54  	if e, exists := q.jobs[name]; exists {
    55  		delete(q.jobs, name)
    56  		level := q.level(e.CompactionLevel)
    57  		level.delete(e)
    58  		level.stats.evictedTotal++
    59  	}
    60  }
    61  
    62  func (q *schedulerQueue) size() int {
    63  	var size int
    64  	for _, level := range q.levels {
    65  		if level != nil {
    66  			size += level.jobs.Len()
    67  		}
    68  	}
    69  	return size
    70  }
    71  
    72  func (q *schedulerQueue) level(x uint32) *jobQueue {
    73  	s := x + 1 // Levels are 0-based.
    74  	if s >= uint32(len(q.levels)) {
    75  		q.levels = slices.Grow(q.levels, int(s))[:s]
    76  	}
    77  	level := q.levels[x]
    78  	if level == nil {
    79  		level = &jobQueue{
    80  			jobs:  new(priorityJobQueue),
    81  			stats: new(queueStats),
    82  		}
    83  		q.levels[x] = level
    84  	}
    85  	return level
    86  }
    87  
    88  func (q *schedulerQueue) resetStats() {
    89  	for _, level := range q.levels {
    90  		if level != nil {
    91  			level.stats.reset()
    92  		}
    93  	}
    94  }
    95  
    96  type jobQueue struct {
    97  	jobs  *priorityJobQueue
    98  	stats *queueStats
    99  }
   100  
   101  type queueStats struct {
   102  	// Counters. Updated on access.
   103  	addedTotal      uint32
   104  	completedTotal  uint32
   105  	assignedTotal   uint32
   106  	reassignedTotal uint32
   107  	evictedTotal    uint32
   108  	// Gauges. Updated periodically.
   109  	assigned   uint32
   110  	unassigned uint32
   111  	reassigned uint32
   112  	failed     uint32
   113  }
   114  
   115  func (s *queueStats) reset() {
   116  	*s = queueStats{}
   117  }
   118  
   119  type jobEntry struct {
   120  	index int // The index of the job in the heap.
   121  	*raft_log.CompactionJobState
   122  }
   123  
   124  func (q *jobQueue) add(e *jobEntry) {
   125  	q.stats.addedTotal++
   126  	heap.Push(q.jobs, e)
   127  }
   128  
   129  func (q *jobQueue) update(e *jobEntry, state *raft_log.CompactionJobState) {
   130  	if e.Status == 0 && state.Status != 0 {
   131  		// Job given a status.
   132  		q.stats.assignedTotal++
   133  	}
   134  	if e.Status != 0 && e.Token != state.Token {
   135  		// Token change.
   136  		q.stats.reassignedTotal++
   137  	}
   138  	e.CompactionJobState = state
   139  	heap.Fix(q.jobs, e.index)
   140  }
   141  
   142  func (q *jobQueue) delete(e *jobEntry) {
   143  	heap.Remove(q.jobs, e.index)
   144  }
   145  
   146  func (q *jobQueue) clone() priorityJobQueue {
   147  	c := make(priorityJobQueue, q.jobs.Len())
   148  	for j, job := range *q.jobs {
   149  		jobCopy := *job
   150  		c[j] = &jobCopy
   151  	}
   152  	return c
   153  }
   154  
   155  // The function determines the scheduling order of the jobs.
   156  func compareJobs(a, b *jobEntry) int {
   157  	// Pick jobs in the "initial" (unspecified) state first.
   158  	if a.Status != b.Status {
   159  		return int(a.Status) - int(b.Status)
   160  	}
   161  	// Faulty jobs should wait. Our aim is to put them at the
   162  	// end of the queue, after all the jobs we may consider
   163  	// for assigment.
   164  	if a.Failures != b.Failures {
   165  		return int(a.Failures) - int(b.Failures)
   166  	}
   167  	// Jobs with earlier deadlines should go first.
   168  	// A job that has been just added has no lease
   169  	// and will always go first.
   170  	if a.LeaseExpiresAt != b.LeaseExpiresAt {
   171  		return int(a.LeaseExpiresAt) - int(b.LeaseExpiresAt)
   172  	}
   173  	// Tiebreaker: the job name must not bias the order.
   174  	return strings.Compare(a.Name, b.Name)
   175  }
   176  
   177  // TODO(kolesnikovae): container/heap is not very efficient,
   178  //  consider implementing own heap, specific to the case.
   179  //  A treap might be suitable as well.
   180  
   181  type priorityJobQueue []*jobEntry
   182  
   183  func (pq priorityJobQueue) Len() int { return len(pq) }
   184  
   185  func (pq priorityJobQueue) Less(i, j int) bool {
   186  	return compareJobs(pq[i], pq[j]) < 0
   187  }
   188  
   189  func (pq priorityJobQueue) Swap(i, j int) {
   190  	pq[i], pq[j] = pq[j], pq[i]
   191  	pq[i].index = i
   192  	pq[j].index = j
   193  }
   194  
   195  func (pq *priorityJobQueue) Push(x interface{}) {
   196  	n := len(*pq)
   197  	job := x.(*jobEntry)
   198  	job.index = n
   199  	*pq = append(*pq, job)
   200  }
   201  
   202  func (pq *priorityJobQueue) Pop() interface{} {
   203  	old := *pq
   204  	n := len(old)
   205  	job := old[n-1]
   206  	old[n-1] = nil
   207  	job.index = -1
   208  	*pq = old[0 : n-1]
   209  	return job
   210  }