go.temporal.io/server@v1.23.0/common/tasks/interleaved_weighted_round_robin.go (about)

     1  // The MIT License
     2  //
     3  // Copyright (c) 2020 Temporal Technologies Inc.  All rights reserved.
     4  //
     5  // Copyright (c) 2020 Uber Technologies, Inc.
     6  //
     7  // Permission is hereby granted, free of charge, to any person obtaining a copy
     8  // of this software and associated documentation files (the "Software"), to deal
     9  // in the Software without restriction, including without limitation the rights
    10  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    11  // copies of the Software, and to permit persons to whom the Software is
    12  // furnished to do so, subject to the following conditions:
    13  //
    14  // The above copyright notice and this permission notice shall be included in
    15  // all copies or substantial portions of the Software.
    16  //
    17  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    18  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    19  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    20  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    21  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    22  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    23  // THE SOFTWARE.
    24  
    25  package tasks
    26  
    27  import (
    28  	"sort"
    29  	"sync"
    30  	"sync/atomic"
    31  	"time"
    32  
    33  	"go.temporal.io/server/common"
    34  	"go.temporal.io/server/common/log"
    35  )
    36  
    37  const (
    38  	checkRateLimiterEnabledInterval = 1 * time.Minute
    39  )
    40  
    41  var _ Scheduler[Task] = (*InterleavedWeightedRoundRobinScheduler[Task, struct{}])(nil)
    42  
    43  type (
    44  	// InterleavedWeightedRoundRobinSchedulerOptions is the config for
    45  	// interleaved weighted round robin scheduler
    46  	InterleavedWeightedRoundRobinSchedulerOptions[T Task, K comparable] struct {
    47  		// Required for mapping a task to it's corresponding task channel
    48  		TaskChannelKeyFn TaskChannelKeyFn[T, K]
    49  		// Required for getting the weight for a task channel
    50  		ChannelWeightFn ChannelWeightFn[K]
    51  		// Optional, if specified, re-evaluate task channel weight when channel is not empty
    52  		ChannelWeightUpdateCh chan struct{}
    53  	}
    54  
    55  	// TaskChannelKeyFn is the function for mapping a task to its task channel (key)
    56  	TaskChannelKeyFn[T Task, K comparable] func(T) K
    57  
    58  	// ChannelWeightFn is the function for mapping a task channel (key) to its weight
    59  	ChannelWeightFn[K comparable] func(K) int
    60  
    61  	// InterleavedWeightedRoundRobinScheduler is a round robin scheduler implementation
    62  	// ref: https://en.wikipedia.org/wiki/Weighted_round_robin#Interleaved_WRR
    63  	InterleavedWeightedRoundRobinScheduler[T Task, K comparable] struct {
    64  		status int32
    65  
    66  		fifoScheduler Scheduler[T]
    67  		logger        log.Logger
    68  
    69  		notifyChan   chan struct{}
    70  		shutdownChan chan struct{}
    71  
    72  		options InterleavedWeightedRoundRobinSchedulerOptions[T, K]
    73  
    74  		numInflightTask int64
    75  
    76  		sync.RWMutex
    77  		weightedChannels map[K]*WeightedChannel[T]
    78  
    79  		// precalculated / flattened task chan according to weight
    80  		// e.g. if
    81  		// ChannelKeyToWeight has the following mapping
    82  		//  0 -> 5
    83  		//  1 -> 3
    84  		//  2 -> 2
    85  		//  3 -> 1
    86  		// then iwrrChannels will contain chan [0, 0, 0, 1, 0, 1, 2, 0, 1, 2, 3] (ID-ed by channel key)
    87  		iwrrChannels atomic.Value // []*WeightedChannel
    88  	}
    89  )
    90  
    91  func NewInterleavedWeightedRoundRobinScheduler[T Task, K comparable](
    92  	options InterleavedWeightedRoundRobinSchedulerOptions[T, K],
    93  	fifoScheduler Scheduler[T],
    94  	logger log.Logger,
    95  ) *InterleavedWeightedRoundRobinScheduler[T, K] {
    96  	iwrrChannels := atomic.Value{}
    97  	iwrrChannels.Store(WeightedChannels[T]{})
    98  
    99  	return &InterleavedWeightedRoundRobinScheduler[T, K]{
   100  		status: common.DaemonStatusInitialized,
   101  
   102  		fifoScheduler: fifoScheduler,
   103  		logger:        logger,
   104  
   105  		options: options,
   106  
   107  		notifyChan:   make(chan struct{}, 1),
   108  		shutdownChan: make(chan struct{}),
   109  
   110  		numInflightTask:  0,
   111  		weightedChannels: make(map[K]*WeightedChannel[T]),
   112  		iwrrChannels:     iwrrChannels,
   113  	}
   114  }
   115  
   116  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) Start() {
   117  	if !atomic.CompareAndSwapInt32(
   118  		&s.status,
   119  		common.DaemonStatusInitialized,
   120  		common.DaemonStatusStarted,
   121  	) {
   122  		return
   123  	}
   124  
   125  	s.fifoScheduler.Start()
   126  
   127  	go s.eventLoop()
   128  
   129  	s.logger.Info("interleaved weighted round robin task scheduler started")
   130  }
   131  
   132  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) Stop() {
   133  	if !atomic.CompareAndSwapInt32(
   134  		&s.status,
   135  		common.DaemonStatusStarted,
   136  		common.DaemonStatusStopped,
   137  	) {
   138  		return
   139  	}
   140  
   141  	close(s.shutdownChan)
   142  
   143  	s.fifoScheduler.Stop()
   144  
   145  	s.abortTasks()
   146  
   147  	s.logger.Info("interleaved weighted round robin task scheduler stopped")
   148  }
   149  
   150  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) Submit(
   151  	task T,
   152  ) {
   153  	numTasks := atomic.AddInt64(&s.numInflightTask, 1)
   154  	if !s.isStopped() && numTasks == 1 {
   155  		s.doDispatchTaskDirectly(task)
   156  		return
   157  	}
   158  
   159  	// there are tasks pending dispatching, need to respect round roubin weight
   160  	// or currently unable to submit to fifo scheduler, either due to buffer is full
   161  	// or exceeding rate limit
   162  	channel := s.getOrCreateTaskChannel(s.options.TaskChannelKeyFn(task))
   163  	channel.Chan() <- task
   164  	s.notifyDispatcher()
   165  }
   166  
   167  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) TrySubmit(
   168  	task T,
   169  ) bool {
   170  	numTasks := atomic.AddInt64(&s.numInflightTask, 1)
   171  	if !s.isStopped() && numTasks == 1 && s.tryDispatchTaskDirectly(task) {
   172  		return true
   173  	}
   174  
   175  	// there are tasks pending dispatching, need to respect round roubin weight
   176  	channel := s.getOrCreateTaskChannel(s.options.TaskChannelKeyFn(task))
   177  	select {
   178  	case channel.Chan() <- task:
   179  		s.notifyDispatcher()
   180  		return true
   181  	default:
   182  		atomic.AddInt64(&s.numInflightTask, -1)
   183  		return false
   184  	}
   185  }
   186  
   187  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) eventLoop() {
   188  	for {
   189  		select {
   190  		case <-s.notifyChan:
   191  			s.dispatchTasksWithWeight()
   192  		case <-s.shutdownChan:
   193  			return
   194  		}
   195  	}
   196  }
   197  
   198  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) getOrCreateTaskChannel(
   199  	channelKey K,
   200  ) *WeightedChannel[T] {
   201  	s.RLock()
   202  	channel, ok := s.weightedChannels[channelKey]
   203  	if ok {
   204  		s.RUnlock()
   205  		return channel
   206  	}
   207  	s.RUnlock()
   208  
   209  	s.Lock()
   210  	defer s.Unlock()
   211  
   212  	channel, ok = s.weightedChannels[channelKey]
   213  	if ok {
   214  		return channel
   215  	}
   216  
   217  	weight := s.options.ChannelWeightFn(channelKey)
   218  	channel = NewWeightedChannel[T](weight, WeightedChannelDefaultSize)
   219  	s.weightedChannels[channelKey] = channel
   220  
   221  	s.flattenWeightedChannelsLocked()
   222  	return channel
   223  }
   224  
   225  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) flattenWeightedChannelsLocked() {
   226  	weightedChannels := make(WeightedChannels[T], 0, len(s.weightedChannels))
   227  	for _, weightedChan := range s.weightedChannels {
   228  		weightedChannels = append(weightedChannels, weightedChan)
   229  	}
   230  	sort.Sort(weightedChannels)
   231  
   232  	iwrrChannels := make(WeightedChannels[T], 0, len(weightedChannels))
   233  	maxWeight := weightedChannels[len(weightedChannels)-1].Weight()
   234  	for round := maxWeight - 1; round > -1; round-- {
   235  		for index := len(weightedChannels) - 1; index > -1 && weightedChannels[index].Weight() > round; index-- {
   236  			iwrrChannels = append(iwrrChannels, weightedChannels[index])
   237  		}
   238  	}
   239  	s.iwrrChannels.Store(iwrrChannels)
   240  }
   241  
   242  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) channels() WeightedChannels[T] {
   243  	return s.iwrrChannels.Load().(WeightedChannels[T])
   244  }
   245  
   246  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) notifyDispatcher() {
   247  	if s.isStopped() {
   248  		s.abortTasks()
   249  		return
   250  	}
   251  
   252  	select {
   253  	case s.notifyChan <- struct{}{}:
   254  	default:
   255  	}
   256  }
   257  
   258  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) receiveWeightUpdateNotification() bool {
   259  	if s.options.ChannelWeightUpdateCh == nil {
   260  		return false
   261  	}
   262  
   263  	select {
   264  	case <-s.options.ChannelWeightUpdateCh:
   265  		// drain the channel as we don't know the channel size
   266  		for {
   267  			select {
   268  			case <-s.options.ChannelWeightUpdateCh:
   269  			default:
   270  				return true
   271  			}
   272  		}
   273  	default:
   274  		return false
   275  	}
   276  }
   277  
   278  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) updateChannelWeightLocked() {
   279  	for channelKey, weightedChannel := range s.weightedChannels {
   280  		weightedChannel.SetWeight(s.options.ChannelWeightFn(channelKey))
   281  	}
   282  }
   283  
   284  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) dispatchTasksWithWeight() {
   285  	for s.hasRemainingTasks() {
   286  		if s.receiveWeightUpdateNotification() {
   287  			s.Lock()
   288  			s.updateChannelWeightLocked()
   289  			s.flattenWeightedChannelsLocked()
   290  			s.Unlock()
   291  		}
   292  
   293  		weightedChannels := s.channels()
   294  		s.doDispatchTasksWithWeight(weightedChannels)
   295  	}
   296  }
   297  
   298  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) doDispatchTasksWithWeight(
   299  	channels WeightedChannels[T],
   300  ) {
   301  	numTasks := int64(0)
   302  LoopDispatch:
   303  	for _, channel := range channels {
   304  		select {
   305  		case task := <-channel.Chan():
   306  			s.fifoScheduler.Submit(task)
   307  			numTasks++
   308  		default:
   309  			continue LoopDispatch
   310  		}
   311  	}
   312  	atomic.AddInt64(&s.numInflightTask, -numTasks)
   313  }
   314  
   315  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) doDispatchTaskDirectly(
   316  	task T,
   317  ) {
   318  	s.fifoScheduler.Submit(task)
   319  	atomic.AddInt64(&s.numInflightTask, -1)
   320  }
   321  
   322  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) tryDispatchTaskDirectly(
   323  	task T,
   324  ) bool {
   325  	dispatched := s.fifoScheduler.TrySubmit(task)
   326  	if dispatched {
   327  		atomic.AddInt64(&s.numInflightTask, -1)
   328  	}
   329  	return dispatched
   330  }
   331  
   332  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) hasRemainingTasks() bool {
   333  	numTasks := atomic.LoadInt64(&s.numInflightTask)
   334  	return numTasks > 0
   335  }
   336  
   337  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) abortTasks() {
   338  	s.RLock()
   339  	defer s.RUnlock()
   340  
   341  	numTasks := int64(0)
   342  DrainLoop:
   343  	for _, channel := range s.weightedChannels {
   344  		for {
   345  			select {
   346  			case task := <-channel.Chan():
   347  				task.Abort()
   348  				numTasks++
   349  			default:
   350  				continue DrainLoop
   351  			}
   352  		}
   353  	}
   354  	atomic.AddInt64(&s.numInflightTask, -numTasks)
   355  }
   356  
   357  func (s *InterleavedWeightedRoundRobinScheduler[T, K]) isStopped() bool {
   358  	return atomic.LoadInt32(&s.status) == common.DaemonStatusStopped
   359  }