discovery: switch to bytes based rate limiting for outbound msgs

In this commit, we revamp the old message based rate limiting. First, we
move to meter by bytes/s instead of messages/s. The old logic had an
error in that it limited groups of message replies, instead of each
message. With this new approach, we'll use the newly added
SerializedSize method to implement fine grained bandwidth metering.

We need to pick two values, the burst rate, and the msg bytes rate. The
burst rate is the max amt that can be sent in a given period of time. We
need to set this above 65 KB, or the max msg limit, otherwise no
messages can be sent. The bucket starts with this many tokens (bytes).
As those are depleted, the amount of tokens is refilled at the msg
bytes rate.

As conservative values, we've chosen 200 KB as the burst rate, and 100
KB/s as the limit.
This commit is contained in:
Olaoluwa Osuntokun committed 2025-03-24 19:21:45 -07:00
1 parent 67d2eac437
commit 05702d48b2
4 files changed
+380 -268

No files matched your search

+167
View File
@@ -1,7 +1,9 @@
package discovery
import (
"bytes"
"fmt"
"io"
"reflect"
"sync"
"sync/atomic"
@@ -15,6 +17,7 @@ import (
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/ticker"
"github.com/stretchr/testify/require"
"golang.org/x/time/rate"
)
// randPeer creates a random peer.
@@ -685,3 +688,167 @@ func assertActiveSyncerTransition(t *testing.T, s *GossipSyncer, peer *mockPeer)
})
assertSyncerStatus(t, s, chansSynced, PassiveSync)
}
// TestSizeableMessage is a test implementation of lnwire.SizeableMessage.
type TestSizeableMessage struct {
size uint32
sizeErr error
}
// Decode implements the lnwire.Message interface.
func (m *TestSizeableMessage) Decode(r io.Reader, pver uint32) error {
return nil
}
// Encode implements the lnwire.Message interface.
func (m *TestSizeableMessage) Encode(w *bytes.Buffer, pver uint32) error {
return nil
}
// MsgType implements the lnwire.Message interface.
func (m *TestSizeableMessage) MsgType() lnwire.MessageType {
// Use a custom message type for testing purposes.
return lnwire.MessageType(9999)
}
// SerializedSize implements the lnwire.SizeableMessage interface.
func (m *TestSizeableMessage) SerializedSize() (uint32, error) {
if m.sizeErr != nil {
return 0, m.sizeErr
}
return m.size, nil
}
// TestDeriveRateLimitReservation tests that the
// SyncManager.deriveRateLimitReservation method correctly computes delays based
// on message size.
func TestDeriveRateLimitReservation(t *testing.T) {
// Define standard test parameters for rate limiting.
bytesPerSec := uint64(1000) // 1000 bytes/second
bytesBurst := uint64(100) // 100 bytes burst capacity
// Helper to create a fresh SyncManager with a clean rate limiter.
newSyncManager := func(perSec, burst uint64) *SyncManager {
limiter := rate.NewLimiter(rate.Limit(perSec), int(burst))
return &SyncManager{
rateLimiter: limiter,
}
}
// Test sequential messages causing increasing delays - this is the core
// property of the rate limiting mechanism.
t.Run("sequential messages have increasing delays", func(t *testing.T) {
sm := newSyncManager(bytesPerSec, bytesBurst)
// Send a series of messages that exactly consume the burst
// limit to make the behavior predictable.
msg := &TestSizeableMessage{
size: uint32(bytesBurst),
}
// First message should have no delay as it fits within burst.
delay1, err := sm.deriveRateLimitReservation(msg)
require.NoError(t, err)
require.Equal(
t, time.Duration(0), delay1.Delay(), "first message "+
"should have no delay",
)
// Second message should have a non-zero delay as the token
// bucket is now depleted.
delay2, err := sm.deriveRateLimitReservation(msg)
require.NoError(t, err)
require.True(
t, delay2.Delay() > 0, "second message should have "+
"non-zero delay, got: %s", delay2,
)
// Third message should have an even longer delay since the
// token bucket is still refilling at a constant rate.
delay3, err := sm.deriveRateLimitReservation(msg)
require.NoError(t, err)
require.True(t, delay3.Delay() > delay2.Delay(), "third "+
"message should have longer delay than second: %s > %s",
delay3, delay2)
// The expected theoretical delay when sending messages at
// exactly the burst size should be approximately 100ms with our
// parameters.
expectedDelay := time.Duration(
float64(msg.size) / float64(bytesPerSec) * float64(time.Second), //nolint:ll
)
// Check that delays are increasing in a coherent manner.
t.Logf("Delay sequence: %s → %s → %s (expected "+
"theoretical: %s)", delay1.Delay(), delay2.Delay(),
delay3.Delay(), expectedDelay)
})
// Test handling of errors from SerializedSize.
t.Run("propagates serialization errors", func(t *testing.T) {
sm := newSyncManager(bytesPerSec, bytesBurst)
// Create a test message that returns an error from
// SerializedSize.
errMsg := fmt.Errorf("failed to serialize message")
msg := &TestSizeableMessage{
sizeErr: errMsg,
}
// The error should propagate through
// deriveRateLimitReservation.
_, err := sm.deriveRateLimitReservation(msg)
require.Error(t, err)
require.Equal(
t, errMsg, err, "Error should be propagated unchanged",
)
})
// Test that message size affects delay.
t.Run("larger messages have longer delays", func(t *testing.T) {
// Create a rate limiter with a known burst size.
sm := newSyncManager(bytesPerSec, bytesBurst)
// First, empty the token bucket with a message exactly at the
// burst size.
initialMsg := &TestSizeableMessage{
size: uint32(bytesBurst),
}
_, err := sm.deriveRateLimitReservation(initialMsg)
require.NoError(t, err)
// Now send two messages of different sizes and compare their
// delays.
smallMsg := &TestSizeableMessage{
size: 50,
}
largeMsg := &TestSizeableMessage{
size: 200,
}
// Send the small message first.
smallDelay, err := sm.deriveRateLimitReservation(smallMsg)
require.NoError(t, err)
// Reset the limiter to the same state, then empty the bucket.
sm.rateLimiter = rate.NewLimiter(
rate.Limit(bytesPerSec), int(bytesBurst),
)
_, err = sm.deriveRateLimitReservation(initialMsg)
require.NoError(t, err)
// Now send the large message.
largeDelay, err := sm.deriveRateLimitReservation(largeMsg)
require.NoError(t, err)
// The large message should have a longer delay than the small
// one.
require.True(
t, largeDelay.Delay() > smallDelay.Delay(),
"large message (size %d) should have longer delay "+
"(%s) than small message (size %d, delay %s)",
largeMsg.size, largeDelay.Delay(), smallMsg.size,
smallDelay.Delay())
})
}