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 }