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  }