aboutsummaryrefslogblamecommitdiffstats
path: root/les/servingqueue.go
blob: 8842cf9e9d5530479fec867f2d52f0363c39d7c3 (plain) (tree)
1
                                         

















                                                                                  
              
              
                     







                                                                                




                                                              



















                                                                                            






                                                              









                                                                                  



                                          
                







                                   












                                                                               







                                                                  
















                                                                                 
                                                                           
                            










                                                                             







                                                     
                                                                                          

                             


                                  















                                                                                 





                                       













                                       




















































                                                                                                           




                                                      

                                                                  






























                                                                              






                                                        


                                                  

                                     

                                                  


                                                        







                                                                                       
                                                  



                                                       


                                                        

                                                                          

































































                                                                                      
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package les

import (
    "sort"
    "sync"
    "sync/atomic"

    "github.com/ethereum/go-ethereum/common/mclock"
    "github.com/ethereum/go-ethereum/common/prque"
)

// servingQueue allows running tasks in a limited number of threads and puts the
// waiting tasks in a priority queue
type servingQueue struct {
    recentTime, queuedTime, servingTimeDiff uint64
    burstLimit, burstDropLimit              uint64
    burstDecRate                            float64
    lastUpdate                              mclock.AbsTime

    queueAddCh, queueBestCh chan *servingTask
    stopThreadCh, quit      chan struct{}
    setThreadsCh            chan int

    wg          sync.WaitGroup
    threadCount int          // number of currently running threads
    queue       *prque.Prque // priority queue for waiting or suspended tasks
    best        *servingTask // the highest priority task (not included in the queue)
    suspendBias int64        // priority bias against suspending an already running task
}

// servingTask represents a request serving task. Tasks can be implemented to
// run in multiple steps, allowing the serving queue to suspend execution between
// steps if higher priority tasks are entered. The creator of the task should
// set the following fields:
//
// - priority: greater value means higher priority; values can wrap around the int64 range
// - run: execute a single step; return true if finished
// - after: executed after run finishes or returns an error, receives the total serving time
type servingTask struct {
    sq                                       *servingQueue
    servingTime, timeAdded, maxTime, expTime uint64
    peer                                     *peer
    priority                                 int64
    biasAdded                                bool
    token                                    runToken
    tokenCh                                  chan runToken
}

// runToken received by servingTask.start allows the task to run. Closing the
// channel by servingTask.stop signals the thread controller to allow a new task
// to start running.
type runToken chan struct{}

// start blocks until the task can start and returns true if it is allowed to run.
// Returning false means that the task should be cancelled.
func (t *servingTask) start() bool {
    if t.peer.isFrozen() {
        return false
    }
    t.tokenCh = make(chan runToken, 1)
    select {
    case t.sq.queueAddCh <- t:
    case <-t.sq.quit:
        return false
    }
    select {
    case t.token = <-t.tokenCh:
    case <-t.sq.quit:
        return false
    }
    if t.token == nil {
        return false
    }
    t.servingTime -= uint64(mclock.Now())
    return true
}

// done signals the thread controller about the task being finished and returns
// the total serving time of the task in nanoseconds.
func (t *servingTask) done() uint64 {
    t.servingTime += uint64(mclock.Now())
    close(t.token)
    diff := t.servingTime - t.timeAdded
    t.timeAdded = t.servingTime
    if t.expTime > diff {
        t.expTime -= diff
        atomic.AddUint64(&t.sq.servingTimeDiff, t.expTime)
    } else {
        t.expTime = 0
    }
    return t.servingTime
}

// waitOrStop can be called during the execution of the task. It blocks if there
// is a higher priority task waiting (a bias is applied in favor of the currently
// running task). Returning true means that the execution can be resumed. False
// means the task should be cancelled.
func (t *servingTask) waitOrStop() bool {
    t.done()
    if !t.biasAdded {
        t.priority += t.sq.suspendBias
        t.biasAdded = true
    }
    return t.start()
}

