github.com/daeglee/go-ethereum@v0.0.0-20190504220456-cad3e8d18e9b/les/flowcontrol/manager.go (about)

     1  // Copyright 2018 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 flowcontrol
    18  
    19  import (
    20  	"fmt"
    21  	"math"
    22  	"sync"
    23  	"time"
    24  
    25  	"github.com/ethereum/go-ethereum/common/mclock"
    26  	"github.com/ethereum/go-ethereum/common/prque"
    27  )
    28  
    29  // cmNodeFields are ClientNode fields used by the client manager
    30  // Note: these fields are locked by the client manager's mutex
    31  type cmNodeFields struct {
    32  	corrBufValue   int64 // buffer value adjusted with the extra recharge amount
    33  	rcLastIntValue int64 // past recharge integrator value when corrBufValue was last updated
    34  	rcFullIntValue int64 // future recharge integrator value when corrBufValue will reach maximum
    35  	queueIndex     int   // position in the recharge queue (-1 if not queued)
    36  }
    37  
    38  // FixedPointMultiplier is applied to the recharge integrator and the recharge curve.
    39  //
    40  // Note: fixed point arithmetic is required for the integrator because it is a
    41  // constantly increasing value that can wrap around int64 limits (which behavior is
    42  // also supported by the priority queue). A floating point value would gradually lose
    43  // precision in this application.
    44  // The recharge curve and all recharge values are encoded as fixed point because
    45  // sumRecharge is frequently updated by adding or subtracting individual recharge
    46  // values and perfect precision is required.
    47  const FixedPointMultiplier = 1000000
    48  
    49  var (
    50  	capFactorDropTC         = 1 / float64(time.Second*10) // time constant for dropping the capacity factor
    51  	capFactorRaiseTC        = 1 / float64(time.Hour)      // time constant for raising the capacity factor
    52  	capFactorRaiseThreshold = 0.75                        // connected / total capacity ratio threshold for raising the capacity factor
    53  )
    54  
    55  // ClientManager controls the capacity assigned to the clients of a server.
    56  // Since ServerParams guarantee a safe lower estimate for processable requests
    57  // even in case of all clients being active, ClientManager calculates a
    58  // corrigated buffer value and usually allows a higher remaining buffer value
    59  // to be returned with each reply.
    60  type ClientManager struct {
    61  	clock     mclock.Clock
    62  	lock      sync.Mutex
    63  	enabledCh chan struct{}
    64  
    65  	curve                                      PieceWiseLinear
    66  	sumRecharge, totalRecharge, totalConnected uint64
    67  	capLogFactor, totalCapacity                float64
    68  	capLastUpdate                              mclock.AbsTime
    69  	totalCapacityCh                            chan uint64
    70  
    71  	// recharge integrator is increasing in each moment with a rate of
    72  	// (totalRecharge / sumRecharge)*FixedPointMultiplier or 0 if sumRecharge==0
    73  	rcLastUpdate   mclock.AbsTime // last time the recharge integrator was updated
    74  	rcLastIntValue int64          // last updated value of the recharge integrator
    75  	// recharge queue is a priority queue with currently recharging client nodes
    76  	// as elements. The priority value is rcFullIntValue which allows to quickly
    77  	// determine which client will first finish recharge.
    78  	rcQueue *prque.Prque
    79  }
    80  
    81  // NewClientManager returns a new client manager.
    82  // Client manager enhances flow control performance by allowing client buffers
    83  // to recharge quicker than the minimum guaranteed recharge rate if possible.
    84  // The sum of all minimum recharge rates (sumRecharge) is updated each time
    85  // a clients starts or finishes buffer recharging. Then an adjusted total
    86  // recharge rate is calculated using a piecewise linear recharge curve:
    87  //
    88  // totalRecharge = curve(sumRecharge)
    89  // (totalRecharge >= sumRecharge is enforced)
    90  //
    91  // Then the "bonus" buffer recharge is distributed between currently recharging
    92  // clients proportionally to their minimum recharge rates.
    93  //
    94  // Note: total recharge is proportional to the average number of parallel running
    95  // serving threads. A recharge value of 1000000 corresponds to one thread in average.
    96  // The maximum number of allowed serving threads should always be considerably
    97  // higher than the targeted average number.
    98  //
    99  // Note 2: although it is possible to specify a curve allowing the total target
   100  // recharge starting from zero sumRecharge, it makes sense to add a linear ramp
   101  // starting from zero in order to not let a single low-priority client use up
   102  // the entire server capacity and thus ensure quick availability for others at
   103  // any moment.
   104  func NewClientManager(curve PieceWiseLinear, clock mclock.Clock) *ClientManager {
   105  	cm := &ClientManager{
   106  		clock:         clock,
   107  		rcQueue:       prque.New(func(a interface{}, i int) { a.(*ClientNode).queueIndex = i }),
   108  		capLastUpdate: clock.Now(),
   109  	}
   110  	if curve != nil {
   111  		cm.SetRechargeCurve(curve)
   112  	}
   113  	return cm
   114  }
   115  
   116  // SetRechargeCurve updates the recharge curve
   117  func (cm *ClientManager) SetRechargeCurve(curve PieceWiseLinear) {
   118  	cm.lock.Lock()
   119  	defer cm.lock.Unlock()
   120  
   121  	now := cm.clock.Now()
   122  	cm.updateRecharge(now)
   123  	cm.updateCapFactor(now, false)
   124  	cm.curve = curve
   125  	if len(curve) > 0 {
   126  		cm.totalRecharge = curve[len(curve)-1].Y
   127  	} else {
   128  		cm.totalRecharge = 0
   129  	}
   130  	cm.refreshCapacity()
   131  }
   132  
   133  // connect should be called when a client is connected, before passing it to any
   134  // other ClientManager function
   135  func (cm *ClientManager) connect(node *ClientNode) {
   136  	cm.lock.Lock()
   137  	defer cm.lock.Unlock()
   138  
   139  	now := cm.clock.Now()
   140  	cm.updateRecharge(now)
   141  	node.corrBufValue = int64(node.params.BufLimit)
   142  	node.rcLastIntValue = cm.rcLastIntValue
   143  	node.queueIndex = -1
   144  	cm.updateCapFactor(now, true)
   145  	cm.totalConnected += node.params.MinRecharge
   146  }
   147  
   148  // disconnect should be called when a client is disconnected
   149  func (cm *ClientManager) disconnect(node *ClientNode) {
   150  	cm.lock.Lock()
   151  	defer cm.lock.Unlock()
   152  
   153  	now := cm.clock.Now()
   154  	cm.updateRecharge(cm.clock.Now())
   155  	cm.updateCapFactor(now, true)
   156  	cm.totalConnected -= node.params.MinRecharge
   157  }
   158  
   159  // accepted is called when a request with given maximum cost is accepted.
   160  // It returns a priority indicator for the request which is used to determine placement
   161  // in the serving queue. Older requests have higher priority by default. If the client
   162  // is almost out of buffer, request priority is reduced.
   163  func (cm *ClientManager) accepted(node *ClientNode, maxCost uint64, now mclock.AbsTime) (priority int64) {
   164  	cm.lock.Lock()
   165  	defer cm.lock.Unlock()
   166  
   167  	cm.updateNodeRc(node, -int64(maxCost), &node.params, now)
   168  	rcTime := (node.params.BufLimit - uint64(node.corrBufValue)) * FixedPointMultiplier / node.params.MinRecharge
   169  	return -int64(now) - int64(rcTime)
   170  }
   171  
   172  // processed updates the client buffer according to actual request cost after
   173  // serving has been finished.
   174  //
   175  // Note: processed should always be called for all accepted requests
   176  func (cm *ClientManager) processed(node *ClientNode, maxCost, realCost uint64, now mclock.AbsTime) {
   177  	cm.lock.Lock()
   178  	defer cm.lock.Unlock()
   179  
   180  	if realCost > maxCost {
   181  		realCost = maxCost
   182  	}
   183  	cm.updateNodeRc(node, int64(maxCost-realCost), &node.params, now)
   184  	if uint64(node.corrBufValue) > node.bufValue {
   185  		if node.log != nil {
   186  			node.log.add(now, fmt.Sprintf("corrected  bv=%d  oldBv=%d", node.corrBufValue, node.bufValue))
   187  		}
   188  		node.bufValue = uint64(node.corrBufValue)
   189  	}
   190  }
   191  
   192  // updateParams updates the flow control parameters of a client node
   193  func (cm *ClientManager) updateParams(node *ClientNode, params ServerParams, now mclock.AbsTime) {
   194  	cm.lock.Lock()
   195  	defer cm.lock.Unlock()
   196  
   197  	cm.updateRecharge(now)
   198  	cm.updateCapFactor(now, true)
   199  	cm.totalConnected += params.MinRecharge - node.params.MinRecharge
   200  	cm.updateNodeRc(node, 0, &params, now)
   201  }
   202  
   203  // updateRecharge updates the recharge integrator and checks the recharge queue
   204  // for nodes with recently filled buffers
   205  func (cm *ClientManager) updateRecharge(now mclock.AbsTime) {
   206  	lastUpdate := cm.rcLastUpdate
   207  	cm.rcLastUpdate = now
   208  	// updating is done in multiple steps if node buffers are filled and sumRecharge
   209  	// is decreased before the given target time
   210  	for cm.sumRecharge > 0 {
   211  		bonusRatio := cm.curve.ValueAt(cm.sumRecharge) / float64(cm.sumRecharge)
   212  		if bonusRatio < 1 {
   213  			bonusRatio = 1
   214  		}
   215  		dt := now - lastUpdate
   216  		// fetch the client that finishes first
   217  		rcqNode := cm.rcQueue.PopItem().(*ClientNode) // if sumRecharge > 0 then the queue cannot be empty
   218  		// check whether it has already finished
   219  		dtNext := mclock.AbsTime(float64(rcqNode.rcFullIntValue-cm.rcLastIntValue) / bonusRatio)
   220  		if dt < dtNext {
   221  			// not finished yet, put it back, update integrator according
   222  			// to current bonusRatio and return
   223  			cm.rcQueue.Push(rcqNode, -rcqNode.rcFullIntValue)
   224  			cm.rcLastIntValue += int64(bonusRatio * float64(dt))
   225  			return
   226  		}
   227  		lastUpdate += dtNext
   228  		// finished recharging, update corrBufValue and sumRecharge if necessary and do next step
   229  		if rcqNode.corrBufValue < int64(rcqNode.params.BufLimit) {
   230  			rcqNode.corrBufValue = int64(rcqNode.params.BufLimit)
   231  			cm.updateCapFactor(lastUpdate, true)
   232  			cm.sumRecharge -= rcqNode.params.MinRecharge
   233  		}
   234  		cm.rcLastIntValue = rcqNode.rcFullIntValue
   235  	}
   236  }
   237  
   238  // updateNodeRc updates a node's corrBufValue and adds an external correction value.
   239  // It also adds or removes the rcQueue entry and updates ServerParams and sumRecharge if necessary.
   240  func (cm *ClientManager) updateNodeRc(node *ClientNode, bvc int64, params *ServerParams, now mclock.AbsTime) {
   241  	cm.updateRecharge(now)
   242  	wasFull := true
   243  	if node.corrBufValue != int64(node.params.BufLimit) {
   244  		wasFull = false
   245  		node.corrBufValue += (cm.rcLastIntValue - node.rcLastIntValue) * int64(node.params.MinRecharge) / FixedPointMultiplier
   246  		if node.corrBufValue > int64(node.params.BufLimit) {
   247  			node.corrBufValue = int64(node.params.BufLimit)
   248  		}
   249  		node.rcLastIntValue = cm.rcLastIntValue
   250  	}
   251  	node.corrBufValue += bvc
   252  	if node.corrBufValue < 0 {
   253  		node.corrBufValue = 0
   254  	}
   255  	diff := int64(params.BufLimit - node.params.BufLimit)
   256  	if diff > 0 {
   257  		node.corrBufValue += diff
   258  	}
   259  	isFull := false
   260  	if node.corrBufValue >= int64(params.BufLimit) {
   261  		node.corrBufValue = int64(params.BufLimit)
   262  		isFull = true
   263  	}
   264  	sumRecharge := cm.sumRecharge
   265  	if !wasFull {
   266  		sumRecharge -= node.params.MinRecharge
   267  	}
   268  	if params != &node.params {
   269  		node.params = *params
   270  	}
   271  	if !isFull {
   272  		sumRecharge += node.params.MinRecharge
   273  		if node.queueIndex != -1 {
   274  			cm.rcQueue.Remove(node.queueIndex)
   275  		}
   276  		node.rcLastIntValue = cm.rcLastIntValue
   277  		node.rcFullIntValue = cm.rcLastIntValue + (int64(node.params.BufLimit)-node.corrBufValue)*FixedPointMultiplier/int64(node.params.MinRecharge)
   278  		cm.rcQueue.Push(node, -node.rcFullIntValue)
   279  	}
   280  	if sumRecharge != cm.sumRecharge {
   281  		cm.updateCapFactor(now, true)
   282  		cm.sumRecharge = sumRecharge
   283  	}
   284  
   285  }
   286  
   287  // updateCapFactor updates the total capacity factor. The capacity factor allows
   288  // the total capacity of the system to go over the allowed total recharge value
   289  // if the sum of momentarily recharging clients only exceeds the total recharge
   290  // allowance in a very small fraction of time.
   291  // The capacity factor is dropped quickly (with a small time constant) if sumRecharge
   292  // exceeds totalRecharge. It is raised slowly (with a large time constant) if most
   293  // of the total capacity is used by connected clients (totalConnected is larger than
   294  // totalCapacity*capFactorRaiseThreshold) and sumRecharge stays under
   295  // totalRecharge*totalConnected/totalCapacity.
   296  func (cm *ClientManager) updateCapFactor(now mclock.AbsTime, refresh bool) {
   297  	if cm.totalRecharge == 0 {
   298  		return
   299  	}
   300  	dt := now - cm.capLastUpdate
   301  	cm.capLastUpdate = now
   302  
   303  	var d float64
   304  	if cm.sumRecharge > cm.totalRecharge {
   305  		d = (1 - float64(cm.sumRecharge)/float64(cm.totalRecharge)) * capFactorDropTC
   306  	} else {
   307  		totalConnected := float64(cm.totalConnected)
   308  		var connRatio float64
   309  		if totalConnected < cm.totalCapacity {
   310  			connRatio = totalConnected / cm.totalCapacity
   311  		} else {
   312  			connRatio = 1
   313  		}
   314  		if connRatio > capFactorRaiseThreshold {
   315  			sumRecharge := float64(cm.sumRecharge)
   316  			limit := float64(cm.totalRecharge) * connRatio
   317  			if sumRecharge < limit {
   318  				d = (1 - sumRecharge/limit) * (connRatio - capFactorRaiseThreshold) * (1 / (1 - capFactorRaiseThreshold)) * capFactorRaiseTC
   319  			}
   320  		}
   321  	}
   322  	if d != 0 {
   323  		cm.capLogFactor += d * float64(dt)
   324  		if cm.capLogFactor < 0 {
   325  			cm.capLogFactor = 0
   326  		}
   327  		if refresh {
   328  			cm.refreshCapacity()
   329  		}
   330  	}
   331  }
   332  
   333  // refreshCapacity recalculates the total capacity value and sends an update to the subscription
   334  // channel if the relative change of the value since the last update is more than 0.1 percent
   335  func (cm *ClientManager) refreshCapacity() {
   336  	totalCapacity := float64(cm.totalRecharge) * math.Exp(cm.capLogFactor)
   337  	if totalCapacity >= cm.totalCapacity*0.999 && totalCapacity <= cm.totalCapacity*1.001 {
   338  		return
   339  	}
   340  	cm.totalCapacity = totalCapacity
   341  	if cm.totalCapacityCh != nil {
   342  		select {
   343  		case cm.totalCapacityCh <- uint64(cm.totalCapacity):
   344  		default:
   345  		}
   346  	}
   347  }
   348  
   349  // SubscribeTotalCapacity returns all future updates to the total capacity value
   350  // through a channel and also returns the current value
   351  func (cm *ClientManager) SubscribeTotalCapacity(ch chan uint64) uint64 {
   352  	cm.lock.Lock()
   353  	defer cm.lock.Unlock()
   354  
   355  	cm.totalCapacityCh = ch
   356  	return uint64(cm.totalCapacity)
   357  }
   358  
   359  // PieceWiseLinear is used to describe recharge curves
   360  type PieceWiseLinear []struct{ X, Y uint64 }
   361  
   362  // ValueAt returns the curve's value at a given point
   363  func (pwl PieceWiseLinear) ValueAt(x uint64) float64 {
   364  	l := 0
   365  	h := len(pwl)
   366  	if h == 0 {
   367  		return 0
   368  	}
   369  	for h != l {
   370  		m := (l + h) / 2
   371  		if x > pwl[m].X {
   372  			l = m + 1
   373  		} else {
   374  			h = m
   375  		}
   376  	}
   377  	if l == 0 {
   378  		return float64(pwl[0].Y)
   379  	}
   380  	l--
   381  	if h == len(pwl) {
   382  		return float64(pwl[l].Y)
   383  	}
   384  	dx := pwl[h].X - pwl[l].X
   385  	if dx < 1 {
   386  		return float64(pwl[l].Y)
   387  	}
   388  	return float64(pwl[l].Y) + float64(pwl[h].Y-pwl[l].Y)*float64(x-pwl[l].X)/float64(dx)
   389  }
   390  
   391  // Valid returns true if the X coordinates of the curve points are non-strictly monotonic
   392  func (pwl PieceWiseLinear) Valid() bool {
   393  	var lastX uint64
   394  	for _, i := range pwl {
   395  		if i.X < lastX {
   396  			return false
   397  		}
   398  		lastX = i.X
   399  	}
   400  	return true
   401  }