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 }