github.com/cryptogateway/go-paymex@v0.0.0-20210204174735-96277fb1e602/les/servingqueue.go (about) 1 // Copyright 2019 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 package les 18 19 import ( 20 "sort" 21 "sync" 22 "sync/atomic" 23 24 "github.com/cryptogateway/go-paymex/common/mclock" 25 "github.com/cryptogateway/go-paymex/common/prque" 26 ) 27 28 // servingQueue allows running tasks in a limited number of threads and puts the 29 // waiting tasks in a priority queue 30 type servingQueue struct { 31 recentTime, queuedTime, servingTimeDiff uint64 32 burstLimit, burstDropLimit uint64 33 burstDecRate float64 34 lastUpdate mclock.AbsTime 35 36 queueAddCh, queueBestCh chan *servingTask 37 stopThreadCh, quit chan struct{} 38 setThreadsCh chan int 39 40 wg sync.WaitGroup 41 threadCount int // number of currently running threads 42 queue *prque.Prque // priority queue for waiting or suspended tasks 43 best *servingTask // the highest priority task (not included in the queue) 44 suspendBias int64 // priority bias against suspending an already running task 45 } 46 47 // servingTask represents a request serving task. Tasks can be implemented to 48 // run in multiple steps, allowing the serving queue to suspend execution between 49 // steps if higher priority tasks are entered. The creator of the task should 50 // set the following fields: 51 // 52 // - priority: greater value means higher priority; values can wrap around the int64 range 53 // - run: execute a single step; return true if finished 54 // - after: executed after run finishes or returns an error, receives the total serving time 55 type servingTask struct { 56 sq *servingQueue 57 servingTime, timeAdded, maxTime, expTime uint64 58 peer *clientPeer 59 priority int64 60 biasAdded bool 61 token runToken 62 tokenCh chan runToken 63 } 64 65 // runToken received by servingTask.start allows the task to run. Closing the 66 // channel by servingTask.stop signals the thread controller to allow a new task 67 // to start running. 68 type runToken chan struct{} 69 70 // start blocks until the task can start and returns true if it is allowed to run. 71 // Returning false means that the task should be cancelled. 72 func (t *servingTask) start() bool { 73 if t.peer.isFrozen() { 74 return false 75 } 76 t.tokenCh = make(chan runToken, 1) 77 select { 78 case t.sq.queueAddCh <- t: 79 case <-t.sq.quit: 80 return false 81 } 82 select { 83 case t.token = <-t.tokenCh: 84 case <-t.sq.quit: 85 return false 86 } 87 if t.token == nil { 88 return false 89 } 90 t.servingTime -= uint64(mclock.Now()) 91 return true 92 } 93 94 // done signals the thread controller about the task being finished and returns 95 // the total serving time of the task in nanoseconds. 96 func (t *servingTask) done() uint64 { 97 t.servingTime += uint64(mclock.Now()) 98 close(t.token) 99 diff := t.servingTime - t.timeAdded 100 t.timeAdded = t.servingTime 101 if t.expTime > diff { 102 t.expTime -= diff 103 atomic.AddUint64(&t.sq.servingTimeDiff, t.expTime) 104 } else { 105 t.expTime = 0 106 } 107 return t.servingTime 108 } 109 110 // waitOrStop can be called during the execution of the task. It blocks if there 111 // is a higher priority task waiting (a bias is applied in favor of the currently 112 // running task). Returning true means that the execution can be resumed. False 113 // means the task should be cancelled. 114 func (t *servingTask) waitOrStop() bool { 115 t.done() 116 if !t.biasAdded { 117 t.priority += t.sq.suspendBias 118 t.biasAdded = true 119 } 120 return t.start() 121 } 122 123 // newServingQueue returns a new servingQueue 124 func newServingQueue(suspendBias int64, utilTarget float64) *servingQueue { 125 sq := &servingQueue{ 126 queue: prque.New(nil), 127 suspendBias: suspendBias, 128 queueAddCh: make(chan *servingTask, 100), 129 queueBestCh: make(chan *servingTask), 130 stopThreadCh: make(chan struct{}), 131 quit: make(chan struct{}), 132 setThreadsCh: make(chan int, 10), 133 burstLimit: uint64(utilTarget * bufLimitRatio * 1200000), 134 burstDropLimit: uint64(utilTarget * bufLimitRatio * 1000000), 135 burstDecRate: utilTarget, 136 lastUpdate: mclock.Now(), 137 } 138 sq.wg.Add(2) 139 go sq.queueLoop() 140 go sq.threadCountLoop() 141 return sq 142 } 143 144 // newTask creates a new task with the given priority 145 func (sq *servingQueue) newTask(peer *clientPeer, maxTime uint64, priority int64) *servingTask { 146 return &servingTask{ 147 sq: sq, 148 peer: peer, 149 maxTime: maxTime, 150 expTime: maxTime, 151 priority: priority, 152 } 153 } 154 155 // threadController is started in multiple goroutines and controls the execution 156 // of tasks. The number of active thread controllers equals the allowed number of 157 // concurrently running threads. It tries to fetch the highest priority queued 158 // task first. If there are no queued tasks waiting then it can directly catch 159 // run tokens from the token channel and allow the corresponding tasks to run 160 // without entering the priority queue. 161 func (sq *servingQueue) threadController() { 162 for { 163 token := make(runToken) 164 select { 165 case best := <-sq.queueBestCh: 166 best.tokenCh <- token 167 case <-sq.stopThreadCh: 168 sq.wg.Done() 169 return 170 case <-sq.quit: 171 sq.wg.Done() 172 return 173 } 174 <-token 175 select { 176 case <-sq.stopThreadCh: 177 sq.wg.Done() 178 return 179 case <-sq.quit: 180 sq.wg.Done() 181 return 182 default: 183 } 184 } 185 } 186 187 type ( 188 // peerTasks lists the tasks received from a given peer when selecting peers to freeze 189 peerTasks struct { 190 peer *clientPeer 191 list []*servingTask 192 sumTime uint64 193 priority float64 194 } 195 // peerList is a sortable list of peerTasks 196 peerList []*peerTasks 197 ) 198 199 func (l peerList) Len() int { 200 return len(l) 201 } 202 203 func (l peerList) Less(i, j int) bool { 204 return l[i].priority < l[j].priority 205 } 206 207 func (l peerList) Swap(i, j int) { 208 l[i], l[j] = l[j], l[i] 209 } 210 211 // freezePeers selects the peers with the worst priority queued tasks and freezes 212 // them until burstTime goes under burstDropLimit or all peers are frozen 213 func (sq *servingQueue) freezePeers() { 214 peerMap := make(map[*clientPeer]*peerTasks) 215 var peerList peerList 216 if sq.best != nil { 217 sq.queue.Push(sq.best, sq.best.priority) 218 } 219 sq.best = nil 220 for sq.queue.Size() > 0 { 221 task := sq.queue.PopItem().(*servingTask) 222 tasks := peerMap[task.peer] 223 if tasks == nil { 224 bufValue, bufLimit := task.peer.fcClient.BufferStatus() 225 if bufLimit < 1 { 226 bufLimit = 1 227 } 228 tasks = &peerTasks{ 229 peer: task.peer, 230 priority: float64(bufValue) / float64(bufLimit), // lower value comes first 231 } 232 peerMap[task.peer] = tasks 233 peerList = append(peerList, tasks) 234 } 235 tasks.list = append(tasks.list, task) 236 tasks.sumTime += task.expTime 237 } 238 sort.Sort(peerList) 239 drop := true 240 for _, tasks := range peerList { 241 if drop { 242 tasks.peer.freeze() 243 tasks.peer.fcClient.Freeze() 244 sq.queuedTime -= tasks.sumTime 245 sqQueuedGauge.Update(int64(sq.queuedTime)) 246 clientFreezeMeter.Mark(1) 247 drop = sq.recentTime+sq.queuedTime > sq.burstDropLimit 248 for _, task := range tasks.list { 249 task.tokenCh <- nil 250 } 251 } else { 252 for _, task := range tasks.list { 253 sq.queue.Push(task, task.priority) 254 } 255 } 256 } 257 if sq.queue.Size() > 0 { 258 sq.best = sq.queue.PopItem().(*servingTask) 259 } 260 } 261 262 // updateRecentTime recalculates the recent serving time value 263 func (sq *servingQueue) updateRecentTime() { 264 subTime := atomic.SwapUint64(&sq.servingTimeDiff, 0) 265 now := mclock.Now() 266 dt := now - sq.lastUpdate 267 sq.lastUpdate = now 268 if dt > 0 { 269 subTime += uint64(float64(dt) * sq.burstDecRate) 270 } 271 if sq.recentTime > subTime { 272 sq.recentTime -= subTime 273 } else { 274 sq.recentTime = 0 275 } 276 } 277 278 // addTask inserts a task into the priority queue 279 func (sq *servingQueue) addTask(task *servingTask) { 280 if sq.best == nil { 281 sq.best = task 282 } else if task.priority > sq.best.priority { 283 sq.queue.Push(sq.best, sq.best.priority) 284 sq.best = task 285 } else { 286 sq.queue.Push(task, task.priority) 287 } 288 sq.updateRecentTime() 289 sq.queuedTime += task.expTime 290 sqServedGauge.Update(int64(sq.recentTime)) 291 sqQueuedGauge.Update(int64(sq.queuedTime)) 292 if sq.recentTime+sq.queuedTime > sq.burstLimit { 293 sq.freezePeers() 294 } 295 } 296 297 // queueLoop is an event loop running in a goroutine. It receives tasks from queueAddCh 298 // and always tries to send the highest priority task to queueBestCh. Successfully sent 299 // tasks are removed from the queue. 300 func (sq *servingQueue) queueLoop() { 301 for { 302 if sq.best != nil { 303 expTime := sq.best.expTime 304 select { 305 case task := <-sq.queueAddCh: 306 sq.addTask(task) 307 case sq.queueBestCh <- sq.best: 308 sq.updateRecentTime() 309 sq.queuedTime -= expTime 310 sq.recentTime += expTime 311 sqServedGauge.Update(int64(sq.recentTime)) 312 sqQueuedGauge.Update(int64(sq.queuedTime)) 313 if sq.queue.Size() == 0 { 314 sq.best = nil 315 } else { 316 sq.best, _ = sq.queue.PopItem().(*servingTask) 317 } 318 case <-sq.quit: 319 sq.wg.Done() 320 return 321 } 322 } else { 323 select { 324 case task := <-sq.queueAddCh: 325 sq.addTask(task) 326 case <-sq.quit: 327 sq.wg.Done() 328 return 329 } 330 } 331 } 332 } 333 334 // threadCountLoop is an event loop running in a goroutine. It adjusts the number 335 // of active thread controller goroutines. 336 func (sq *servingQueue) threadCountLoop() { 337 var threadCountTarget int 338 for { 339 for threadCountTarget > sq.threadCount { 340 sq.wg.Add(1) 341 go sq.threadController() 342 sq.threadCount++ 343 } 344 if threadCountTarget < sq.threadCount { 345 select { 346 case threadCountTarget = <-sq.setThreadsCh: 347 case sq.stopThreadCh <- struct{}{}: 348 sq.threadCount-- 349 case <-sq.quit: 350 sq.wg.Done() 351 return 352 } 353 } else { 354 select { 355 case threadCountTarget = <-sq.setThreadsCh: 356 case <-sq.quit: 357 sq.wg.Done() 358 return 359 } 360 } 361 } 362 } 363 364 // setThreads sets the allowed processing thread count, suspending tasks as soon as 365 // possible if necessary. 366 func (sq *servingQueue) setThreads(threadCount int) { 367 select { 368 case sq.setThreadsCh <- threadCount: 369 case <-sq.quit: 370 return 371 } 372 } 373 374 // stop stops task processing as soon as possible and shuts down the serving queue. 375 func (sq *servingQueue) stop() { 376 close(sq.quit) 377 sq.wg.Wait() 378 }