Repository: libp2p/go-libp2p
Pull request #2999 (2024-10-08 20:49:02)

fix: basichost: Add backoff for updating local IP addresses on error
closes #2996 


Patch:
diff --git a/p2p/host/basic/basic_host.go b/p2p/host/basic/basic_host.go
index 761a39b6..3263f6e4 100644
--- a/p2p/host/basic/basic_host.go
+++ b/p2p/host/basic/basic_host.go
@@ -21,6 +21,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/record"
 	"github.com/libp2p/go-libp2p/core/transport"
 	"github.com/libp2p/go-libp2p/p2p/host/autonat"
+	"github.com/libp2p/go-libp2p/p2p/host/basic/internal/backoff"
 	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 	"github.com/libp2p/go-libp2p/p2p/host/pstoremanager"
 	"github.com/libp2p/go-libp2p/p2p/host/relaysvc"
@@ -98,6 +99,8 @@ type BasicHost struct {
 	addrChangeChan chan struct{}
 
 	addrMu                 sync.RWMutex
+	updateLocalIPv4Backoff backoff.ExpBackoff
+	updateLocalIPv6Backoff backoff.ExpBackoff
 	filteredInterfaceAddrs []ma.Multiaddr
 	allInterfaceAddrs      []ma.Multiaddr
 
@@ -367,18 +370,32 @@ func (h *BasicHost) updateLocalIpAddr() {
 	if r, err := netroute.New(); err != nil {
 		log.Debugw("failed to build Router for kernel's routing table", "error", err)
 	} else {
-		if _, _, localIPv4, err := r.Route(net.IPv4zero); err != nil {
+
+		var localIPv4 net.IP
+		var ran bool
+		err, ran = h.updateLocalIPv4Backoff.Run(func() error {
+			_, _, localIPv4, err = r.Route(net.IPv4zero)
+			return err
+		})
+
+		if ran && err != nil {
 			log.Debugw("failed to fetch local IPv4 address", "error", err)
-		} else if localIPv4.IsGlobalUnicast() {
+		} else if ran && localIPv4.IsGlobalUnicast() {
 			maddr, err := manet.FromIP(localIPv4)
 			if err == nil {
 				h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, maddr)
 			}
 		}
 
-		if _, _, localIPv6, err := r.Route(net.IPv6unspecified); err != nil {
+		var localIPv6 net.IP
+		err, ran = h.updateLocalIPv6Backoff.Run(func() error {
+			_, _, localIPv6, err = r.Route(net.IPv6unspecified)
+			return err
+		})
+
+		if ran && err != nil {
 			log.Debugw("failed to fetch local IPv6 address", "error", err)
-		} else if localIPv6.IsGlobalUnicast() {
+		} else if ran && localIPv6.IsGlobalUnicast() {
 			maddr, err := manet.FromIP(localIPv6)
 			if err == nil {
 				h.filteredInterfaceAddrs = append(h.filteredInterfaceAddrs, maddr)
diff --git a/p2p/host/basic/internal/backoff/backoff.go b/p2p/host/basic/internal/backoff/backoff.go
new file mode 100644
index 00000000..3d1fd237
--- /dev/null
+++ b/p2p/host/basic/internal/backoff/backoff.go
@@ -0,0 +1,52 @@
+package backoff
+
+import (
+	"time"
+)
+
+var since = time.Since
+
+const defaultDelay = 100 * time.Millisecond
+const defaultMaxDelay = 1 * time.Minute
+
+type ExpBackoff struct {
+	Delay    time.Duration
+	MaxDelay time.Duration
+
+	failures int
+	lastRun  time.Time
+}
+
+func (b *ExpBackoff) init() {
+	if b.Delay == 0 {
+		b.Delay = defaultDelay
+	}
+	if b.MaxDelay == 0 {
+		b.MaxDelay = defaultMaxDelay
+	}
+}
+
+func (b *ExpBackoff) calcDelay() time.Duration {
+	delay := b.Delay * time.Duration(1<<(b.failures-1))
+	delay = min(delay, b.MaxDelay)
+	return delay
+}
+
+func (b *ExpBackoff) Run(f func() error) (err error, ran bool) {
+	b.init()
+
+	if b.failures != 0 {
+		if since(b.lastRun) < b.calcDelay() {
+			return nil, false
+		}
+	}
+
+	b.lastRun = time.Now()
+	err = f()
+	if err == nil {
+		b.failures = 0
+	} else {
+		b.failures++
+	}
+	return err, true
+}


Test patch:
diff --git a/p2p/host/basic/internal/backoff/backoff_test.go b/p2p/host/basic/internal/backoff/backoff_test.go
new file mode 100644
index 00000000..6396b4e6
--- /dev/null
+++ b/p2p/host/basic/internal/backoff/backoff_test.go
@@ -0,0 +1,59 @@
+package backoff
+
+import (
+	"errors"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestBackoff(t *testing.T) {
+	origSince := since
+	defer func() { since = origSince }()
+
+	var timeSince time.Duration
+	since = func(time.Time) time.Duration {
+		return timeSince
+	}
+
+	var maybeErr error
+	b := &ExpBackoff{}
+	f := func() error { return maybeErr }
+
+	err, ran := b.Run(f)
+	require.True(t, ran)
+	require.NoError(t, err)
+
+	maybeErr = errors.New("some error")
+	err, ran = b.Run(f)
+	require.True(t, ran)
+	require.Error(t, err)
+
+	// Rerun again
+	_, ran = b.Run(f)
+	require.False(t, ran)
+
+	timeSince = 100*time.Millisecond + 1
+	err, ran = b.Run(f)
+	require.True(t, ran)
+	require.Error(t, err)
+
+	timeSince = 100*time.Millisecond + 1
+	_, ran = b.Run(f)
+	require.False(t, ran)
+
+	timeSince = 200*time.Millisecond + 1
+	err, ran = b.Run(f)
+	require.True(t, ran)
+	require.Error(t, err)
+
+	for timeSince < defaultMaxDelay*4 {
+		timeSince *= 2
+		err, ran = b.Run(f)
+		require.True(t, ran)
+		require.Error(t, err)
+	}
+
+	require.Equal(t, defaultMaxDelay, b.calcDelay())
+}