// newServingQueue returns a new servingQueue
func newServingQueue(suspendBias int64, utilTarget float64) *servingQueue {
    sq := &servingQueue{
        queue:          prque.New(nil),
        suspendBias:    suspendBias,
        queueAddCh:     make(chan *servingTask, 100),
        queueBestCh:    make(chan *servingTask),
        stopThreadCh:   make(chan struct{}),
        quit:           make(chan struct{}),
        setThreadsCh:   make(chan int, 10),
        burstLimit:     uint64(utilTarget * bufLimitRatio * 1200000),
        burstDropLimit: uint64(utilTarget * bufLimitRatio * 1000000),
        burstDecRate:   utilTarget,
        lastUpdate:     mclock.Now(),
    }
    sq.wg.Add(2)
    go sq.queueLoop()
    go sq.threadCountLoop()
    return sq
}

// newTask creates a new task with the given priority
func (sq *servingQueue) newTask(peer *peer, maxTime uint64, priority int64) *servingTask {
    return &servingTask{
        sq:       sq,
        peer:     peer,
        maxTime:  maxTime,
        expTime:  maxTime,
        priority: priority,
    }
}

// threadController is started in multiple goroutines and controls the execution
// of tasks. The number of active thread controllers equals the allowed number of
// concurrently running threads. It tries to fetch the highest priority queued
// task first. If there are no queued tasks waiting then it can directly catch
// run tokens from the token channel and allow the corresponding tasks to run
// without entering the priority queue.
func (sq *servingQueue) threadController() {
    for {
        token := make(runToken)
        select {
        case best := <-sq.queueBestCh:
            best.tokenCh <- token
        case <-sq.stopThreadCh:
            sq.wg.Done()
            return
        case <-sq.quit:
            sq.wg.Done()
            return
        }
        <-token
        select {
        case <-sq.stopThreadCh:
            sq.wg.Done()
            return
        case <-sq.quit:
            sq.wg.Done()
            return
        default:
        }
    }
}

type (
    // peerTasks lists the tasks received from a given peer when selecting peers to freeze
    peerTasks struct {
        peer     *peer
        list     []*servingTask
        sumTime  uint64
        priority float64
    }
    // peerList is a sortable list of peerTasks
    peerList []*peerTasks
)

func (l peerList) Len() int {
    return len(l)
}

func (l peerList) Less(i, j int) bool {
    return l[i].priority < l[j].priority
}

func (l peerList) Swap(i, j int) {
    l[i], l[j] = l[j], l[i]
}

// freezePeers selects the peers with the worst priority queued tasks and freezes
// them until burstTime goes under burstDropLimit or all peers are frozen
func (sq *servingQueue) freezePeers() {
    peerMap := make(map[*peer]*peerTasks)
    var peerList peerList
    if sq.best != nil {
        sq.queue.Push(sq.best, sq.best.priority)
    }
    sq.best = nil
    for sq.queue.Size() > 0 {
        task := sq.queue.PopItem().(*servingTask)
        tasks := peerMap[task.peer]
        if tasks == nil {
            bufValue, bufLimit := task.peer.fcClient.BufferStatus()
            if bufLimit < 1 {
                bufLimit = 1
            }
            tasks = &peerTasks{
                peer:     task.peer,
                priority: float64(bufValue) / float64(bufLimit), // lower value comes first
            }
            peerMap[task.peer] = tasks
            peerList = append(peerList, tasks)
        }
        tasks.list = append(tasks.list, task)
        tasks.sumTime += task.expTime
    }
    sort.Sort(peerList)
    drop := true
    for _, tasks := range peerList {
        if drop {
            tasks.peer.freezeClient()
            tasks.peer.fcClient.Freeze()
            sq.queuedTime -= tasks.sumTime
            sqQueuedGauge.Update(int64(sq.queuedTime))
            clientFreezeMeter.Mark(1)
            drop = sq.recentTime+sq.queuedTime > sq.burstDropLimit
            for _, task := range tasks.list {
                task.tokenCh <- nil
            }
        } else {
            for _, task := range tasks.list {
                sq.queue.Push(task, task.priority)
            }
        }
    }
    if sq.queue.Size() > 0 {
        sq.best = sq.queue.PopItem().(*servingTask)
    }
}

