github.com/aigarnetwork/aigar@v0.0.0-20191115204914-d59a6eb70f8e/les/flowcontrol/manager.go (about)

     1  //  Copyright 2018 The go-ethereum Authors
     2  //  Copyright 2019 The go-aigar Authors
     3  //  This file is part of the go-aigar library.
     4  //
     5  //  The go-aigar library is free software: you can redistribute it and/or modify
     6  //  it under the terms of the GNU Lesser General Public License as published by
     7  //  the Free Software Foundation, either version 3 of the License, or
     8  //  (at your option) any later version.
     9  //
    10  //  The go-aigar library is distributed in the hope that it will be useful,
    11  //  but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    13  //  GNU Lesser General Public License for more details.
    14  //
    15  //  You should have received a copy of the GNU Lesser General Public License
    16  //  along with the go-aigar library. If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package flowcontrol
    19  
    20  import (
    21  	"fmt"
    22  	"math"
    23  	"sync"
    24  	"time"
    25  
    26  	"github.com/AigarNetwork/aigar/common/mclock"
    27  	"github.com/AigarNetwork/aigar/common/prque"
    28  )
    29  
    30  // cmNodeFields are ClientNode fields used by the client manager
    31  // Note: these fields are locked by the client manager's mutex
    32  type cmNodeFields struct {
    33  	corrBufValue   int64 // buffer value adjusted with the extra recharge amount
    34  	rcLastIntValue int64 // past recharge integrator value when corrBufValue was last updated
    35  	rcFullIntValue int64 // future recharge integrator value when corrBufValue will reach maximum
    36  	queueIndex     int   // position in the recharge queue (-1 if not queued)
    37  }
    38  
    39  // FixedPointMultiplier is applied to the recharge integrator and the recharge curve.
    40  //
    41  // Note: fixed point arithmetic is required for the integrator because it is a
    42  // constantly increasing value that can wrap around int64 limits (which behavior is
    43  // also supported by the priority queue). A floating point value would gradually lose
    44  // precision in this application.
    45  // The recharge curve and all recharge values are encoded as fixed point because
    46  // sumRecharge is frequently updated by adding or subtracting individual recharge
    47  // values and perfect precision is required.
    48  const FixedPointMultiplier = 1000000
    49  
    50  var (
    51  	capacityDropFactor          = 0.1
    52  	capacityRaiseTC             = 1 / (3 * float64(time.Hour)) // time constant for raising the capacity factor
    53  	capacityRaiseThresholdRatio = 1.125                        // total/connected capacity ratio threshold for raising the capacity factor
    54  )
    55  
    56  // ClientManager controls the capacity assigned to the clients of a server.
    57  // Since ServerParams guarantee a safe lower estimate for processable requests
    58  // even in case of all clients being active, ClientManager calculates a
    59  // corrigated buffer value and usually allows a higher remaining buffer value
    60  // to be returned with each reply.
    61  type ClientManager struct {
    62  	clock     mclock.Clock
    63  	lock      sync.Mutex
    64  	enabledCh chan struct{}
    65  	stop      chan chan struct{}
    66  
    67  	curve                                      PieceWiseLinear
    68  	sumRecharge, totalRecharge, totalConnected uint64
    69  	logTotalCap, totalCapacity                 float64
    70  	logTotalCapRaiseLimit                      float64
    71  	minLogTotalCap, maxLogTotalCap             float64
    72  	capacityRaiseThreshold                     uint64
    73  	capLastUpdate                              mclock.AbsTime
    74  	totalCapacityCh                            chan uint64
    75  
    76  	// recharge integrator is increasing in each moment with a rate of
    77  	// (totalRecharge / sumRecharge)*FixedPointMultiplier or 0 if sumRecharge==0
    78  	rcLastUpdate   mclock.AbsTime // last time the recharge integrator was updated
    79  	rcLastIntValue int64          // last updated value of the recharge integrator
    80  	// recharge queue is a priority queue with currently recharging client nodes
    81  	// as elements. The priority value is rcFullIntValue which allows to quickly
    82  	// determine which client will first finish recharge.
    83  	rcQueue *prque.Prque
    84  }
    85  
    86  // NewClientManager returns a new client manager.
    87  // Client manager enhances flow control performance by allowing client buffers
    88  // to recharge quicker than the minimum guaranteed recharge rate if possible.
    89  // The sum of all minimum recharge rates (sumRecharge) is updated each time
    90  // a clients starts or finishes buffer recharging. Then an adjusted total
    91  // recharge rate is calculated using a piecewise linear recharge curve:
    92  //
    93  // totalRecharge = curve(sumRecharge)
    94  // (totalRecharge >= sumRecharge is enforced)
    95  //
    96  // Then the "bonus" buffer recharge is distributed between currently recharging
    97  // clients proportionally to their minimum recharge rates.
    98  //
    99  // Note: total recharge is proportional to the average number of parallel running
   100  // serving threads. A recharge value of 1000000 corresponds to one thread in average.
   101  // The maximum number of allowed serving threads should always be considerably
   102  // higher than the targeted average number.
   103  //
   104  // Note 2: although it is possible to specify a curve allowing the total target
   105  // recharge starting from zero sumRecharge, it makes sense to add a linear ramp
   106  // starting from zero in order to not let a single low-priority client use up
   107  // the entire server capacity and thus ensure quick availability for others at
   108  // any moment.
   109  func NewClientManager(curve PieceWiseLinear, clock mclock.Clock) *ClientManager {
   110  	cm := &ClientManager{
   111  		clock:         clock,
   112  		rcQueue:       prque.New(func(a interface{}, i int) { a.(*ClientNode).queueIndex = i }),
   113  		capLastUpdate: clock.Now(),
   114  		stop:          make(chan chan struct{}),
   115  	}
   116  	if curve != nil {
   117  		cm.SetRechargeCurve(curve)
   118  	}
   119  	go func() {
   120  		// regularly recalculate and update total capacity
   121  		for {
   122  			select {
   123  			case <-time.After(time.Minute):
   124  				cm.lock.Lock()
   125  				cm.updateTotalCapacity(cm.clock.Now(), true)
   126  				cm.lock.Unlock()
   127  			case stop := <-cm.stop:
   128  				close(stop)
   129  				return
   130  			}
   131  		}
   132  	}()
   133  	return cm
   134  }
   135  
   136  // Stop stops the client manager
   137  func (cm *ClientManager) Stop() {
   138  	stop := make(chan struct{})
   139  	cm.stop <- stop
   140  	<-stop
   141  }
   142  
   143  // SetRechargeCurve updates the recharge curve
   144  func (cm *ClientManager) SetRechargeCurve(curve PieceWiseLinear) {
   145  	cm.lock.Lock()
   146  	defer cm.lock.Unlock()
   147  
   148  	now := cm.clock.Now()
   149  	cm.updateRecharge(now)
   150  	cm.curve = curve
   151  	if len(curve) > 0 {
   152  		cm.totalRecharge = curve[len(curve)-1].Y
   153  	} else {
   154  		cm.totalRecharge = 0
   155  	}
   156  }
   157  
   158  // SetCapacityRaiseThreshold sets a threshold value used for raising capFactor.
   159  // Either if the difference between total allowed and connected capacity is less
   160  // than this threshold or if their ratio is less than capacityRaiseThresholdRatio
   161  // then capFactor is allowed to slowly raise.
   162  func (cm *ClientManager) SetCapacityLimits(min, max, raiseThreshold uint64) {
   163  	if min < 1 {
   164  		min = 1
   165  	}
   166  	cm.minLogTotalCap = math.Log(float64(min))
   167  	if max < 1 {
   168  		max = 1
   169  	}
   170  	cm.maxLogTotalCap = math.Log(float64(max))
   171  	cm.logTotalCap = cm.maxLogTotalCap
   172  	cm.capacityRaiseThreshold = raiseThreshold
   173  	cm.refreshCapacity()
   174  }
   175  
   176  // connect should be called when a client is connected, before passing it to any
   177  // other ClientManager function
   178  func (cm *ClientManager) connect(node *ClientNode) {
   179  	cm.lock.Lock()
   180  	defer cm.lock.Unlock()
   181  
   182  	now := cm.clock.Now()
   183  	cm.updateRecharge(now)
   184  	node.corrBufValue = int64(node.params.BufLimit)
   185  	node.rcLastIntValue = cm.rcLastIntValue
   186  	node.queueIndex = -1
   187  	cm.updateTotalCapacity(now, true)
   188  	cm.totalConnected += node.params.MinRecharge
   189  	cm.updateRaiseLimit()
   190  }
   191  
   192  // disconnect should be called when a client is disconnected
   193  func (cm *ClientManager) disconnect(node *ClientNode) {
   194  	cm.lock.Lock()
   195  	defer cm.lock.Unlock()
   196  
   197  	now := cm.clock.Now()
   198  	cm.updateRecharge(cm.clock.Now())
   199  	cm.updateTotalCapacity(now, true)
   200  	cm.totalConnected -= node.params.MinRecharge
   201  	cm.updateRaiseLimit()
   202  }
   203  
   204  // accepted is called when a request with given maximum cost is accepted.
   205  // It returns a priority indicator for the request which is used to determine placement
   206  // in the serving queue. Older requests have higher priority by default. If the client
   207  // is almost out of buffer, request priority is reduced.
   208  func (cm *ClientManager) accepted(node *ClientNode, maxCost uint64, now mclock.AbsTime) (priority int64) {
   209  	cm.lock.Lock()
   210  	defer cm.lock.Unlock()
   211  
   212  	cm.updateNodeRc(node, -int64(maxCost), &node.params, now)
   213  	rcTime := (node.params.BufLimit - uint64(node.corrBufValue)) * FixedPointMultiplier / node.params.MinRecharge
   214  	return -int64(now) - int64(rcTime)
   215  }
   216  
   217  // processed updates the client buffer according to actual request cost after
   218  // serving has been finished.
   219  //
   220  // Note: processed should always be called for all accepted requests
   221  func (cm *ClientManager) processed(node *ClientNode, maxCost, realCost uint64, now mclock.AbsTime) {
   222  	if realCost > maxCost {
   223  		realCost = maxCost
   224  	}
   225  	cm.updateBuffer(node, int64(maxCost-realCost), now)
   226  }
   227  
   228  // updateBuffer recalulates the corrected buffer value, adds the given value to it
   229  // and updates the node's actual buffer value if possible
   230  func (cm *ClientManager) updateBuffer(node *ClientNode, add int64, now mclock.AbsTime) {
   231  	cm.lock.Lock()
   232  	defer cm.lock.Unlock()
   233  
   234  	cm.updateNodeRc(node, add, &node.params, now)
   235  	if node.corrBufValue > node.bufValue {
   236  		if node.log != nil {
   237  			node.log.add(now, fmt.Sprintf("corrected  bv=%d  oldBv=%d", node.corrBufValue, node.bufValue))
   238  		}
   239  		node.bufValue = node.corrBufValue
   240  	}
   241  }
   242  
   243  // updateParams updates the flow control parameters of a client node
   244  func (cm *ClientManager) updateParams(node *ClientNode, params ServerParams, now mclock.AbsTime) {
   245  	cm.lock.Lock()
   246  	defer cm.lock.Unlock()
   247  
   248  	cm.updateRecharge(now)
   249  	cm.updateTotalCapacity(now, true)
   250  	cm.totalConnected += params.MinRecharge - node.params.MinRecharge
   251  	cm.updateRaiseLimit()
   252  	cm.updateNodeRc(node, 0, &params, now)
   253  }
   254  
   255  // updateRaiseLimit recalculates the limiting value until which logTotalCap
   256  // can be raised when no client freeze events occur
   257  func (cm *ClientManager) updateRaiseLimit() {
   258  	if cm.capacityRaiseThreshold == 0 {
   259  		cm.logTotalCapRaiseLimit = 0
   260  		return
   261  	}
   262  	limit := float64(cm.totalConnected + cm.capacityRaiseThreshold)
   263  	limit2 := float64(cm.totalConnected) * capacityRaiseThresholdRatio
   264  	if limit2 > limit {
   265  		limit = limit2
   266  	}
   267  	if limit < 1 {
   268  		limit = 1
   269  	}
   270  	cm.logTotalCapRaiseLimit = math.Log(limit)
   271  }
   272  
   273  // updateRecharge updates the recharge integrator and checks the recharge queue
   274  // for nodes with recently filled buffers
   275  func (cm *ClientManager) updateRecharge(now mclock.AbsTime) {
   276  	lastUpdate := cm.rcLastUpdate
   277  	cm.rcLastUpdate = now
   278  	// updating is done in multiple steps if node buffers are filled and sumRecharge
   279  	// is decreased before the given target time
   280  	for cm.sumRecharge > 0 {
   281  		sumRecharge := cm.sumRecharge
   282  		if sumRecharge > cm.totalRecharge {
   283  			sumRecharge = cm.totalRecharge
   284  		}
   285  		bonusRatio := float64(1)
   286  		v := cm.curve.ValueAt(sumRecharge)
   287  		s := float64(sumRecharge)
   288  		if v > s && s > 0 {
   289  			bonusRatio = v / s
   290  		}
   291  		dt := now - lastUpdate
   292  		// fetch the client that finishes first
   293  		rcqNode := cm.rcQueue.PopItem().(*ClientNode) // if sumRecharge > 0 then the queue cannot be empty
   294  		// check whether it has already finished
   295  		dtNext := mclock.AbsTime(float64(rcqNode.rcFullIntValue-cm.rcLastIntValue) / bonusRatio)
   296  		if dt < dtNext {
   297  			// not finished yet, put it back, update integrator according
   298  			// to current bonusRatio and return
   299  			cm.rcQueue.Push(rcqNode, -rcqNode.rcFullIntValue)
   300  			cm.rcLastIntValue += int64(bonusRatio * float64(dt))
   301  			return
   302  		}
   303  		lastUpdate += dtNext
   304  		// finished recharging, update corrBufValue and sumRecharge if necessary and do next step
   305  		if rcqNode.corrBufValue < int64(rcqNode.params.BufLimit) {
   306  			rcqNode.corrBufValue = int64(rcqNode.params.BufLimit)
   307  			cm.sumRecharge -= rcqNode.params.MinRecharge
   308  		}
   309  		cm.rcLastIntValue = rcqNode.rcFullIntValue
   310  	}
   311  }
   312  
   313  // updateNodeRc updates a node's corrBufValue and adds an external correction value.
   314  // It also adds or removes the rcQueue entry and updates ServerParams and sumRecharge if necessary.
   315  func (cm *ClientManager) updateNodeRc(node *ClientNode, bvc int64, params *ServerParams, now mclock.AbsTime) {
   316  	cm.updateRecharge(now)
   317  	wasFull := true
   318  	if node.corrBufValue != int64(node.params.BufLimit) {
   319  		wasFull = false
   320  		node.corrBufValue += (cm.rcLastIntValue - node.rcLastIntValue) * int64(node.params.MinRecharge) / FixedPointMultiplier
   321  		if node.corrBufValue > int64(node.params.BufLimit) {
   322  			node.corrBufValue = int64(node.params.BufLimit)
   323  		}
   324  		node.rcLastIntValue = cm.rcLastIntValue
   325  	}
   326  	node.corrBufValue += bvc
   327  	diff := int64(params.BufLimit - node.params.BufLimit)
   328  	if diff > 0 {
   329  		node.corrBufValue += diff
   330  	}
   331  	isFull := false
   332  	if node.corrBufValue >= int64(params.BufLimit) {
   333  		node.corrBufValue = int64(params.BufLimit)
   334  		isFull = true
   335  	}
   336  	if !wasFull {
   337  		cm.sumRecharge -= node.params.MinRecharge
   338  	}
   339  	if params != &node.params {
   340  		node.params = *params
   341  	}
   342  	if !isFull {
   343  		cm.sumRecharge += node.params.MinRecharge
   344  		if node.queueIndex != -1 {
   345  			cm.rcQueue.Remove(node.queueIndex)
   346  		}
   347  		node.rcLastIntValue = cm.rcLastIntValue
   348  		node.rcFullIntValue = cm.rcLastIntValue + (int64(node.params.BufLimit)-node.corrBufValue)*FixedPointMultiplier/int64(node.params.MinRecharge)
   349  		cm.rcQueue.Push(node, -node.rcFullIntValue)
   350  	}
   351  }
   352  
   353  // reduceTotalCapacity reduces the total capacity allowance in case of a client freeze event
   354  func (cm *ClientManager) reduceTotalCapacity(frozenCap uint64) {
   355  	cm.lock.Lock()
   356  	defer cm.lock.Unlock()
   357  
   358  	ratio := float64(1)
   359  	if frozenCap < cm.totalConnected {
   360  		ratio = float64(frozenCap) / float64(cm.totalConnected)
   361  	}
   362  	now := cm.clock.Now()
   363  	cm.updateTotalCapacity(now, false)
   364  	cm.logTotalCap -= capacityDropFactor * ratio
   365  	if cm.logTotalCap < cm.minLogTotalCap {
   366  		cm.logTotalCap = cm.minLogTotalCap
   367  	}
   368  	cm.updateTotalCapacity(now, true)
   369  }
   370  
   371  // updateTotalCapacity updates the total capacity factor. The capacity factor allows
   372  // the total capacity of the system to go over the allowed total recharge value
   373  // if clients go to frozen state sufficiently rarely.
   374  // The capacity factor is dropped instantly by a small amount if a clients is frozen.
   375  // It is raised slowly (with a large time constant) if the total connected capacity
   376  // is close to the total allowed amount and no clients are frozen.
   377  func (cm *ClientManager) updateTotalCapacity(now mclock.AbsTime, refresh bool) {
   378  	dt := now - cm.capLastUpdate
   379  	cm.capLastUpdate = now
   380  
   381  	if cm.logTotalCap < cm.logTotalCapRaiseLimit {
   382  		cm.logTotalCap += capacityRaiseTC * float64(dt)
   383  		if cm.logTotalCap > cm.logTotalCapRaiseLimit {
   384  			cm.logTotalCap = cm.logTotalCapRaiseLimit
   385  		}
   386  	}
   387  	if cm.logTotalCap > cm.maxLogTotalCap {
   388  		cm.logTotalCap = cm.maxLogTotalCap
   389  	}
   390  	if refresh {
   391  		cm.refreshCapacity()
   392  	}
   393  }
   394  
   395  // refreshCapacity recalculates the total capacity value and sends an update to the subscription
   396  // channel if the relative change of the value since the last update is more than 0.1 percent
   397  func (cm *ClientManager) refreshCapacity() {
   398  	totalCapacity := math.Exp(cm.logTotalCap)
   399  	if totalCapacity >= cm.totalCapacity*0.999 && totalCapacity <= cm.totalCapacity*1.001 {
   400  		return
   401  	}
   402  	cm.totalCapacity = totalCapacity
   403  	if cm.totalCapacityCh != nil {
   404  		select {
   405  		case cm.totalCapacityCh <- uint64(cm.totalCapacity):
   406  		default:
   407  		}
   408  	}
   409  }
   410  
   411  // SubscribeTotalCapacity returns all future updates to the total capacity value
   412  // through a channel and also returns the current value
   413  func (cm *ClientManager) SubscribeTotalCapacity(ch chan uint64) uint64 {
   414  	cm.lock.Lock()
   415  	defer cm.lock.Unlock()
   416  
   417  	cm.totalCapacityCh = ch
   418  	return uint64(cm.totalCapacity)
   419  }
   420  
   421  // PieceWiseLinear is used to describe recharge curves
   422  type PieceWiseLinear []struct{ X, Y uint64 }
   423  
   424  // ValueAt returns the curve's value at a given point
   425  func (pwl PieceWiseLinear) ValueAt(x uint64) float64 {
   426  	l := 0
   427  	h := len(pwl)
   428  	if h == 0 {
   429  		return 0
   430  	}
   431  	for h != l {
   432  		m := (l + h) / 2
   433  		if x > pwl[m].X {
   434  			l = m + 1
   435  		} else {
   436  			h = m
   437  		}
   438  	}
   439  	if l == 0 {
   440  		return float64(pwl[0].Y)
   441  	}
   442  	l--
   443  	if h == len(pwl) {
   444  		return float64(pwl[l].Y)
   445  	}
   446  	dx := pwl[h].X - pwl[l].X
   447  	if dx < 1 {
   448  		return float64(pwl[l].Y)
   449  	}
   450  	return float64(pwl[l].Y) + float64(pwl[h].Y-pwl[l].Y)*float64(x-pwl[l].X)/float64(dx)
   451  }
   452  
   453  // Valid returns true if the X coordinates of the curve points are non-strictly monotonic
   454  func (pwl PieceWiseLinear) Valid() bool {
   455  	var lastX uint64
   456  	for _, i := range pwl {
   457  		if i.X < lastX {
   458  			return false
   459  		}
   460  		lastX = i.X
   461  	}
   462  	return true
   463  }