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 }