github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/kbfssync/semaphore.go (about)

     1  // Copyright 2017 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package kbfssync
     6  
     7  import (
     8  	"fmt"
     9  	"math"
    10  	"sync"
    11  
    12  	"github.com/pkg/errors"
    13  	"golang.org/x/net/context"
    14  )
    15  
    16  // Semaphore implements a counting semaphore; it maintains a resource
    17  // count, and exposes methods for acquiring those resources -- waiting
    18  // if desired -- and releasing those resources back.
    19  type Semaphore struct {
    20  	lock      sync.RWMutex
    21  	count     int64
    22  	onRelease chan struct{}
    23  }
    24  
    25  // NewSemaphore returns a new Semaphore with a resource count of
    26  // 0. Use Release() to set the initial resource count.
    27  func NewSemaphore() *Semaphore {
    28  	return &Semaphore{
    29  		onRelease: make(chan struct{}),
    30  	}
    31  }
    32  
    33  // Count returns the current resource count.
    34  func (s *Semaphore) Count() int64 {
    35  	s.lock.RLock()
    36  	defer s.lock.RUnlock()
    37  	return s.count
    38  }
    39  
    40  // tryAcquire tries to acquire n resources. If successful, nil is
    41  // returned. Otherwise, a channel which will be closed when new
    42  // resources are available is returned. In either case, the
    43  // possibly-updated resource count is returned.
    44  func (s *Semaphore) tryAcquire(n int64) (<-chan struct{}, int64) {
    45  	s.lock.Lock()
    46  	defer s.lock.Unlock()
    47  	if n <= s.count {
    48  		s.count -= n
    49  		return nil, s.count
    50  	}
    51  
    52  	return s.onRelease, s.count
    53  }
    54  
    55  // Acquire blocks until it is possible to atomically subtract n (which
    56  // must be positive) from the resource count without causing it to go
    57  // negative, and then returns the updated resource count and nil. If
    58  // the given context is canceled or times out first, it instead does
    59  // not change the resource count, and returns the resource count at
    60  // the time it blocked (which is necessarily less than n), and a
    61  // wrapped ctx.Err().
    62  func (s *Semaphore) Acquire(ctx context.Context, n int64) (int64, error) {
    63  	if n <= 0 {
    64  		panic(fmt.Sprintf("n=%d must be positive", n))
    65  	}
    66  
    67  	for {
    68  		onRelease, count := s.tryAcquire(n)
    69  		if onRelease == nil {
    70  			return count, nil
    71  		}
    72  
    73  		select {
    74  		case <-onRelease:
    75  			// Go to the top of the loop.
    76  		case <-ctx.Done():
    77  			return count, errors.WithStack(ctx.Err())
    78  		}
    79  	}
    80  }
    81  
    82  // ForceAcquire atomically subtracts n (which must be positive) from the
    83  // resource count without waking up any waiting acquirers. It is meant for
    84  // correcting the initial resource count of the semaphore. It's okay if adding
    85  // n causes the resource count goes negative, but it must not cause the
    86  // resource count to underflow. The updated resource count is returned.
    87  func (s *Semaphore) ForceAcquire(n int64) int64 {
    88  	if n <= 0 {
    89  		panic(fmt.Sprintf("n=%d must be positive", n))
    90  	}
    91  
    92  	s.lock.Lock()
    93  	defer s.lock.Unlock()
    94  	if s.count < (math.MinInt64 + n) {
    95  		panic(fmt.Sprintf("s.count=%d - n=%d would underflow",
    96  			s.count, n))
    97  	}
    98  	s.count -= n
    99  	return s.count
   100  }
   101  
   102  // TryAcquire atomically subtracts n (which must be positive) from the resource
   103  // count without waking up any waiting acquirers, as long as it wouldn't go
   104  // negative. If the count would go negative, it doesn't update the count but
   105  // still returns the difference between the count and n. TryAcquire is
   106  // successful if the return value is non-negative, and unsuccessful if the
   107  // return value is negative. If the count would underflow, it panics.
   108  // Otherwise, TryAcquire returns the updated resource count.
   109  func (s *Semaphore) TryAcquire(n int64) int64 {
   110  	if n <= 0 {
   111  		panic(fmt.Sprintf("n=%d must be positive", n))
   112  	}
   113  
   114  	s.lock.Lock()
   115  	defer s.lock.Unlock()
   116  	if s.count < n {
   117  		if s.count < (math.MinInt64 + n) {
   118  			panic(fmt.Sprintf("s.count=%d - n=%d would overflow",
   119  				s.count, n))
   120  		}
   121  		return s.count - n
   122  	}
   123  	s.count -= n
   124  	return s.count
   125  }
   126  
   127  // Release atomically adds n (which must be positive) to the resource
   128  // count. It must not cause the resource count to overflow. If there
   129  // are waiting acquirers, it wakes up at least one of them to make
   130  // progress, assuming that no new acquirers arrive in the meantime.
   131  // The updated resource count is returned.
   132  func (s *Semaphore) Release(n int64) int64 {
   133  	if n <= 0 {
   134  		panic(fmt.Sprintf("n=%d must be positive", n))
   135  	}
   136  
   137  	s.lock.Lock()
   138  	defer s.lock.Unlock()
   139  	if s.count > (math.MaxInt64 - n) {
   140  		panic(fmt.Sprintf("s.count=%d + n=%d would overflow",
   141  			s.count, n))
   142  	}
   143  	s.count += n
   144  	// TODO: A better implementation would keep track of each
   145  	// waiter and how much it wants to acquire and only wake up
   146  	// waiters that could possibly succeed.
   147  	close(s.onRelease)
   148  	s.onRelease = make(chan struct{})
   149  	return s.count
   150  }