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  }