// updateRecentTime recalculates the recent serving time value
func (sq *servingQueue) updateRecentTime() {
    subTime := atomic.SwapUint64(&sq.servingTimeDiff, 0)
    now := mclock.Now()
    dt := now - sq.lastUpdate
    sq.lastUpdate = now
    if dt > 0 {
        subTime += uint64(float64(dt) * sq.burstDecRate)
    }
    if sq.recentTime > subTime {
        sq.recentTime -= subTime
    } else {
        sq.recentTime = 0
    }
}

// addTask inserts a task into the priority queue
func (sq *servingQueue) addTask(task *servingTask) {
    if sq.best == nil {
        sq.best = task
    } else if task.priority > sq.best.priority {
        sq.queue.Push(sq.best, sq.best.priority)
        sq.best = task
    } else {
        sq.queue.Push(task, task.priority)
    }
    sq.updateRecentTime()
    sq.queuedTime += task.expTime
    sqServedGauge.Update(int64(sq.recentTime))
    sqQueuedGauge.Update(int64(sq.queuedTime))
    if sq.recentTime+sq.queuedTime > sq.burstLimit {
        sq.freezePeers()
    }
}

// queueLoop is an event loop running in a goroutine. It receives tasks from queueAddCh
// and always tries to send the highest priority task to queueBestCh. Successfully sent
// tasks are removed from the queue.
func (sq *servingQueue) queueLoop() {
    for {
        if sq.best != nil {
            expTime := sq.best.expTime
            select {
            case task := <-sq.queueAddCh:
                sq.addTask(task)
            case sq.queueBestCh <- sq.best:
                sq.updateRecentTime()
                sq.queuedTime -= expTime
                sq.recentTime += expTime
                sqServedGauge.Update(int64(sq.recentTime))
                sqQueuedGauge.Update(int64(sq.queuedTime))
                if sq.queue.Size() == 0 {
                    sq.best = nil
                } else {
                    sq.best, _ = sq.queue.PopItem().(*servingTask)
                }
            case <-sq.quit:
                sq.wg.Done()
                return
            }
        } else {
            select {
            case task := <-sq.queueAddCh:
                sq.addTask(task)
            case <-sq.quit:
                sq.wg.Done()
                return
            }
        }
    }
}

// threadCountLoop is an event loop running in a goroutine. It adjusts the number
// of active thread controller goroutines.
func (sq *servingQueue) threadCountLoop() {
    var threadCountTarget int
    for {
        for threadCountTarget > sq.threadCount {
            sq.wg.Add(1)
            go sq.threadController()
            sq.threadCount++
        }
        if threadCountTarget < sq.threadCount {
            select {
            case threadCountTarget = <-sq.setThreadsCh:
            case sq.stopThreadCh <- struct{}{}:
                sq.threadCount--
            case <-sq.quit:
                sq.wg.Done()
                return
            }
        } else {
            select {
            case threadCountTarget = <-sq.setThreadsCh:
            case <-sq.quit:
                sq.wg.Done()
                return
            }
        }
    }
}

// setThreads sets the allowed processing thread count, suspending tasks as soon as
// possible if necessary.
func (sq *servingQueue) setThreads(threadCount int) {
    select {
    case sq.setThreadsCh <- threadCount:
    case <-sq.quit:
        return
    }
}

// stop stops task processing as soon as possible and shuts down the serving queue.
func (sq *servingQueue) stop() {
    close(sq.quit)
    sq.wg.Wait()
}