github.com/rudderlabs/rudder-go-kit@v0.30.0/sync/limiter.go (about) 1 package sync 2 3 import ( 4 "container/heap" 5 "context" 6 "fmt" 7 "sync" 8 "time" 9 10 "github.com/samber/lo" 11 12 "github.com/rudderlabs/rudder-go-kit/queue" 13 "github.com/rudderlabs/rudder-go-kit/stats" 14 ) 15 16 // LimiterPriorityValue defines the priority values supported by Limiter. 17 // Greater priority value means higher priority 18 type LimiterPriorityValue int 19 20 const ( 21 _ LimiterPriorityValue = iota 22 // LimiterPriorityValueLow Priority.... 23 LimiterPriorityValueLow 24 // LimiterPriorityValueMedium Priority.... 25 LimiterPriorityValueMedium 26 // LimiterPriorityValueMediumHigh Priority.... 27 LimiterPriorityValueMediumHigh 28 // LimiterPriorityValueHigh Priority..... 29 LimiterPriorityValueHigh 30 ) 31 32 // Limiter limits the number of concurrent operations that can be performed 33 type Limiter interface { 34 // Do executes the function f, but only if there are available slots. 35 // Otherwise blocks until a slot becomes available 36 Do(key string, f func()) 37 38 // DoWithPriority executes the function f, but only if there are available slots. 39 // Otherwise blocks until a slot becomes available, respecting the priority 40 DoWithPriority(key string, priority LimiterPriorityValue, f func()) 41 42 // Begin starts a new operation, blocking until a slot becomes available. 43 // Caller is expected to call the returned function to end the operation, otherwise 44 // the slot will be reserved indefinitely. End can be called multiple times without any side effects 45 Begin(key string) (end func()) 46 47 // BeginWithPriority starts a new operation, blocking until a slot becomes available, respecting the priority. 48 // Caller is expected to call the returned function to end the operation, otherwise 49 // the slot will be reserved indefinitely. End can be called multiple times without any side effects 50 BeginWithPriority(key string, priority LimiterPriorityValue) (end func()) 51 } 52 53 var WithLimiterStatsTriggerFunc = func(triggerFunc func() <-chan time.Time) func(*limiter) { 54 return func(l *limiter) { 55 l.stats.triggerFunc = triggerFunc 56 } 57 } 58 59 var WithLimiterDynamicPeriod = func(dynamicPeriod time.Duration) func(*limiter) { 60 return func(l *limiter) { 61 l.dynamicPeriod = dynamicPeriod 62 } 63 } 64 65 var WithLimiterTags = func(tags stats.Tags) func(*limiter) { 66 return func(l *limiter) { 67 l.tags = tags 68 } 69 } 70 71 // NewLimiter creates a new limiter 72 func NewLimiter(ctx context.Context, wg *sync.WaitGroup, name string, limit int, statsf stats.Stats, opts ...func(*limiter)) Limiter { 73 if limit <= 0 { 74 panic(fmt.Errorf("limit for %q must be greater than 0", name)) 75 } 76 l := &limiter{ 77 name: name, 78 limit: limit, 79 tags: stats.Tags{}, 80 waitList: make(queue.PriorityQueue[chan struct{}], 0), 81 } 82 heap.Init(&l.waitList) 83 l.stats.triggerFunc = func() <-chan time.Time { 84 return time.After(15 * time.Second) 85 } 86 for _, opt := range opts { 87 opt(l) 88 } 89 90 l.stats.stat = statsf 91 l.stats.waitGauge = statsf.NewTaggedStat(name+"_limiter_waiting_routines", stats.GaugeType, l.tags) 92 l.stats.activeGauge = statsf.NewTaggedStat(name+"_limiter_active_routines", stats.GaugeType, l.tags) 93 l.stats.availabilityGauge = statsf.NewTaggedStat(name+"_limiter_availability", stats.GaugeType, l.tags) 94 95 wg.Add(1) 96 go func() { 97 defer wg.Done() 98 for { 99 select { 100 case <-ctx.Done(): 101 return 102 case <-l.stats.triggerFunc(): 103 } 104 l.mu.Lock() 105 l.stats.activeGauge.Gauge(l.count) 106 l.stats.waitGauge.Gauge(len(l.waitList)) 107 availability := float64(l.limit-l.count) / float64(l.limit) 108 l.stats.availabilityGauge.Gauge(availability) 109 l.mu.Unlock() 110 } 111 }() 112 return l 113 } 114 115 type limiter struct { 116 name string 117 limit int 118 tags stats.Tags 119 dynamicPeriod time.Duration 120 121 mu sync.Mutex // protects count and waitList below 122 count int 123 waitList queue.PriorityQueue[chan struct{}] 124 125 stats struct { 126 triggerFunc func() <-chan time.Time 127 stat stats.Stats 128 waitGauge stats.Measurement // gauge showing number of operations waiting in the queue 129 activeGauge stats.Measurement // gauge showing active number of operations 130 availabilityGauge stats.Measurement // gauge showing availability percentage of limiter (0.0 to 1.0) 131 } 132 } 133 134 func (l *limiter) Do(key string, f func()) { 135 l.DoWithPriority(key, LimiterPriorityValueLow, f) 136 } 137 138 func (l *limiter) DoWithPriority(key string, priority LimiterPriorityValue, f func()) { 139 defer l.BeginWithPriority(key, priority)() 140 f() 141 } 142 143 func (l *limiter) Begin(key string) (end func()) { 144 return l.BeginWithPriority(key, LimiterPriorityValueLow) 145 } 146 147 func (l *limiter) BeginWithPriority(key string, priority LimiterPriorityValue) (end func()) { 148 start := time.Now() 149 l.wait(priority) 150 tags := lo.Assign(l.tags, stats.Tags{"key": key}) 151 l.stats.stat.NewTaggedStat(l.name+"_limiter_waiting", stats.TimerType, tags).Since(start) 152 start = time.Now() 153 var endOnce sync.Once 154 155 end = func() { 156 endOnce.Do(func() { 157 defer l.stats.stat.NewTaggedStat(l.name+"_limiter_working", stats.TimerType, tags).Since(start) 158 l.mu.Lock() 159 l.count-- 160 if len(l.waitList) == 0 { 161 l.mu.Unlock() 162 return 163 } 164 next := heap.Pop(&l.waitList).(*queue.Item[chan struct{}]) 165 l.count++ 166 l.mu.Unlock() 167 next.Value <- struct{}{} 168 close(next.Value) 169 }) 170 } 171 return end 172 } 173 174 // wait until a slot becomes available 175 func (l *limiter) wait(priority LimiterPriorityValue) { 176 l.mu.Lock() 177 if l.count < l.limit { 178 l.count++ 179 l.mu.Unlock() 180 return 181 } 182 w := &queue.Item[chan struct{}]{ 183 Priority: int(priority), 184 Value: make(chan struct{}), 185 } 186 heap.Push(&l.waitList, w) 187 l.mu.Unlock() 188 189 // no dynamic priority 190 if l.dynamicPeriod == 0 || priority == LimiterPriorityValueHigh { 191 <-w.Value 192 return 193 } 194 195 // dynamic priority (increment priority every dynamicPeriod) 196 ticker := time.NewTicker(l.dynamicPeriod) 197 defer ticker.Stop() 198 for { 199 select { 200 case <-w.Value: 201 ticker.Stop() 202 return 203 case <-ticker.C: 204 if w.Priority < int(LimiterPriorityValueHigh) { 205 l.mu.Lock() 206 l.waitList.Update(w, w.Priority+1) 207 l.mu.Unlock() 208 } else { 209 ticker.Stop() 210 <-w.Value 211 return 212 } 213 } 214 } 215 }