github.com/ava-labs/avalanchego@v1.11.11/network/throttling/outbound_msg_throttler.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package throttling
     5  
     6  import (
     7  	"errors"
     8  
     9  	"github.com/prometheus/client_golang/prometheus"
    10  	"go.uber.org/zap"
    11  
    12  	"github.com/ava-labs/avalanchego/ids"
    13  	"github.com/ava-labs/avalanchego/message"
    14  	"github.com/ava-labs/avalanchego/snow/validators"
    15  	"github.com/ava-labs/avalanchego/utils/constants"
    16  	"github.com/ava-labs/avalanchego/utils/logging"
    17  )
    18  
    19  var (
    20  	_ OutboundMsgThrottler = (*outboundMsgThrottler)(nil)
    21  	_ OutboundMsgThrottler = (*noOutboundMsgThrottler)(nil)
    22  )
    23  
    24  // Rate-limits outgoing messages
    25  type OutboundMsgThrottler interface {
    26  	// Returns true if we can queue the message [msg] to be sent to node [nodeID].
    27  	// Returns false if the message should be dropped (not sent to [nodeID]).
    28  	// If this method returns true, Release([msg], [nodeID]) must be called (!) when
    29  	// the message is sent (or when we give up trying to send the message, if applicable.)
    30  	// If this method returns false, do not make a corresponding call to Release.
    31  	Acquire(msg message.OutboundMessage, nodeID ids.NodeID) bool
    32  
    33  	// Mark that a message [msg] has been sent to [nodeID] or we have given up
    34  	// sending the message. Must correspond to a previous call to
    35  	// Acquire([msg], [nodeID]) that returned true.
    36  	Release(msg message.OutboundMessage, nodeID ids.NodeID)
    37  }
    38  
    39  type outboundMsgThrottler struct {
    40  	commonMsgThrottler
    41  	metrics outboundMsgThrottlerMetrics
    42  }
    43  
    44  func NewSybilOutboundMsgThrottler(
    45  	log logging.Logger,
    46  	registerer prometheus.Registerer,
    47  	vdrs validators.Manager,
    48  	config MsgByteThrottlerConfig,
    49  ) (OutboundMsgThrottler, error) {
    50  	t := &outboundMsgThrottler{
    51  		commonMsgThrottler: commonMsgThrottler{
    52  			log:                    log,
    53  			vdrs:                   vdrs,
    54  			maxVdrBytes:            config.VdrAllocSize,
    55  			remainingVdrBytes:      config.VdrAllocSize,
    56  			remainingAtLargeBytes:  config.AtLargeAllocSize,
    57  			nodeMaxAtLargeBytes:    config.NodeMaxAtLargeBytes,
    58  			nodeToVdrBytesUsed:     make(map[ids.NodeID]uint64),
    59  			nodeToAtLargeBytesUsed: make(map[ids.NodeID]uint64),
    60  		},
    61  	}
    62  	return t, t.metrics.initialize(registerer)
    63  }
    64  
    65  func (t *outboundMsgThrottler) Acquire(msg message.OutboundMessage, nodeID ids.NodeID) bool {
    66  	// no need to acquire for this message
    67  	if msg.BypassThrottling() {
    68  		return true
    69  	}
    70  
    71  	t.lock.Lock()
    72  	defer t.lock.Unlock()
    73  
    74  	// Take as many bytes as we can from the at-large allocation.
    75  	bytesNeeded := uint64(len(msg.Bytes()))
    76  	atLargeBytesUsed := min(
    77  		// only give as many bytes as needed
    78  		bytesNeeded,
    79  		// don't exceed per-node limit
    80  		t.nodeMaxAtLargeBytes-t.nodeToAtLargeBytesUsed[nodeID],
    81  		// don't give more bytes than are in the allocation
    82  		t.remainingAtLargeBytes,
    83  	)
    84  	bytesNeeded -= atLargeBytesUsed
    85  
    86  	// Take as many bytes as we can from [nodeID]'s validator allocation.
    87  	// Calculate [nodeID]'s validator allocation size based on its weight
    88  	vdrAllocationSize := uint64(0)
    89  	weight := t.vdrs.GetWeight(constants.PrimaryNetworkID, nodeID)
    90  	if weight != 0 {
    91  		totalWeight, err := t.vdrs.TotalWeight(constants.PrimaryNetworkID)
    92  		if err != nil {
    93  			t.log.Error("Failed to get total weight of primary network validators",
    94  				zap.Error(err),
    95  			)
    96  		} else {
    97  			vdrAllocationSize = uint64(float64(t.maxVdrBytes) * float64(weight) / float64(totalWeight))
    98  		}
    99  	}
   100  	vdrBytesAlreadyUsed := t.nodeToVdrBytesUsed[nodeID]
   101  	// [vdrBytesAllowed] is the number of bytes this node
   102  	// may take from its validator allocation.
   103  	vdrBytesAllowed := vdrAllocationSize
   104  	if vdrBytesAlreadyUsed >= vdrAllocationSize {
   105  		// We're already using all the bytes we can from the validator allocation
   106  		vdrBytesAllowed = 0
   107  	} else {
   108  		vdrBytesAllowed -= vdrBytesAlreadyUsed
   109  	}
   110  	vdrBytesUsed := min(t.remainingVdrBytes, bytesNeeded, vdrBytesAllowed)
   111  	bytesNeeded -= vdrBytesUsed
   112  	if bytesNeeded != 0 {
   113  		// Can't acquire enough bytes to queue this message to be sent
   114  		t.metrics.acquireFailures.Inc()
   115  		return false
   116  	}
   117  	// Can acquire enough bytes to queue this message to be sent.
   118  	// Update the state.
   119  	if atLargeBytesUsed > 0 {
   120  		t.remainingAtLargeBytes -= atLargeBytesUsed
   121  		t.nodeToAtLargeBytesUsed[nodeID] += atLargeBytesUsed
   122  		t.metrics.remainingAtLargeBytes.Set(float64(t.remainingAtLargeBytes))
   123  	}
   124  	if vdrBytesUsed > 0 {
   125  		// Mark that [nodeID] used [vdrBytesUsed] from its validator allocation
   126  		t.remainingVdrBytes -= vdrBytesUsed
   127  		t.nodeToVdrBytesUsed[nodeID] += vdrBytesUsed
   128  		t.metrics.remainingVdrBytes.Set(float64(t.remainingVdrBytes))
   129  	}
   130  	t.metrics.acquireSuccesses.Inc()
   131  	t.metrics.awaitingRelease.Inc()
   132  	return true
   133  }
   134  
   135  func (t *outboundMsgThrottler) Release(msg message.OutboundMessage, nodeID ids.NodeID) {
   136  	// no need to release for this message
   137  	if msg.BypassThrottling() {
   138  		return
   139  	}
   140  
   141  	t.lock.Lock()
   142  	defer func() {
   143  		t.metrics.remainingAtLargeBytes.Set(float64(t.remainingAtLargeBytes))
   144  		t.metrics.remainingVdrBytes.Set(float64(t.remainingVdrBytes))
   145  		t.metrics.awaitingRelease.Dec()
   146  		t.lock.Unlock()
   147  	}()
   148  
   149  	// [vdrBytesToReturn] is the number of bytes from [msgSize]
   150  	// that will be given back to [nodeID]'s validator allocation.
   151  	vdrBytesUsed := t.nodeToVdrBytesUsed[nodeID]
   152  	msgSize := uint64(len(msg.Bytes()))
   153  	vdrBytesToReturn := min(msgSize, vdrBytesUsed)
   154  	t.nodeToVdrBytesUsed[nodeID] -= vdrBytesToReturn
   155  	if t.nodeToVdrBytesUsed[nodeID] == 0 {
   156  		delete(t.nodeToVdrBytesUsed, nodeID)
   157  	}
   158  	t.remainingVdrBytes += vdrBytesToReturn
   159  
   160  	// [atLargeBytesToReturn] is the number of bytes from [msgSize]
   161  	// that will be given to the at-large allocation.
   162  	atLargeBytesToReturn := msgSize - vdrBytesToReturn
   163  	// Mark that [nodeID] has released these bytes.
   164  	t.remainingAtLargeBytes += atLargeBytesToReturn
   165  	t.nodeToAtLargeBytesUsed[nodeID] -= atLargeBytesToReturn
   166  	if t.nodeToAtLargeBytesUsed[nodeID] == 0 {
   167  		delete(t.nodeToAtLargeBytesUsed, nodeID)
   168  	}
   169  }
   170  
   171  type outboundMsgThrottlerMetrics struct {
   172  	acquireSuccesses      prometheus.Counter
   173  	acquireFailures       prometheus.Counter
   174  	remainingAtLargeBytes prometheus.Gauge
   175  	remainingVdrBytes     prometheus.Gauge
   176  	awaitingRelease       prometheus.Gauge
   177  }
   178  
   179  func (m *outboundMsgThrottlerMetrics) initialize(registerer prometheus.Registerer) error {
   180  	m.acquireSuccesses = prometheus.NewCounter(prometheus.CounterOpts{
   181  		Name: "throttler_outbound_acquire_successes",
   182  		Help: "Outbound messages not dropped due to rate-limiting",
   183  	})
   184  	m.acquireFailures = prometheus.NewCounter(prometheus.CounterOpts{
   185  		Name: "throttler_outbound_acquire_failures",
   186  		Help: "Outbound messages dropped due to rate-limiting",
   187  	})
   188  	m.remainingAtLargeBytes = prometheus.NewGauge(prometheus.GaugeOpts{
   189  		Name: "throttler_outbound_remaining_at_large_bytes",
   190  		Help: "Bytes remaining in the at large byte allocation",
   191  	})
   192  	m.remainingVdrBytes = prometheus.NewGauge(prometheus.GaugeOpts{
   193  		Name: "throttler_outbound_remaining_validator_bytes",
   194  		Help: "Bytes remaining in the validator byte allocation",
   195  	})
   196  	m.awaitingRelease = prometheus.NewGauge(prometheus.GaugeOpts{
   197  		Name: "throttler_outbound_awaiting_release",
   198  		Help: "Number of messages waiting to be sent",
   199  	})
   200  	return errors.Join(
   201  		registerer.Register(m.acquireSuccesses),
   202  		registerer.Register(m.acquireFailures),
   203  		registerer.Register(m.remainingAtLargeBytes),
   204  		registerer.Register(m.remainingVdrBytes),
   205  		registerer.Register(m.awaitingRelease),
   206  	)
   207  }
   208  
   209  func NewNoOutboundThrottler() OutboundMsgThrottler {
   210  	return &noOutboundMsgThrottler{}
   211  }
   212  
   213  // [Acquire] always returns true. [Release] does nothing.
   214  type noOutboundMsgThrottler struct{}
   215  
   216  func (*noOutboundMsgThrottler) Acquire(message.OutboundMessage, ids.NodeID) bool {
   217  	return true
   218  }
   219  
   220  func (*noOutboundMsgThrottler) Release(message.OutboundMessage, ids.NodeID) {}