github.com/cryptogateway/go-paymex@v0.0.0-20210204174735-96277fb1e602/les/clientpool.go (about) 1 // Copyright 2019 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 les 18 19 import ( 20 "fmt" 21 "sync" 22 "time" 23 24 "github.com/cryptogateway/go-paymex/common/mclock" 25 "github.com/cryptogateway/go-paymex/ethdb" 26 lps "github.com/cryptogateway/go-paymex/les/lespay/server" 27 "github.com/cryptogateway/go-paymex/les/utils" 28 "github.com/cryptogateway/go-paymex/log" 29 "github.com/cryptogateway/go-paymex/p2p/enode" 30 "github.com/cryptogateway/go-paymex/p2p/enr" 31 "github.com/cryptogateway/go-paymex/p2p/nodestate" 32 ) 33 34 const ( 35 defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance 36 37 // defaultConnectedBias is applied to already connected clients So that 38 // already connected client won't be kicked out very soon and we 39 // can ensure all connected clients can have enough time to request 40 // or sync some data. 41 // 42 // todo(rjl493456442) make it configurable. It can be the option of 43 // free trial time! 44 defaultConnectedBias = time.Minute * 3 45 inactiveTimeout = time.Second * 10 46 ) 47 48 // clientPool implements a client database that assigns a priority to each client 49 // based on a positive and negative balance. Positive balance is externally assigned 50 // to prioritized clients and is decreased with connection time and processed 51 // requests (unless the price factors are zero). If the positive balance is zero 52 // then negative balance is accumulated. 53 // 54 // Balance tracking and priority calculation for connected clients is done by 55 // balanceTracker. activeQueue ensures that clients with the lowest positive or 56 // highest negative balance get evicted when the total capacity allowance is full 57 // and new clients with a better balance want to connect. 58 // 59 // Already connected nodes receive a small bias in their favor in order to avoid 60 // accepting and instantly kicking out clients. In theory, we try to ensure that 61 // each client can have several minutes of connection time. 62 // 63 // Balances of disconnected clients are stored in nodeDB including positive balance 64 // and negative banalce. Boeth positive balance and negative balance will decrease 65 // exponentially. If the balance is low enough, then the record will be dropped. 66 type clientPool struct { 67 lps.BalanceTrackerSetup 68 lps.PriorityPoolSetup 69 lock sync.Mutex 70 clock mclock.Clock 71 closed bool 72 removePeer func(enode.ID) 73 ns *nodestate.NodeStateMachine 74 pp *lps.PriorityPool 75 bt *lps.BalanceTracker 76 77 defaultPosFactors, defaultNegFactors lps.PriceFactors 78 posExpTC, negExpTC uint64 79 minCap uint64 // The minimal capacity value allowed for any client 80 connectedBias time.Duration 81 capLimit uint64 82 } 83 84 // clientPoolPeer represents a client peer in the pool. 85 // Positive balances are assigned to node key while negative balances are assigned 86 // to freeClientId. Currently network IP address without port is used because 87 // clients have a limited access to IP addresses while new node keys can be easily 88 // generated so it would be useless to assign a negative value to them. 89 type clientPoolPeer interface { 90 Node() *enode.Node 91 freeClientId() string 92 updateCapacity(uint64) 93 freeze() 94 allowInactive() bool 95 } 96 97 // clientInfo defines all information required by clientpool. 98 type clientInfo struct { 99 node *enode.Node 100 address string 101 peer clientPoolPeer 102 connected, priority bool 103 connectedAt mclock.AbsTime 104 balance *lps.NodeBalance 105 } 106 107 // newClientPool creates a new client pool 108 func newClientPool(ns *nodestate.NodeStateMachine, lespayDb ethdb.Database, minCap uint64, connectedBias time.Duration, clock mclock.Clock, removePeer func(enode.ID)) *clientPool { 109 pool := &clientPool{ 110 ns: ns, 111 BalanceTrackerSetup: balanceTrackerSetup, 112 PriorityPoolSetup: priorityPoolSetup, 113 clock: clock, 114 minCap: minCap, 115 connectedBias: connectedBias, 116 removePeer: removePeer, 117 } 118 pool.bt = lps.NewBalanceTracker(ns, balanceTrackerSetup, lespayDb, clock, &utils.Expirer{}, &utils.Expirer{}) 119 pool.pp = lps.NewPriorityPool(ns, priorityPoolSetup, clock, minCap, connectedBias, 4) 120 121 // set default expiration constants used by tests 122 // Note: server overwrites this if token sale is active 123 pool.bt.SetExpirationTCs(0, defaultNegExpTC) 124 125 ns.SubscribeState(pool.InactiveFlag.Or(pool.PriorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { 126 if newState.Equals(pool.InactiveFlag) { 127 ns.AddTimeout(node, pool.InactiveFlag, inactiveTimeout) 128 } 129 if oldState.Equals(pool.InactiveFlag) && newState.Equals(pool.InactiveFlag.Or(pool.PriorityFlag)) { 130 ns.SetStateSub(node, pool.InactiveFlag, nodestate.Flags{}, 0) // remove timeout 131 } 132 }) 133 134 ns.SubscribeState(pool.ActiveFlag.Or(pool.PriorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { 135 c, _ := ns.GetField(node, clientInfoField).(*clientInfo) 136 if c == nil { 137 return 138 } 139 c.priority = newState.HasAll(pool.PriorityFlag) 140 if newState.Equals(pool.ActiveFlag) { 141 cap, _ := ns.GetField(node, pool.CapacityField).(uint64) 142 if cap > minCap { 143 pool.pp.RequestCapacity(node, minCap, 0, true) 144 } 145 } 146 }) 147 148 ns.SubscribeState(pool.InactiveFlag.Or(pool.ActiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) { 149 if oldState.IsEmpty() { 150 clientConnectedMeter.Mark(1) 151 log.Debug("Client connected", "id", node.ID()) 152 } 153 if oldState.Equals(pool.InactiveFlag) && newState.Equals(pool.ActiveFlag) { 154 clientActivatedMeter.Mark(1) 155 log.Debug("Client activated", "id", node.ID()) 156 } 157 if oldState.Equals(pool.ActiveFlag) && newState.Equals(pool.InactiveFlag) { 158 clientDeactivatedMeter.Mark(1) 159 log.Debug("Client deactivated", "id", node.ID()) 160 c, _ := ns.GetField(node, clientInfoField).(*clientInfo) 161 if c == nil || !c.peer.allowInactive() { 162 pool.removePeer(node.ID()) 163 } 164 } 165 if newState.IsEmpty() { 166 clientDisconnectedMeter.Mark(1) 167 log.Debug("Client disconnected", "id", node.ID()) 168 pool.removePeer(node.ID()) 169 } 170 }) 171 172 var totalConnected uint64 173 ns.SubscribeField(pool.CapacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) { 174 oldCap, _ := oldValue.(uint64) 175 newCap, _ := newValue.(uint64) 176 totalConnected += newCap - oldCap 177 totalConnectedGauge.Update(int64(totalConnected)) 178 c, _ := ns.GetField(node, clientInfoField).(*clientInfo) 179 if c != nil { 180 c.peer.updateCapacity(newCap) 181 } 182 }) 183 return pool 184 } 185 186 // stop shuts the client pool down 187 func (f *clientPool) stop() { 188 f.lock.Lock() 189 f.closed = true 190 f.lock.Unlock() 191 f.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { 192 // enforces saving all balances in BalanceTracker 193 f.disconnectNode(node) 194 }) 195 f.bt.Stop() 196 } 197 198 // connect should be called after a successful handshake. If the connection was 199 // rejected, there is no need to call disconnect. 200 func (f *clientPool) connect(peer clientPoolPeer) (uint64, error) { 201 f.lock.Lock() 202 defer f.lock.Unlock() 203 204 // Short circuit if clientPool is already closed. 205 if f.closed { 206 return 0, fmt.Errorf("Client pool is already closed") 207 } 208 // Dedup connected peers. 209 node, freeID := peer.Node(), peer.freeClientId() 210 if f.ns.GetField(node, clientInfoField) != nil { 211 log.Debug("Client already connected", "address", freeID, "id", node.ID().String()) 212 return 0, fmt.Errorf("Client already connected address=%s id=%s", freeID, node.ID().String()) 213 } 214 now := f.clock.Now() 215 c := &clientInfo{ 216 node: node, 217 address: freeID, 218 peer: peer, 219 connected: true, 220 connectedAt: now, 221 } 222 f.ns.SetField(node, clientInfoField, c) 223 f.ns.SetField(node, connAddressField, freeID) 224 if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*lps.NodeBalance); c.balance == nil { 225 f.disconnect(peer) 226 return 0, nil 227 } 228 c.balance.SetPriceFactors(f.defaultPosFactors, f.defaultNegFactors) 229 230 f.ns.SetState(node, f.InactiveFlag, nodestate.Flags{}, 0) 231 var allowed bool 232 f.ns.Operation(func() { 233 _, allowed = f.pp.RequestCapacity(node, f.minCap, f.connectedBias, true) 234 }) 235 if allowed { 236 return f.minCap, nil 237 } 238 if !peer.allowInactive() { 239 f.disconnect(peer) 240 } 241 return 0, nil 242 } 243 244 // setConnectedBias sets the connection bias, which is applied to already connected clients 245 // So that already connected client won't be kicked out very soon and we can ensure all 246 // connected clients can have enough time to request or sync some data. 247 func (f *clientPool) setConnectedBias(bias time.Duration) { 248 f.lock.Lock() 249 defer f.lock.Unlock() 250 251 f.connectedBias = bias 252 f.pp.SetActiveBias(bias) 253 } 254 255 // disconnect should be called when a connection is terminated. If the disconnection 256 // was initiated by the pool itself using disconnectFn then calling disconnect is 257 // not necessary but permitted. 258 func (f *clientPool) disconnect(p clientPoolPeer) { 259 f.disconnectNode(p.Node()) 260 } 261 262 // disconnectNode removes node fields and flags related to connected status 263 func (f *clientPool) disconnectNode(node *enode.Node) { 264 f.ns.SetField(node, connAddressField, nil) 265 f.ns.SetField(node, clientInfoField, nil) 266 } 267 268 // setDefaultFactors sets the default price factors applied to subsequently connected clients 269 func (f *clientPool) setDefaultFactors(posFactors, negFactors lps.PriceFactors) { 270 f.lock.Lock() 271 defer f.lock.Unlock() 272 273 f.defaultPosFactors = posFactors 274 f.defaultNegFactors = negFactors 275 } 276 277 // capacityInfo returns the total capacity allowance, the total capacity of connected 278 // clients and the total capacity of connected and prioritized clients 279 func (f *clientPool) capacityInfo() (uint64, uint64, uint64) { 280 f.lock.Lock() 281 defer f.lock.Unlock() 282 283 // total priority active cap will be supported when the token issuer module is added 284 _, activeCap := f.pp.Active() 285 return f.capLimit, activeCap, 0 286 } 287 288 // setLimits sets the maximum number and total capacity of connected clients, 289 // dropping some of them if necessary. 290 func (f *clientPool) setLimits(totalConn int, totalCap uint64) { 291 f.lock.Lock() 292 defer f.lock.Unlock() 293 294 f.capLimit = totalCap 295 f.pp.SetLimits(uint64(totalConn), totalCap) 296 } 297 298 // setCapacity sets the assigned capacity of a connected client 299 func (f *clientPool) setCapacity(node *enode.Node, freeID string, capacity uint64, bias time.Duration, setCap bool) (uint64, error) { 300 c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) 301 if c == nil { 302 if setCap { 303 return 0, fmt.Errorf("client %064x is not connected", node.ID()) 304 } 305 c = &clientInfo{node: node} 306 f.ns.SetField(node, clientInfoField, c) 307 f.ns.SetField(node, connAddressField, freeID) 308 if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*lps.NodeBalance); c.balance == nil { 309 log.Error("BalanceField is missing", "node", node.ID()) 310 return 0, fmt.Errorf("BalanceField of %064x is missing", node.ID()) 311 } 312 defer func() { 313 f.ns.SetField(node, connAddressField, nil) 314 f.ns.SetField(node, clientInfoField, nil) 315 }() 316 } 317 var ( 318 minPriority int64 319 allowed bool 320 ) 321 f.ns.Operation(func() { 322 if !setCap || c.priority { 323 // check clientInfo.priority inside Operation to ensure thread safety 324 minPriority, allowed = f.pp.RequestCapacity(node, capacity, bias, setCap) 325 } 326 }) 327 if allowed { 328 return 0, nil 329 } 330 missing := c.balance.PosBalanceMissing(minPriority, capacity, bias) 331 if missing < 1 { 332 // ensure that we never return 0 missing and insufficient priority error 333 missing = 1 334 } 335 return missing, errNoPriority 336 } 337 338 // setCapacityLocked is the equivalent of setCapacity used when f.lock is already locked 339 func (f *clientPool) setCapacityLocked(node *enode.Node, freeID string, capacity uint64, minConnTime time.Duration, setCap bool) (uint64, error) { 340 f.lock.Lock() 341 defer f.lock.Unlock() 342 343 return f.setCapacity(node, freeID, capacity, minConnTime, setCap) 344 } 345 346 // forClients calls the supplied callback for either the listed node IDs or all connected 347 // nodes. It passes a valid clientInfo to the callback and ensures that the necessary 348 // fields and flags are set in order for BalanceTracker and PriorityPool to work even if 349 // the node is not connected. 350 func (f *clientPool) forClients(ids []enode.ID, cb func(client *clientInfo)) { 351 f.lock.Lock() 352 defer f.lock.Unlock() 353 354 if len(ids) == 0 { 355 f.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) { 356 c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) 357 if c != nil { 358 cb(c) 359 } 360 }) 361 } else { 362 for _, id := range ids { 363 node := f.ns.GetNode(id) 364 if node == nil { 365 node = enode.SignNull(&enr.Record{}, id) 366 } 367 c, _ := f.ns.GetField(node, clientInfoField).(*clientInfo) 368 if c != nil { 369 cb(c) 370 } else { 371 c = &clientInfo{node: node} 372 f.ns.SetField(node, clientInfoField, c) 373 f.ns.SetField(node, connAddressField, "") 374 if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*lps.NodeBalance); c.balance != nil { 375 cb(c) 376 } else { 377 log.Error("BalanceField is missing") 378 } 379 f.ns.SetField(node, connAddressField, nil) 380 f.ns.SetField(node, clientInfoField, nil) 381 } 382 } 383 } 384 }