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