golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/coordinator/pool/queue/quota.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build linux || darwin 6 7 package queue 8 9 import ( 10 "container/heap" 11 "context" 12 "sort" 13 "sync" 14 ) 15 16 // NewQuota returns an initialized *Quota ready for use. 17 func NewQuota() *Quota { 18 return &Quota{ 19 queue: new(buildletQueue), 20 } 21 } 22 23 // Quota manages a queue for a single quota. 24 type Quota struct { 25 mu sync.Mutex 26 queue *buildletQueue 27 limit int 28 used int 29 // On GCE, other instances run in the same project as buildlet 30 // instances. Track those separately, and subtract from available. 31 untrackedUsed int 32 } 33 34 func (q *Quota) push(item *Item) { 35 defer q.updated() 36 q.mu.Lock() 37 defer q.mu.Unlock() 38 heap.Push(q.queue, item) 39 } 40 41 func (q *Quota) cancel(item *Item) { 42 defer q.updated() 43 q.mu.Lock() 44 defer q.mu.Unlock() 45 if item.index != -1 { 46 heap.Remove(q.queue, item.index) 47 } 48 } 49 50 func (q *Quota) updated() { 51 for { 52 if q.tryPop() == nil { 53 return 54 } 55 } 56 } 57 58 // tryPop returns a Item if quota is available and unblocks the 59 // AwaitQueue call. 60 func (q *Quota) tryPop() *Item { 61 q.mu.Lock() 62 defer q.mu.Unlock() 63 if !(q.queue.Len() != 0 && q.queue.Peek().cost <= q.limit-q.used-q.untrackedUsed) { 64 return nil 65 } 66 b := q.queue.PopBuildlet() 67 q.used += b.cost 68 b.ready() 69 return b 70 } 71 72 // Empty returns true when there are no items in the queue. 73 func (q *Quota) Empty() bool { 74 return q.Len() == 0 75 } 76 77 // Len returns the number of items in the queue. 78 func (q *Quota) Len() int { 79 q.mu.Lock() 80 defer q.mu.Unlock() 81 return q.queue.Len() 82 } 83 84 // UpdateQuotas updates the limit and used values on the queue. 85 func (q *Quota) UpdateQuotas(used, limit int) { 86 defer q.updated() 87 q.mu.Lock() 88 defer q.mu.Unlock() 89 q.limit = limit 90 q.used = used 91 } 92 93 // UpdateLimit updates the limit values on the queue. 94 func (q *Quota) UpdateLimit(limit int) { 95 defer q.updated() 96 q.mu.Lock() 97 defer q.mu.Unlock() 98 q.limit = limit 99 } 100 101 func (q *Quota) UpdateUntracked(n int) { 102 defer q.updated() 103 q.mu.Lock() 104 defer q.mu.Unlock() 105 q.untrackedUsed = n 106 } 107 108 // ReturnQuota decrements the used quota value by v. 109 func (q *Quota) ReturnQuota(v int) { 110 defer q.updated() 111 q.mu.Lock() 112 defer q.mu.Unlock() 113 q.used -= v 114 } 115 116 type Usage struct { 117 Used int 118 Limit int 119 UntrackedUsed int 120 } 121 122 // Quotas returns the used, limit, and untracked values for the queue. 123 func (q *Quota) Quotas() Usage { 124 q.mu.Lock() 125 defer q.mu.Unlock() 126 return Usage{ 127 Used: q.used, 128 Limit: q.limit, 129 UntrackedUsed: q.untrackedUsed, 130 } 131 } 132 133 // Enqueue a build and return an Item. See Item's documentation for 134 // waiting and releasing quota. 135 func (q *Quota) Enqueue(cost int, si *SchedItem) *Item { 136 item := &Item{ 137 cost: cost, 138 release: func() { q.ReturnQuota(cost) }, 139 popped: make(chan struct{}), 140 build: si, 141 } 142 item.cancel = func() { q.cancel(item) } 143 q.push(item) 144 return item 145 } 146 147 // AwaitQueue enqueues a build and returns once the item is unblocked 148 // by quota, by order of minimum priority. 149 // 150 // If the provided context is cancelled before popping, the item is 151 // removed from the queue and an error is returned. 152 func (q *Quota) AwaitQueue(ctx context.Context, cost int, si *SchedItem) error { 153 if err := ctx.Err(); err != nil { 154 return ctx.Err() 155 } 156 return q.Enqueue(cost, si).Await(ctx) 157 } 158 159 type QuotaStats struct { 160 Usage 161 Items []ItemStats 162 } 163 164 type ItemStats struct { 165 Build *SchedItem 166 Cost int 167 } 168 169 func (q *Quota) ToExported() *QuotaStats { 170 q.mu.Lock() 171 qs := &QuotaStats{ 172 Usage: Usage{ 173 Used: q.used, 174 Limit: q.limit, 175 UntrackedUsed: q.untrackedUsed, 176 }, 177 Items: make([]ItemStats, q.queue.Len()), 178 } 179 for i, item := range *q.queue { 180 qs.Items[i].Build = item.SchedItem() 181 qs.Items[i].Cost = item.cost 182 } 183 q.mu.Unlock() 184 185 sort.Slice(qs.Items, func(i, j int) bool { 186 return qs.Items[i].Build.Less(qs.Items[j].Build) 187 }) 188 return qs 189 } 190 191 // An Item is something we manage in a priority buildletQueue. 192 type Item struct { 193 build *SchedItem 194 cancel func() 195 cost int 196 popped chan struct{} 197 release func() 198 // index is maintained by the heap.Interface methods. 199 index int 200 } 201 202 // SchedItem returns a copy of the SchedItem for a build. 203 func (i *Item) SchedItem() *SchedItem { 204 build := *i.build 205 return &build 206 } 207 208 // Await blocks until the Item holds the necessary quota amount, or the 209 // context is cancelled. 210 // 211 // On success, the caller must call ReturnQuota() to release the quota. 212 func (i *Item) Await(ctx context.Context) error { 213 if ctx.Err() != nil { 214 i.cancel() 215 i.ReturnQuota() 216 return ctx.Err() 217 } 218 select { 219 case <-ctx.Done(): 220 i.cancel() 221 i.ReturnQuota() 222 return ctx.Err() 223 case <-i.popped: 224 return nil 225 } 226 } 227 228 // ReturnQuota returns quota to the Queue. ReturnQuota is a no-op if 229 // the item has never been popped. 230 func (i *Item) ReturnQuota() { 231 select { 232 case <-i.popped: 233 i.release() 234 default: 235 // We haven't been popped yet, nothing to release. 236 return 237 } 238 } 239 240 func (i *Item) ready() { 241 close(i.popped) 242 } 243 244 // A buildletQueue implements heap.Interface and holds Items. 245 type buildletQueue []*Item 246 247 func (q buildletQueue) Len() int { return len(q) } 248 249 func (q buildletQueue) Less(i, j int) bool { 250 return q[i].build.Less(q[j].build) 251 } 252 253 func (q buildletQueue) Swap(i, j int) { 254 q[i], q[j] = q[j], q[i] 255 q[i].index = i 256 q[j].index = j 257 } 258 259 func (q *buildletQueue) Push(x interface{}) { 260 n := len(*q) 261 item := x.(*Item) 262 item.index = n 263 *q = append(*q, item) 264 } 265 266 func (q *buildletQueue) Pop() interface{} { 267 old := *q 268 n := len(old) 269 item := old[n-1] 270 old[n-1] = nil // avoid memory leak 271 item.index = -1 // necessary to avoid races in (*Queue).cancel(). 272 *q = old[0 : n-1] 273 return item 274 } 275 276 func (q *buildletQueue) PopBuildlet() *Item { 277 return heap.Pop(q).(*Item) 278 } 279 280 func (q buildletQueue) Peek() *Item { 281 return q[0] 282 }