github.com/hardtosaygoodbye/go-ethereum@v1.10.16-0.20220122011429-97003b9e6c15/les/vflux/server/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 server
    18  
    19  import (
    20  	"errors"
    21  	"sync"
    22  	"time"
    23  
    24  	"github.com/hardtosaygoodbye/go-ethereum/common/mclock"
    25  	"github.com/hardtosaygoodbye/go-ethereum/ethdb"
    26  	"github.com/hardtosaygoodbye/go-ethereum/les/utils"
    27  	"github.com/hardtosaygoodbye/go-ethereum/les/vflux"
    28  	"github.com/hardtosaygoodbye/go-ethereum/log"
    29  	"github.com/hardtosaygoodbye/go-ethereum/p2p/enode"
    30  	"github.com/hardtosaygoodbye/go-ethereum/p2p/nodestate"
    31  	"github.com/hardtosaygoodbye/go-ethereum/rlp"
    32  )
    33  
    34  var (
    35  	ErrNotConnected    = errors.New("client not connected")
    36  	ErrNoPriority      = errors.New("priority too low to raise capacity")
    37  	ErrCantFindMaximum = errors.New("Unable to find maximum allowed capacity")
    38  )
    39  
    40  // ClientPool implements a client database that assigns a priority to each client
    41  // based on a positive and negative balance. Positive balance is externally assigned
    42  // to prioritized clients and is decreased with connection time and processed
    43  // requests (unless the price factors are zero). If the positive balance is zero
    44  // then negative balance is accumulated.
    45  //
    46  // Balance tracking and priority calculation for connected clients is done by
    47  // balanceTracker. PriorityQueue ensures that clients with the lowest positive or
    48  // highest negative balance get evicted when the total capacity allowance is full
    49  // and new clients with a better balance want to connect.
    50  //
    51  // Already connected nodes receive a small bias in their favor in order to avoid
    52  // accepting and instantly kicking out clients. In theory, we try to ensure that
    53  // each client can have several minutes of connection time.
    54  //
    55  // Balances of disconnected clients are stored in nodeDB including positive balance
    56  // and negative banalce. Boeth positive balance and negative balance will decrease
    57  // exponentially. If the balance is low enough, then the record will be dropped.
    58  type ClientPool struct {
    59  	*priorityPool
    60  	*balanceTracker
    61  
    62  	setup  *serverSetup
    63  	clock  mclock.Clock
    64  	closed bool
    65  	ns     *nodestate.NodeStateMachine
    66  	synced func() bool
    67  
    68  	lock          sync.RWMutex
    69  	connectedBias time.Duration
    70  
    71  	minCap     uint64      // the minimal capacity value allowed for any client
    72  	capReqNode *enode.Node // node that is requesting capacity change; only used inside NSM operation
    73  }
    74  
    75  // clientPeer represents a peer in the client pool. None of the callbacks should block.
    76  type clientPeer interface {
    77  	Node() *enode.Node
    78  	FreeClientId() string                         // unique id for non-priority clients (typically a prefix of the network address)
    79  	InactiveAllowance() time.Duration             // disconnection timeout for inactive non-priority peers
    80  	UpdateCapacity(newCap uint64, requested bool) // signals a capacity update (requested is true if it is a result of a SetCapacity call on the given peer
    81  	Disconnect()                                  // initiates disconnection (Unregister should always be called)
    82  }
    83  
    84  // NewClientPool creates a new client pool
    85  func NewClientPool(balanceDb ethdb.KeyValueStore, minCap uint64, connectedBias time.Duration, clock mclock.Clock, synced func() bool) *ClientPool {
    86  	setup := newServerSetup()
    87  	ns := nodestate.NewNodeStateMachine(nil, nil, clock, setup.setup)
    88  	cp := &ClientPool{
    89  		priorityPool:   newPriorityPool(ns, setup, clock, minCap, connectedBias, 4, 100),
    90  		balanceTracker: newBalanceTracker(ns, setup, balanceDb, clock, &utils.Expirer{}, &utils.Expirer{}),
    91  		setup:          setup,
    92  		ns:             ns,
    93  		clock:          clock,
    94  		minCap:         minCap,
    95  		connectedBias:  connectedBias,
    96  		synced:         synced,
    97  	}
    98  
    99  	ns.SubscribeState(nodestate.MergeFlags(setup.activeFlag, setup.inactiveFlag, setup.priorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) {
   100  		if newState.Equals(setup.inactiveFlag) {
   101  			// set timeout for non-priority inactive client
   102  			var timeout time.Duration
   103  			if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok {
   104  				timeout = c.InactiveAllowance()
   105  			}
   106  			ns.AddTimeout(node, setup.inactiveFlag, timeout)
   107  		}
   108  		if oldState.Equals(setup.inactiveFlag) && newState.Equals(setup.inactiveFlag.Or(setup.priorityFlag)) {
   109  			ns.SetStateSub(node, setup.inactiveFlag, nodestate.Flags{}, 0) // priority gained; remove timeout
   110  		}
   111  		if newState.Equals(setup.activeFlag) {
   112  			// active with no priority; limit capacity to minCap
   113  			cap, _ := ns.GetField(node, setup.capacityField).(uint64)
   114  			if cap > minCap {
   115  				cp.requestCapacity(node, minCap, minCap, 0)
   116  			}
   117  		}
   118  		if newState.Equals(nodestate.Flags{}) {
   119  			if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok {
   120  				c.Disconnect()
   121  			}
   122  		}
   123  	})
   124  
   125  	ns.SubscribeField(setup.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
   126  		if c, ok := ns.GetField(node, setup.clientField).(clientPeer); ok {
   127  			newCap, _ := newValue.(uint64)
   128  			c.UpdateCapacity(newCap, node == cp.capReqNode)
   129  		}
   130  	})
   131  
   132  	// add metrics
   133  	cp.ns.SubscribeState(nodestate.MergeFlags(cp.setup.activeFlag, cp.setup.inactiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) {
   134  		if oldState.IsEmpty() && !newState.IsEmpty() {
   135  			clientConnectedMeter.Mark(1)
   136  		}
   137  		if !oldState.IsEmpty() && newState.IsEmpty() {
   138  			clientDisconnectedMeter.Mark(1)
   139  		}
   140  		if oldState.HasNone(cp.setup.activeFlag) && oldState.HasAll(cp.setup.activeFlag) {
   141  			clientActivatedMeter.Mark(1)
   142  		}
   143  		if oldState.HasAll(cp.setup.activeFlag) && oldState.HasNone(cp.setup.activeFlag) {
   144  			clientDeactivatedMeter.Mark(1)
   145  		}
   146  		activeCount, activeCap := cp.Active()
   147  		totalActiveCountGauge.Update(int64(activeCount))
   148  		totalActiveCapacityGauge.Update(int64(activeCap))
   149  		totalInactiveCountGauge.Update(int64(cp.Inactive()))
   150  	})
   151  	return cp
   152  }
   153  
   154  // Start starts the client pool. Should be called before Register/Unregister.
   155  func (cp *ClientPool) Start() {
   156  	cp.ns.Start()
   157  }
   158  
   159  // Stop shuts the client pool down. The clientPeer interface callbacks will not be called
   160  // after Stop. Register calls will return nil.
   161  func (cp *ClientPool) Stop() {
   162  	cp.balanceTracker.stop()
   163  	cp.ns.Stop()
   164  }
   165  
   166  // Register registers the peer into the client pool. If the peer has insufficient
   167  // priority and remains inactive for longer than the allowed timeout then it will be
   168  // disconnected by calling the Disconnect function of the clientPeer interface.
   169  func (cp *ClientPool) Register(peer clientPeer) ConnectedBalance {
   170  	cp.ns.SetField(peer.Node(), cp.setup.clientField, peerWrapper{peer})
   171  	balance, _ := cp.ns.GetField(peer.Node(), cp.setup.balanceField).(*nodeBalance)
   172  	return balance
   173  }
   174  
   175  // Unregister removes the peer from the client pool
   176  func (cp *ClientPool) Unregister(peer clientPeer) {
   177  	cp.ns.SetField(peer.Node(), cp.setup.clientField, nil)
   178  }
   179  
   180  // setConnectedBias sets the connection bias, which is applied to already connected clients
   181  // So that already connected client won't be kicked out very soon and we can ensure all
   182  // connected clients can have enough time to request or sync some data.
   183  func (cp *ClientPool) SetConnectedBias(bias time.Duration) {
   184  	cp.lock.Lock()
   185  	cp.connectedBias = bias
   186  	cp.setActiveBias(bias)
   187  	cp.lock.Unlock()
   188  }
   189  
   190  // SetCapacity sets the assigned capacity of a connected client
   191  func (cp *ClientPool) SetCapacity(node *enode.Node, reqCap uint64, bias time.Duration, requested bool) (capacity uint64, err error) {
   192  	cp.lock.RLock()
   193  	if cp.connectedBias > bias {
   194  		bias = cp.connectedBias
   195  	}
   196  	cp.lock.RUnlock()
   197  
   198  	cp.ns.Operation(func() {
   199  		balance, _ := cp.ns.GetField(node, cp.setup.balanceField).(*nodeBalance)
   200  		if balance == nil {
   201  			err = ErrNotConnected
   202  			return
   203  		}
   204  		capacity, _ = cp.ns.GetField(node, cp.setup.capacityField).(uint64)
   205  		if capacity == 0 {
   206  			// if the client is inactive then it has insufficient priority for the minimal capacity
   207  			// (will be activated automatically with minCap when possible)
   208  			return
   209  		}
   210  		if reqCap < cp.minCap {
   211  			// can't request less than minCap; switching between 0 (inactive state) and minCap is
   212  			// performed by the server automatically as soon as necessary/possible
   213  			reqCap = cp.minCap
   214  		}
   215  		if reqCap > cp.minCap && cp.ns.GetState(node).HasNone(cp.setup.priorityFlag) {
   216  			err = ErrNoPriority
   217  			return
   218  		}
   219  		if reqCap == capacity {
   220  			return
   221  		}
   222  		if requested {
   223  			// mark the requested node so that the UpdateCapacity callback can signal
   224  			// whether the update is the direct result of a SetCapacity call on the given node
   225  			cp.capReqNode = node
   226  			defer func() {
   227  				cp.capReqNode = nil
   228  			}()
   229  		}
   230  
   231  		var minTarget, maxTarget uint64
   232  		if reqCap > capacity {
   233  			// Estimate maximum available capacity at the current priority level and request
   234  			// the estimated amount.
   235  			// Note: requestCapacity could find the highest available capacity between the
   236  			// current and the requested capacity but it could cost a lot of iterations with
   237  			// fine step adjustment if the requested capacity is very high. By doing a quick
   238  			// estimation of the maximum available capacity based on the capacity curve we
   239  			// can limit the number of required iterations.
   240  			curve := cp.getCapacityCurve().exclude(node.ID())
   241  			maxTarget = curve.maxCapacity(func(capacity uint64) int64 {
   242  				return balance.estimatePriority(capacity, 0, 0, bias, false)
   243  			})
   244  			if maxTarget < reqCap {
   245  				return
   246  			}
   247  			maxTarget = reqCap
   248  
   249  			// Specify a narrow target range that allows a limited number of fine step
   250  			// iterations
   251  			minTarget = maxTarget - maxTarget/20
   252  			if minTarget < capacity {
   253  				minTarget = capacity
   254  			}
   255  		} else {
   256  			minTarget, maxTarget = reqCap, reqCap
   257  		}
   258  		if newCap := cp.requestCapacity(node, minTarget, maxTarget, bias); newCap >= minTarget && newCap <= maxTarget {
   259  			capacity = newCap
   260  			return
   261  		}
   262  		// we should be able to find the maximum allowed capacity in a few iterations
   263  		log.Error("Unable to find maximum allowed capacity")
   264  		err = ErrCantFindMaximum
   265  	})
   266  	return
   267  }
   268  
   269  // serveCapQuery serves a vflux capacity query. It receives multiple token amount values
   270  // and a bias time value. For each given token amount it calculates the maximum achievable
   271  // capacity in case the amount is added to the balance.
   272  func (cp *ClientPool) serveCapQuery(id enode.ID, freeID string, data []byte) []byte {
   273  	var req vflux.CapacityQueryReq
   274  	if rlp.DecodeBytes(data, &req) != nil {
   275  		return nil
   276  	}
   277  	if l := len(req.AddTokens); l == 0 || l > vflux.CapacityQueryMaxLen {
   278  		return nil
   279  	}
   280  	result := make(vflux.CapacityQueryReply, len(req.AddTokens))
   281  	if !cp.synced() {
   282  		capacityQueryZeroMeter.Mark(1)
   283  		reply, _ := rlp.EncodeToBytes(&result)
   284  		return reply
   285  	}
   286  
   287  	bias := time.Second * time.Duration(req.Bias)
   288  	cp.lock.RLock()
   289  	if cp.connectedBias > bias {
   290  		bias = cp.connectedBias
   291  	}
   292  	cp.lock.RUnlock()
   293  
   294  	// use capacityCurve to answer request for multiple newly bought token amounts
   295  	curve := cp.getCapacityCurve().exclude(id)
   296  	cp.BalanceOperation(id, freeID, func(balance AtomicBalanceOperator) {
   297  		pb, _ := balance.GetBalance()
   298  		for i, addTokens := range req.AddTokens {
   299  			add := addTokens.Int64()
   300  			result[i] = curve.maxCapacity(func(capacity uint64) int64 {
   301  				return balance.estimatePriority(capacity, add, 0, bias, false) / int64(capacity)
   302  			})
   303  			if add <= 0 && uint64(-add) >= pb && result[i] > cp.minCap {
   304  				result[i] = cp.minCap
   305  			}
   306  			if result[i] < cp.minCap {
   307  				result[i] = 0
   308  			}
   309  		}
   310  	})
   311  	// add first result to metrics (don't care about priority client multi-queries yet)
   312  	if result[0] == 0 {
   313  		capacityQueryZeroMeter.Mark(1)
   314  	} else {
   315  		capacityQueryNonZeroMeter.Mark(1)
   316  	}
   317  	reply, _ := rlp.EncodeToBytes(&result)
   318  	return reply
   319  }
   320  
   321  // Handle implements Service
   322  func (cp *ClientPool) Handle(id enode.ID, address string, name string, data []byte) []byte {
   323  	switch name {
   324  	case vflux.CapacityQueryName:
   325  		return cp.serveCapQuery(id, address, data)
   326  	default:
   327  		return nil
   328  	}
   329  }