github.com/aidoskuneen/adk-node@v0.0.0-20220315131952-2e32567cb7f4/les/servingqueue.go (about)

     1  // Copyright 2021 The adkgo Authors
     2  // This file is part of the adkgo library (adapted for adkgo from go--ethereum v1.10.8).
     3  //
     4  // the adkgo 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 adkgo 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 adkgo 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/aidoskuneen/adk-node/common/mclock"
    25  	"github.com/aidoskuneen/adk-node/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.NewWrapAround(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  	defer sq.wg.Done()
   163  	for {
   164  		token := make(runToken)
   165  		select {
   166  		case best := <-sq.queueBestCh:
   167  			best.tokenCh <- token
   168  		case <-sq.stopThreadCh:
   169  			return
   170  		case <-sq.quit:
   171  			return
   172  		}
   173  		select {
   174  		case <-sq.stopThreadCh:
   175  			return
   176  		case <-sq.quit:
   177  			return
   178  		case <-token:
   179  		}
   180  	}
   181  }
   182  
   183  type (
   184  	// peerTasks lists the tasks received from a given peer when selecting peers to freeze
   185  	peerTasks struct {
   186  		peer     *clientPeer
   187  		list     []*servingTask
   188  		sumTime  uint64
   189  		priority float64
   190  	}
   191  	// peerList is a sortable list of peerTasks
   192  	peerList []*peerTasks
   193  )
   194  
   195  func (l peerList) Len() int {
   196  	return len(l)
   197  }
   198  
   199  func (l peerList) Less(i, j int) bool {
   200  	return l[i].priority < l[j].priority
   201  }
   202  
   203  func (l peerList) Swap(i, j int) {
   204  	l[i], l[j] = l[j], l[i]
   205  }
   206  
   207  // freezePeers selects the peers with the worst priority queued tasks and freezes
   208  // them until burstTime goes under burstDropLimit or all peers are frozen
   209  func (sq *servingQueue) freezePeers() {
   210  	peerMap := make(map[*clientPeer]*peerTasks)
   211  	var peerList peerList
   212  	if sq.best != nil {
   213  		sq.queue.Push(sq.best, sq.best.priority)
   214  	}
   215  	sq.best = nil
   216  	for sq.queue.Size() > 0 {
   217  		task := sq.queue.PopItem().(*servingTask)
   218  		tasks := peerMap[task.peer]
   219  		if tasks == nil {
   220  			bufValue, bufLimit := task.peer.fcClient.BufferStatus()
   221  			if bufLimit < 1 {
   222  				bufLimit = 1
   223  			}
   224  			tasks = &peerTasks{
   225  				peer:     task.peer,
   226  				priority: float64(bufValue) / float64(bufLimit), // lower value comes first
   227  			}
   228  			peerMap[task.peer] = tasks
   229  			peerList = append(peerList, tasks)
   230  		}
   231  		tasks.list = append(tasks.list, task)
   232  		tasks.sumTime += task.expTime
   233  	}
   234  	sort.Sort(peerList)
   235  	drop := true
   236  	for _, tasks := range peerList {
   237  		if drop {
   238  			tasks.peer.freeze()
   239  			tasks.peer.fcClient.Freeze()
   240  			sq.queuedTime -= tasks.sumTime
   241  			sqQueuedGauge.Update(int64(sq.queuedTime))
   242  			clientFreezeMeter.Mark(1)
   243  			drop = sq.recentTime+sq.queuedTime > sq.burstDropLimit
   244  			for _, task := range tasks.list {
   245  				task.tokenCh <- nil
   246  			}
   247  		} else {
   248  			for _, task := range tasks.list {
   249  				sq.queue.Push(task, task.priority)
   250  			}
   251  		}
   252  	}
   253  	if sq.queue.Size() > 0 {
   254  		sq.best = sq.queue.PopItem().(*servingTask)
   255  	}
   256  }
   257  
   258  // updateRecentTime recalculates the recent serving time value
   259  func (sq *servingQueue) updateRecentTime() {
   260  	subTime := atomic.SwapUint64(&sq.servingTimeDiff, 0)
   261  	now := mclock.Now()
   262  	dt := now - sq.lastUpdate
   263  	sq.lastUpdate = now
   264  	if dt > 0 {
   265  		subTime += uint64(float64(dt) * sq.burstDecRate)
   266  	}
   267  	if sq.recentTime > subTime {
   268  		sq.recentTime -= subTime
   269  	} else {
   270  		sq.recentTime = 0
   271  	}
   272  }
   273  
   274  // addTask inserts a task into the priority queue
   275  func (sq *servingQueue) addTask(task *servingTask) {
   276  	if sq.best == nil {
   277  		sq.best = task
   278  	} else if task.priority-sq.best.priority > 0 {
   279  		sq.queue.Push(sq.best, sq.best.priority)
   280  		sq.best = task
   281  	} else {
   282  		sq.queue.Push(task, task.priority)
   283  	}
   284  	sq.updateRecentTime()
   285  	sq.queuedTime += task.expTime
   286  	sqServedGauge.Update(int64(sq.recentTime))
   287  	sqQueuedGauge.Update(int64(sq.queuedTime))
   288  	if sq.recentTime+sq.queuedTime > sq.burstLimit {
   289  		sq.freezePeers()
   290  	}
   291  }
   292  
   293  // queueLoop is an event loop running in a goroutine. It receives tasks from queueAddCh
   294  // and always tries to send the highest priority task to queueBestCh. Successfully sent
   295  // tasks are removed from the queue.
   296  func (sq *servingQueue) queueLoop() {
   297  	defer sq.wg.Done()
   298  	for {
   299  		if sq.best != nil {
   300  			expTime := sq.best.expTime
   301  			select {
   302  			case task := <-sq.queueAddCh:
   303  				sq.addTask(task)
   304  			case sq.queueBestCh <- sq.best:
   305  				sq.updateRecentTime()
   306  				sq.queuedTime -= expTime
   307  				sq.recentTime += expTime
   308  				sqServedGauge.Update(int64(sq.recentTime))
   309  				sqQueuedGauge.Update(int64(sq.queuedTime))
   310  				if sq.queue.Size() == 0 {
   311  					sq.best = nil
   312  				} else {
   313  					sq.best, _ = sq.queue.PopItem().(*servingTask)
   314  				}
   315  			case <-sq.quit:
   316  				return
   317  			}
   318  		} else {
   319  			select {
   320  			case task := <-sq.queueAddCh:
   321  				sq.addTask(task)
   322  			case <-sq.quit:
   323  				return
   324  			}
   325  		}
   326  	}
   327  }
   328  
   329  // threadCountLoop is an event loop running in a goroutine. It adjusts the number
   330  // of active thread controller goroutines.
   331  func (sq *servingQueue) threadCountLoop() {
   332  	var threadCountTarget int
   333  	defer sq.wg.Done()
   334  	for {
   335  		for threadCountTarget > sq.threadCount {
   336  			sq.wg.Add(1)
   337  			go sq.threadController()
   338  			sq.threadCount++
   339  		}
   340  		if threadCountTarget < sq.threadCount {
   341  			select {
   342  			case threadCountTarget = <-sq.setThreadsCh:
   343  			case sq.stopThreadCh <- struct{}{}:
   344  				sq.threadCount--
   345  			case <-sq.quit:
   346  				return
   347  			}
   348  		} else {
   349  			select {
   350  			case threadCountTarget = <-sq.setThreadsCh:
   351  			case <-sq.quit:
   352  				return
   353  			}
   354  		}
   355  	}
   356  }
   357  
   358  // setThreads sets the allowed processing thread count, suspending tasks as soon as
   359  // possible if necessary.
   360  func (sq *servingQueue) setThreads(threadCount int) {
   361  	select {
   362  	case sq.setThreadsCh <- threadCount:
   363  	case <-sq.quit:
   364  		return
   365  	}
   366  }
   367  
   368  // stop stops task processing as soon as possible and shuts down the serving queue.
   369  func (sq *servingQueue) stop() {
   370  	close(sq.quit)
   371  	sq.wg.Wait()
   372  }