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