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, ¶ms, 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 }