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  }