Repository: libp2p/go-libp2p
Pull request #2022 (2023-01-27 08:39:31)

autorelay: Split libp2p.EnableAutoRelay into 2 functions
Provide two specific ways to enable the autorelay subsystem libp2p.EnableAutoRelayWithStaticRelays
libp2p.EnableAutoRelayWithPeerSource

fixes: https://github.com/libp2p/go-libp2p/issues/1866

sukunrt: @marten-seemann addressed review comments. 
I've removed the test TestIncorrectInit since that should be fixed by this PR and shouldn't need this test that checks for panic on incorrect initialisation. 

Patch:
diff --git a/options.go b/options.go
index 381706f7..c97bc9c8 100644
--- a/options.go
+++ b/options.go
@@ -16,6 +16,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/metrics"
 	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/peerstore"
 	"github.com/libp2p/go-libp2p/core/pnet"
 	"github.com/libp2p/go-libp2p/core/protocol"
@@ -307,6 +308,8 @@ func EnableRelayService(opts ...relayv2.Option) Option {
 //
 // This subsystem performs automatic address rewriting to advertise relay addresses when it
 // detects that the node is publicly unreachable (e.g. behind a NAT).
+//
+// Deprecated: Use EnableAutoRelayWithStaticRelays or EnableAutoRelayWithPeerSource
 func EnableAutoRelay(opts ...autorelay.Option) Option {
 	return func(cfg *Config) error {
 		cfg.EnableAutoRelay = true
@@ -315,6 +318,31 @@ func EnableAutoRelay(opts ...autorelay.Option) Option {
 	}
 }
 
+// EnableAutoRelayWithStaticRelays configures libp2p to enable the AutoRelay subsystem using
+// the provided relays as relay candidates.
+// This subsystem performs automatic address rewriting to advertise relay addresses when it
+// detects that the node is publicly unreachable (e.g. behind a NAT).
+func EnableAutoRelayWithStaticRelays(static []peer.AddrInfo, opts ...autorelay.Option) Option {
+	return func(cfg *Config) error {
+		cfg.EnableAutoRelay = true
+		cfg.AutoRelayOpts = append([]autorelay.Option{autorelay.WithStaticRelays(static)}, opts...)
+		return nil
+	}
+}
+
+// EnableAutoRelayWithPeerSource configures libp2p to enable the AutoRelay
+// subsystem using the provided PeerSource callback to get more relay
+// candidates.  This subsystem performs automatic address rewriting to advertise
+// relay addresses when it detects that the node is publicly unreachable (e.g.
+// behind a NAT).
+func EnableAutoRelayWithPeerSource(peerSource autorelay.PeerSource, opts ...autorelay.Option) Option {
+	return func(cfg *Config) error {
+		cfg.EnableAutoRelay = true
+		cfg.AutoRelayOpts = append([]autorelay.Option{autorelay.WithPeerSource(peerSource)}, opts...)
+		return nil
+	}
+}
+
 // ForceReachabilityPublic overrides automatic reachability detection in the AutoNAT subsystem,
 // forcing the local node to believe it is reachable externally.
 func ForceReachabilityPublic() Option {
diff --git a/p2p/host/autonat/autonat.go b/p2p/host/autonat/autonat.go
index 0fd8b0ec..7368cccf 100644
--- a/p2p/host/autonat/autonat.go
+++ b/p2p/host/autonat/autonat.go
@@ -119,7 +119,10 @@ func New(h host.Host, options ...Option) (AutoNAT, error) {
 	}
 	as.status.Store(autoNATResult{network.ReachabilityUnknown, nil})
 
-	subscriber, err := as.host.EventBus().Subscribe([]interface{}{new(event.EvtLocalAddressesUpdated), new(event.EvtPeerIdentificationCompleted)})
+	subscriber, err := as.host.EventBus().Subscribe(
+		[]any{new(event.EvtLocalAddressesUpdated), new(event.EvtPeerIdentificationCompleted)},
+		eventbus.Name("autonat"),
+	)
 	if err != nil {
 		return nil, err
 	}
diff --git a/p2p/host/autorelay/autorelay.go b/p2p/host/autorelay/autorelay.go
index e4e3568e..58cebfb4 100644
--- a/p2p/host/autorelay/autorelay.go
+++ b/p2p/host/autorelay/autorelay.go
@@ -8,6 +8,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/network"
 	basic "github.com/libp2p/go-libp2p/p2p/host/basic"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 
 	logging "github.com/ipfs/go-log/v2"
 	ma "github.com/multiformats/go-multiaddr"
@@ -57,7 +58,7 @@ func NewAutoRelay(bhost *basic.BasicHost, opts ...Option) (*AutoRelay, error) {
 }
 
 func (r *AutoRelay) background() {
-	subReachability, err := r.host.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged))
+	subReachability, err := r.host.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged), eventbus.Name("autorelay (background)"))
 	if err != nil {
 		log.Debug("failed to subscribe to the EvtLocalReachabilityChanged")
 		return
diff --git a/p2p/host/autorelay/options.go b/p2p/host/autorelay/options.go
index fd132849..f8d1414c 100644
--- a/p2p/host/autorelay/options.go
+++ b/p2p/host/autorelay/options.go
@@ -10,9 +10,22 @@ import (
 	"github.com/benbjohnson/clock"
 )
 
+// AutoRelay will call this function when it needs new candidates because it is
+// not connected to the desired number of relays or we get disconnected from one
+// of the relays. Implementations must send *at most* numPeers, and close the
+// channel when they don't intend to provide any more peers. AutoRelay will not
+// call the callback again until the channel is closed. Implementations should
+// send new peers, but may send peers they sent before. AutoRelay implements a
+// per-peer backoff (see WithBackoff). See WithMinInterval for setting the
+// minimum interval between calls to the callback. The context.Context passed
+// may be canceled when AutoRelay feels satisfied, it will be canceled when the
+// node is shutting down. If the context is canceled you MUST close the output
+// channel at some point.
+type PeerSource func(ctx context.Context, num int) <-chan peer.AddrInfo
+
 type config struct {
 	clock      clock.Clock
-	peerSource func(ctx context.Context, num int) <-chan peer.AddrInfo
+	peerSource PeerSource
 	// minimum interval used to call the peerSource callback
 	minInterval time.Duration
 	// see WithMinCandidates
@@ -40,6 +53,7 @@ var defaultConfig = config{
 	backoff:         time.Hour,
 	desiredRelays:   2,
 	maxCandidateAge: 30 * time.Minute,
+	minInterval:     30 * time.Second,
 }
 
 var (
@@ -65,7 +79,7 @@ func WithStaticRelays(static []peer.AddrInfo) Option {
 				c <- static[i]
 			}
 			return c
-		}, 30*time.Second)(c)
+		})(c)
 		WithMinCandidates(len(static))(c)
 		WithMaxCandidates(len(static))(c)
 		WithNumRelays(len(static))(c)
@@ -75,23 +89,12 @@ func WithStaticRelays(static []peer.AddrInfo) Option {
 }
 
 // WithPeerSource defines a callback for AutoRelay to query for more relay candidates.
-// AutoRelay will call this function when it needs new candidates is connected to the desired number of
-// relays, and it has enough candidates (in case we get disconnected from one of the relays).
-// Implementations must send *at most* numPeers, and close the channel when they don't intend to provide
-// any more peers.
-// AutoRelay will not call the callback again until the channel is closed.
-// Implementations should send new peers, but may send peers they sent before. AutoRelay implements
-// a per-peer backoff (see WithBackoff).
-// minInterval is the minimum interval this callback is called with, even if AutoRelay needs new candidates.
-// The context.Context passed MAY be canceled when AutoRelay feels satisfied, it will be canceled when the node is shutting down.
-// If the channel is canceled you MUST close the output channel at some point.
-func WithPeerSource(f func(ctx context.Context, numPeers int) <-chan peer.AddrInfo, minInterval time.Duration) Option {
+func WithPeerSource(f PeerSource) Option {
 	return func(c *config) error {
 		if c.peerSource != nil {
 			return errAlreadyHavePeerSource
 		}
 		c.peerSource = f
-		c.minInterval = minInterval
 		return nil
 	}
 }
@@ -174,3 +177,12 @@ func WithClock(cl clock.Clock) Option {
 		return nil
 	}
 }
+
+// WithMinInterval sets the minimum interval after which peerSource callback will be called for more
+// candidates even if AutoRelay needs new candidates.
+func WithMinInterval(interval time.Duration) Option {
+	return func(c *config) error {
+		c.minInterval = interval
+		return nil
+	}
+}
diff --git a/p2p/host/autorelay/relay_finder.go b/p2p/host/autorelay/relay_finder.go
index 20d71240..098b19c2 100644
--- a/p2p/host/autorelay/relay_finder.go
+++ b/p2p/host/autorelay/relay_finder.go
@@ -14,6 +14,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
 	basic "github.com/libp2p/go-libp2p/p2p/host/basic"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 	relayv1 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv1/relay"
 	circuitv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"
 	circuitv2_proto "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
@@ -59,7 +60,7 @@ type relayFinder struct {
 	ctxCancel   context.CancelFunc
 	ctxCancelMx sync.Mutex
 
-	peerSource func(context.Context, int) <-chan peer.AddrInfo
+	peerSource PeerSource
 
 	candidateFound             chan struct{} // receives every time we find a new relay candidate
 	candidateMx                sync.Mutex
@@ -82,7 +83,7 @@ type relayFinder struct {
 	cachedAddrsExpiry time.Time
 }
 
-func newRelayFinder(host *basic.BasicHost, peerSource func(context.Context, int) <-chan peer.AddrInfo, conf *config) *relayFinder {
+func newRelayFinder(host *basic.BasicHost, peerSource PeerSource, conf *config) *relayFinder {
 	if peerSource == nil {
 		panic("Can not create a new relayFinder. Need a Peer Source fn or a list of static relays. Refer to the documentation around `libp2p.EnableAutoRelay`")
 	}
@@ -115,7 +116,7 @@ func (rf *relayFinder) background(ctx context.Context) {
 		rf.handleNewCandidates(ctx)
 	}()
 
-	subConnectedness, err := rf.host.EventBus().Subscribe(new(event.EvtPeerConnectednessChanged))
+	subConnectedness, err := rf.host.EventBus().Subscribe(new(event.EvtPeerConnectednessChanged), eventbus.Name("autorelay (relay finder)"))
 	if err != nil {
 		log.Error("failed to subscribe to the EvtPeerConnectednessChanged")
 		return
diff --git a/p2p/host/eventbus/basic_metrics.go b/p2p/host/eventbus/basic_metrics.go
index 0a6edf8a..3c306b6a 100644
--- a/p2p/host/eventbus/basic_metrics.go
+++ b/p2p/host/eventbus/basic_metrics.go
@@ -5,6 +5,8 @@ import (
 	"strings"
 	"sync"
 
+	"github.com/libp2p/go-libp2p/p2p/metricshelper"
+
 	"github.com/prometheus/client_golang/prometheus"
 )
 
@@ -107,23 +109,43 @@ func NewMetricsTracer(opts ...MetricsTracerOption) MetricsTracer {
 }
 
 func (m *metricsTracer) EventEmitted(typ reflect.Type) {
-	eventsEmitted.WithLabelValues(strings.TrimPrefix(typ.String(), "event.")).Inc()
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
+	*tags = append(*tags, strings.TrimPrefix(typ.String(), "event."))
+	eventsEmitted.WithLabelValues(*tags...).Inc()
 }
 
 func (m *metricsTracer) AddSubscriber(typ reflect.Type) {
-	totalSubscribers.WithLabelValues(strings.TrimPrefix(typ.String(), "event.")).Inc()
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
+	*tags = append(*tags, strings.TrimPrefix(typ.String(), "event."))
+	totalSubscribers.WithLabelValues(*tags...).Inc()
 }
 
 func (m *metricsTracer) RemoveSubscriber(typ reflect.Type) {
-	totalSubscribers.WithLabelValues(strings.TrimPrefix(typ.String(), "event.")).Dec()
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
+	*tags = append(*tags, strings.TrimPrefix(typ.String(), "event."))
+	totalSubscribers.WithLabelValues(*tags...).Dec()
 }
 
 func (m *metricsTracer) SubscriberQueueLength(name string, n int) {
-	subscriberQueueLength.WithLabelValues(name).Set(float64(n))
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
+	*tags = append(*tags, name)
+	subscriberQueueLength.WithLabelValues(*tags...).Set(float64(n))
 }
 
 func (m *metricsTracer) SubscriberQueueFull(name string, isFull bool) {
-	observer := subscriberQueueFull.WithLabelValues(name)
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
+	*tags = append(*tags, name)
+	observer := subscriberQueueFull.WithLabelValues(*tags...)
 	if isFull {
 		observer.Set(1)
 	} else {
@@ -132,5 +154,9 @@ func (m *metricsTracer) SubscriberQueueFull(name string, isFull bool) {
 }
 
 func (m *metricsTracer) SubscriberEventQueued(name string) {
-	subscriberEventQueued.WithLabelValues(name).Inc()
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
+	*tags = append(*tags, name)
+	subscriberEventQueued.WithLabelValues(*tags...).Inc()
 }
diff --git a/p2p/host/pstoremanager/pstoremanager.go b/p2p/host/pstoremanager/pstoremanager.go
index f8382c70..da8f84c4 100644
--- a/p2p/host/pstoremanager/pstoremanager.go
+++ b/p2p/host/pstoremanager/pstoremanager.go
@@ -9,6 +9,7 @@ import (
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/peerstore"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 
 	logging "github.com/ipfs/go-log/v2"
 )
@@ -68,7 +69,7 @@ func NewPeerstoreManager(pstore peerstore.Peerstore, eventBus event.Bus, opts ..
 func (m *PeerstoreManager) Start() {
 	ctx, cancel := context.WithCancel(context.Background())
 	m.cancel = cancel
-	sub, err := m.eventBus.Subscribe(&event.EvtPeerConnectednessChanged{})
+	sub, err := m.eventBus.Subscribe(&event.EvtPeerConnectednessChanged{}, eventbus.Name("pstoremanager"))
 	if err != nil {
 		log.Warnf("subscription failed. Peerstore manager not activated. Error: %s", err)
 		return
diff --git a/p2p/host/relaysvc/relay.go b/p2p/host/relaysvc/relay.go
index a36e20e0..bea8aa73 100644
--- a/p2p/host/relaysvc/relay.go
+++ b/p2p/host/relaysvc/relay.go
@@ -4,11 +4,11 @@ import (
 	"context"
 	"sync"
 
-	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
-
 	"github.com/libp2p/go-libp2p/core/event"
 	"github.com/libp2p/go-libp2p/core/host"
 	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
+	relayv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"
 )
 
 type RelayManager struct {
@@ -44,7 +44,7 @@ func (m *RelayManager) background(ctx context.Context) {
 		m.mutex.Unlock()
 	}()
 
-	subReachability, _ := m.host.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged))
+	subReachability, _ := m.host.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged), eventbus.Name("relaysvc"))
 	defer subReachability.Close()
 
 	for {
diff --git a/p2p/metricshelper/pool.go b/p2p/metricshelper/pool.go
new file mode 100644
index 00000000..3290ed5a
--- /dev/null
+++ b/p2p/metricshelper/pool.go
@@ -0,0 +1,26 @@
+package metricshelper
+
+import (
+	"fmt"
+	"sync"
+)
+
+const capacity = 8
+
+var stringPool = sync.Pool{New: func() any {
+	s := make([]string, 0, capacity)
+	return &s
+}}
+
+func GetStringSlice() *[]string {
+	s := stringPool.Get().(*[]string)
+	*s = (*s)[:0]
+	return s
+}
+
+func PutStringSlice(s *[]string) {
+	if c := cap(*s); c < capacity {
+		panic(fmt.Sprintf("expected a string slice with capacity 8 or greater, got %d", c))
+	}
+	stringPool.Put(s)
+}
diff --git a/p2p/net/swarm/swarm_metrics.go b/p2p/net/swarm/swarm_metrics.go
index 16f431b5..30edd8bc 100644
--- a/p2p/net/swarm/swarm_metrics.go
+++ b/p2p/net/swarm/swarm_metrics.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/libp2p/go-libp2p/core/crypto"
 	"github.com/libp2p/go-libp2p/core/network"
+	"github.com/libp2p/go-libp2p/p2p/metricshelper"
 
 	ma "github.com/multiformats/go-multiaddr"
 
@@ -93,19 +94,6 @@ func NewMetricsTracer() *metricsTracer {
 	return &metricsTracer{}
 }
 
-var stringPool = sync.Pool{New: func() any {
-	s := make([]string, 0, 8)
-	return &s
-}}
-
-func getStringSlice() *[]string {
-	s := stringPool.Get().(*[]string)
-	*s = (*s)[:0]
-	return s
-}
-
-func putStringSlice(s *[]string) { stringPool.Put(s) }
-
 func getDirection(dir network.Direction) string {
 	switch dir {
 	case network.DirOutbound:
@@ -132,8 +120,8 @@ func appendConnectionState(tags []string, cs network.ConnectionState) []string {
 }
 
 func (m *metricsTracer) OpenedConnection(dir network.Direction, p crypto.PubKey, cs network.ConnectionState) {
-	tags := getStringSlice()
-	defer putStringSlice(tags)
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
 
 	*tags = append(*tags, getDirection(dir))
 	*tags = appendConnectionState(*tags, cs)
@@ -146,8 +134,9 @@ func (m *metricsTracer) OpenedConnection(dir network.Direction, p crypto.PubKey,
 }
 
 func (m *metricsTracer) ClosedConnection(dir network.Direction, duration time.Duration, cs network.ConnectionState) {
-	tags := getStringSlice()
-	defer putStringSlice(tags)
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
 	*tags = append(*tags, getDirection(dir))
 	*tags = appendConnectionState(*tags, cs)
 	connsClosed.WithLabelValues(*tags...).Inc()
@@ -159,8 +148,9 @@ func (m *metricsTracer) ClosedConnection(dir network.Direction, duration time.Du
 }
 
 func (m *metricsTracer) CompletedHandshake(t time.Duration, cs network.ConnectionState) {
-	tags := getStringSlice()
-	defer putStringSlice(tags)
+	tags := metricshelper.GetStringSlice()
+	defer metricshelper.PutStringSlice(tags)
+
 	*tags = appendConnectionState(*tags, cs)
 	connHandshakeLatency.WithLabelValues(*tags...).Observe(t.Seconds())
 }
diff --git a/p2p/protocol/holepunch/svc.go b/p2p/protocol/holepunch/svc.go
index 8476686b..5de7c7cf 100644
--- a/p2p/protocol/holepunch/svc.go
+++ b/p2p/protocol/holepunch/svc.go
@@ -13,9 +13,11 @@ import (
 	"github.com/libp2p/go-libp2p/core/network"
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/libp2p/go-libp2p/core/protocol"
+	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
 	"github.com/libp2p/go-libp2p/p2p/protocol/holepunch/pb"
 	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
 	"github.com/libp2p/go-msgio/pbio"
+
 	ma "github.com/multiformats/go-multiaddr"
 )
 
@@ -122,7 +124,7 @@ func (s *Service) watchForPublicAddr() {
 	}
 
 	// Only start the holePuncher if we're behind a NAT / firewall.
-	sub, err := s.host.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})
+	sub, err := s.host.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{}, eventbus.Name("holepunch"))
 	if err != nil {
 		log.Debugf("failed to subscripe to Reachability event: %s", err)
 		return
diff --git a/p2p/protocol/identify/id.go b/p2p/protocol/identify/id.go
index 2a1af713..6f569310 100644
--- a/p2p/protocol/identify/id.go
+++ b/p2p/protocol/identify/id.go
@@ -189,10 +189,11 @@ func (ids *idService) loop() {
 	defer ids.refCount.Done()
 
 	phs := make(map[peer.ID]*peerHandler)
-	sub, err := ids.Host.EventBus().Subscribe([]interface{}{
-		&event.EvtLocalProtocolsUpdated{},
-		&event.EvtLocalAddressesUpdated{},
-	}, eventbus.BufSize(256))
+	sub, err := ids.Host.EventBus().Subscribe(
+		[]any{&event.EvtLocalProtocolsUpdated{}, &event.EvtLocalAddressesUpdated{}},
+		eventbus.BufSize(256),
+		eventbus.Name("identify (loop)"),
+	)
 	if err != nil {
 		log.Errorf("failed to subscribe to events on the bus, err=%s", err)
 		return
diff --git a/p2p/protocol/identify/obsaddr.go b/p2p/protocol/identify/obsaddr.go
index bd72175d..9b20ee4f 100644
--- a/p2p/protocol/identify/obsaddr.go
+++ b/p2p/protocol/identify/obsaddr.go
@@ -141,7 +141,7 @@ func NewObservedAddrManager(host host.Host) (*ObservedAddrManager, error) {
 	}
 	oas.ctx, oas.ctxCancel = context.WithCancel(context.Background())
 
-	reachabilitySub, err := host.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged))
+	reachabilitySub, err := host.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged), eventbus.Name("identify (obsaddr)"))
 	if err != nil {
 		return nil, fmt.Errorf("failed to subscribe to reachability event: %s", err)
 	}


Test patch:
diff --git a/p2p/host/autorelay/autorelay_test.go b/p2p/host/autorelay/autorelay_test.go
index fd0cdc94..768c8ec2 100644
--- a/p2p/host/autorelay/autorelay_test.go
+++ b/p2p/host/autorelay/autorelay_test.go
@@ -52,11 +52,22 @@ func usedRelays(h host.Host) []peer.ID {
 	return peers
 }
 
-func newPrivateNode(t *testing.T, opts ...autorelay.Option) host.Host {
+func newPrivateNode(t *testing.T, peerSource func(context.Context, int) <-chan peer.AddrInfo,
+	opts ...autorelay.Option) host.Host {
 	t.Helper()
 	h, err := libp2p.New(
 		libp2p.ForceReachabilityPrivate(),
-		libp2p.EnableAutoRelay(opts...),
+		libp2p.EnableAutoRelayWithPeerSource(peerSource, opts...),
+	)
+	require.NoError(t, err)
+	return h
+}
+
+func newPrivateNodeWithStaticRelays(t *testing.T, static []peer.AddrInfo, opts ...autorelay.Option) host.Host {
+	t.Helper()
+	h, err := libp2p.New(
+		libp2p.ForceReachabilityPrivate(),
+		libp2p.EnableAutoRelayWithStaticRelays(static, opts...),
 	)
 	require.NoError(t, err)
 	return h
@@ -117,7 +128,7 @@ func newRelayV1(t *testing.T) host.Host {
 func TestSingleCandidate(t *testing.T) {
 	var counter int
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(_ context.Context, num int) <-chan peer.AddrInfo {
+		func(_ context.Context, num int) <-chan peer.AddrInfo {
 			counter++
 			require.Equal(t, 1, num)
 			peerChan := make(chan peer.AddrInfo, num)
@@ -126,10 +137,11 @@ func TestSingleCandidate(t *testing.T) {
 			t.Cleanup(func() { r.Close() })
 			peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
 			return peerChan
-		}, time.Hour),
+		},
 		autorelay.WithMaxCandidates(1),
 		autorelay.WithNumRelays(99999),
 		autorelay.WithBootDelay(0),
+		autorelay.WithMinInterval(time.Hour),
 	)
 	defer h.Close()
 
@@ -151,15 +163,16 @@ func TestSingleRelay(t *testing.T) {
 	close(peerChan)
 
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(_ context.Context, num int) <-chan peer.AddrInfo {
+		func(_ context.Context, num int) <-chan peer.AddrInfo {
 			require.False(t, called, "expected the peer source callback to only have been called once")
 			called = true
 			require.Equal(t, numCandidates, num)
 			return peerChan
-		}, time.Hour),
+		},
 		autorelay.WithMaxCandidates(numCandidates),
 		autorelay.WithNumRelays(1),
 		autorelay.WithBootDelay(0),
+		autorelay.WithMinInterval(time.Hour),
 	)
 	defer h.Close()
 
@@ -178,15 +191,16 @@ func TestPreferRelayV2(t *testing.T) {
 	})
 
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo {
+		func(context.Context, int) <-chan peer.AddrInfo {
 			peerChan := make(chan peer.AddrInfo, 1)
 			defer close(peerChan)
 			peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
 			return peerChan
-		}, time.Hour),
+		},
 		autorelay.WithMaxCandidates(1),
 		autorelay.WithNumRelays(99999),
 		autorelay.WithBootDelay(0),
+		autorelay.WithMinInterval(time.Hour),
 	)
 	defer h.Close()
 
@@ -196,10 +210,11 @@ func TestPreferRelayV2(t *testing.T) {
 func TestWaitForCandidates(t *testing.T) {
 	peerChan := make(chan peer.AddrInfo)
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+		func(context.Context, int) <-chan peer.AddrInfo { return peerChan },
 		autorelay.WithMinCandidates(2),
 		autorelay.WithNumRelays(1),
 		autorelay.WithBootDelay(time.Hour),
+		autorelay.WithMinInterval(time.Hour),
 	)
 	defer h.Close()
 
@@ -244,18 +259,19 @@ func TestBackoff(t *testing.T) {
 
 	var counter int32 // to be used atomically
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo {
+		func(context.Context, int) <-chan peer.AddrInfo {
 			// always return the same node, and make sure we don't try to connect to it too frequently
 			atomic.AddInt32(&counter, 1)
 			peerChan := make(chan peer.AddrInfo, 1)
 			peerChan <- peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()}
 			close(peerChan)
 			return peerChan
-		}, time.Second),
+		},
 		autorelay.WithNumRelays(1),
 		autorelay.WithBootDelay(0),
 		autorelay.WithBackoff(backoff),
 		autorelay.WithClock(cl),
+		autorelay.WithMinInterval(time.Second),
 	)
 	defer h.Close()
 
@@ -280,8 +296,8 @@ func TestStaticRelays(t *testing.T) {
 		staticRelays = append(staticRelays, peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()})
 	}
 
-	h := newPrivateNode(t,
-		autorelay.WithStaticRelays(staticRelays),
+	h := newPrivateNodeWithStaticRelays(t,
+		staticRelays,
 		autorelay.WithNumRelays(1),
 	)
 	defer h.Close()
@@ -298,8 +314,9 @@ func TestRelayV1(t *testing.T) {
 		close(peerChan)
 
 		h := newPrivateNode(t,
-			autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+			func(context.Context, int) <-chan peer.AddrInfo { return peerChan },
 			autorelay.WithBootDelay(0),
+			autorelay.WithMinInterval(time.Hour),
 		)
 		defer h.Close()
 
@@ -314,9 +331,10 @@ func TestRelayV1(t *testing.T) {
 		close(peerChan)
 
 		h := newPrivateNode(t,
-			autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+			func(context.Context, int) <-chan peer.AddrInfo { return peerChan },
 			autorelay.WithBootDelay(0),
 			autorelay.WithCircuitV1Support(),
+			autorelay.WithMinInterval(time.Hour),
 		)
 		defer h.Close()
 
@@ -338,11 +356,12 @@ func TestConnectOnDisconnect(t *testing.T) {
 		relays = append(relays, r)
 	}
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo { return peerChan }, time.Hour),
+		func(context.Context, int) <-chan peer.AddrInfo { return peerChan },
 		autorelay.WithMinCandidates(1),
 		autorelay.WithMaxCandidates(num),
 		autorelay.WithNumRelays(1),
 		autorelay.WithBootDelay(0),
+		autorelay.WithMinInterval(time.Hour),
 	)
 	defer h.Close()
 
@@ -387,18 +406,19 @@ func TestMaxAge(t *testing.T) {
 	close(peerChans)
 
 	h := newPrivateNode(t,
-		autorelay.WithPeerSource(func(context.Context, int) <-chan peer.AddrInfo {
+		func(context.Context, int) <-chan peer.AddrInfo {
 			c, ok := <-peerChans
 			if !ok {
 				t.Fatal("unexpected call to PeerSource")
 			}
 			return c
-		}, time.Second),
+		},
 		autorelay.WithNumRelays(1),
 		autorelay.WithMaxCandidates(100),
 		autorelay.WithBootDelay(0),
 		autorelay.WithMaxCandidateAge(20*time.Minute),
 		autorelay.WithClock(cl),
+		autorelay.WithMinInterval(time.Second),
 	)
 	defer h.Close()
 
@@ -449,18 +469,6 @@ func TestMaxAge(t *testing.T) {
 	require.Contains(t, ids, relays[0])
 }
 
-func TestIncorrectInit(t *testing.T) {
-	// Check if we panic if we do not correctly initialize the autorelay system.
-	// Common since it's easy to initialize without passing in the correct options: https://github.com/libp2p/go-libp2p/issues/1852
-
-	defer func() {
-		if r := recover(); r == nil {
-			t.Errorf("Expected to panic")
-		}
-	}()
-	_ = newPrivateNode(t)
-}
-
 func expectDeltaInAddrUpdated(t *testing.T, addrUpdated event.Subscription, expectedDelta int) {
 	t.Helper()
 	delta := 0
@@ -507,8 +515,8 @@ func TestReconnectToStaticRelays(t *testing.T) {
 		staticRelays = append(staticRelays, peer.AddrInfo{ID: r.ID(), Addrs: r.Addrs()})
 	}
 
-	h := newPrivateNode(t,
-		autorelay.WithStaticRelays(staticRelays),
+	h := newPrivateNodeWithStaticRelays(t,
+		staticRelays,
 		autorelay.WithClock(cl),
 	)
 
@@ -532,3 +540,25 @@ func TestReconnectToStaticRelays(t *testing.T) {
 	cl.Add(time.Hour)
 	expectDeltaInAddrUpdated(t, addrUpdated, -1)
 }
+
+func TestMinInterval(t *testing.T) {
+	h := newPrivateNode(t,
+		func(context.Context, int) <-chan peer.AddrInfo {
+			peerChan := make(chan peer.AddrInfo, 1)
+			defer close(peerChan)
+			r1 := newRelay(t)
+			t.Cleanup(func() { r1.Close() })
+			peerChan <- peer.AddrInfo{ID: r1.ID(), Addrs: r1.Addrs()}
+			return peerChan
+		},
+		autorelay.WithMinCandidates(2),
+		autorelay.WithNumRelays(1),
+		autorelay.WithBootDelay(time.Hour),
+		autorelay.WithMinInterval(500*time.Millisecond),
+	)
+	defer h.Close()
+
+	// The second call to peerSource should happen after 1 second
+	require.Never(t, func() bool { return numRelays(h) > 0 }, 500*time.Millisecond, 100*time.Millisecond)
+	require.Eventually(t, func() bool { return numRelays(h) > 0 }, 1*time.Second, 100*time.Millisecond)
+}
diff --git a/p2p/host/eventbus/basic_metrics_test.go b/p2p/host/eventbus/basic_metrics_test.go
index 8322f73e..d27fcbe1 100644
--- a/p2p/host/eventbus/basic_metrics_test.go
+++ b/p2p/host/eventbus/basic_metrics_test.go
@@ -9,8 +9,11 @@ import (
 
 func BenchmarkEventEmitted(b *testing.B) {
 	b.ReportAllocs()
-	types := []reflect.Type{reflect.TypeOf(new(event.EvtLocalAddressesUpdated)), reflect.TypeOf(new(event.EvtNATDeviceTypeChanged)),
-		reflect.TypeOf(new(event.EvtLocalProtocolsUpdated))}
+	types := []reflect.Type{
+		reflect.TypeOf(new(event.EvtLocalAddressesUpdated)),
+		reflect.TypeOf(new(event.EvtNATDeviceTypeChanged)),
+		reflect.TypeOf(new(event.EvtLocalProtocolsUpdated)),
+	}
 	mt := NewMetricsTracer()
 	for i := 0; i < b.N; i++ {
 		mt.EventEmitted(types[i%len(types)])
diff --git a/p2p/metricshelper/pool_test.go b/p2p/metricshelper/pool_test.go
new file mode 100644
index 00000000..85021e55
--- /dev/null
+++ b/p2p/metricshelper/pool_test.go
@@ -0,0 +1,21 @@
+package metricshelper
+
+import (
+	"math/rand"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestStringSlicePool(t *testing.T) {
+	for i := 0; i < 1e5; i++ {
+		s := GetStringSlice()
+		require.Empty(t, *s)
+		require.Equal(t, 8, cap(*s))
+		*s = append(*s, "foo")
+		*s = append(*s, "bar")
+		if rand.Int()%3 == 0 {
+			PutStringSlice(s)
+		}
+	}
+}
diff --git a/p2p/protocol/holepunch/holepunch_test.go b/p2p/protocol/holepunch/holepunch_test.go
index 95b3991f..9b4d7f80 100644
--- a/p2p/protocol/holepunch/holepunch_test.go
+++ b/p2p/protocol/holepunch/holepunch_test.go
@@ -417,9 +417,9 @@ func mkHostWithStaticAutoRelay(t *testing.T, relay host.Host) host.Host {
 	h, err := libp2p.New(
 		libp2p.ListenAddrs(ma.StringCast("/ip4/127.0.0.1/tcp/0")),
 		libp2p.EnableRelay(),
-		libp2p.EnableAutoRelay(
+		libp2p.EnableAutoRelayWithStaticRelays(
+			[]peer.AddrInfo{pi},
 			autorelay.WithCircuitV1Support(),
-			autorelay.WithStaticRelays([]peer.AddrInfo{pi}),
 		),
 		libp2p.ForceReachabilityPrivate(),
 		libp2p.ResourceManager(&network.NullResourceManager{}),
diff --git a/p2p/transport/webtransport/crypto_test.go b/p2p/transport/webtransport/crypto_test.go
index 143cd6de..ba439c28 100644
--- a/p2p/transport/webtransport/crypto_test.go
+++ b/p2p/transport/webtransport/crypto_test.go
@@ -138,25 +138,23 @@ func TestDeterministicCertHashes(t *testing.T) {
 	// Run this test 1000 times since we want to make sure the signatures are deterministic
 	runs := 1000
 	for i := 0; i < runs; i++ {
-		t.Run(fmt.Sprintf("Run=%d", i), func(t *testing.T) {
-			zeroSeed := [32]byte{}
-			priv, _, err := ic.GenerateEd25519Key(bytes.NewReader(zeroSeed[:]))
-			require.NoError(t, err)
-			cert, certPriv, err := generateCert(priv, time.Time{}, time.Time{}.Add(time.Hour*24*14))
-			require.NoError(t, err)
-
-			keyBytes, err := x509.MarshalECPrivateKey(certPriv)
-			require.NoError(t, err)
-
-			cert2, certPriv2, err := generateCert(priv, time.Time{}, time.Time{}.Add(time.Hour*24*14))
-			require.NoError(t, err)
-
-			require.Equal(t, cert2.Signature, cert.Signature)
-			require.Equal(t, cert2.Raw, cert.Raw)
-			keyBytes2, err := x509.MarshalECPrivateKey(certPriv2)
-			require.NoError(t, err)
-			require.Equal(t, keyBytes, keyBytes2)
-		})
+		zeroSeed := [32]byte{}
+		priv, _, err := ic.GenerateEd25519Key(bytes.NewReader(zeroSeed[:]))
+		require.NoError(t, err)
+		cert, certPriv, err := generateCert(priv, time.Time{}, time.Time{}.Add(time.Hour*24*14))
+		require.NoError(t, err)
+
+		keyBytes, err := x509.MarshalECPrivateKey(certPriv)
+		require.NoError(t, err)
+
+		cert2, certPriv2, err := generateCert(priv, time.Time{}, time.Time{}.Add(time.Hour*24*14))
+		require.NoError(t, err)
+
+		require.Equal(t, cert2.Signature, cert.Signature)
+		require.Equal(t, cert2.Raw, cert.Raw)
+		keyBytes2, err := x509.MarshalECPrivateKey(certPriv2)
+		require.NoError(t, err)
+		require.Equal(t, keyBytes, keyBytes2)
 	}
 }
 
@@ -168,33 +166,31 @@ func TestDeterministicSig(t *testing.T) {
 	// Run this test 1000 times since we want to make sure the signatures are deterministic
 	runs := 1000
 	for i := 0; i < runs; i++ {
-		t.Run(fmt.Sprintf("Run=%d", i), func(t *testing.T) {
-			zeroSeed := [32]byte{}
-			deterministicHKDFReader := newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo)
-			b := [1024]byte{}
-			io.ReadFull(deterministicHKDFReader, b[:])
-			caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
-			require.NoError(t, err)
-
-			sig, err := caPrivateKey.Sign(deterministicHKDFReader, b[:], crypto.SHA256)
-			require.NoError(t, err)
-
-			deterministicHKDFReader = newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo)
-			b2 := [1024]byte{}
-			io.ReadFull(deterministicHKDFReader, b2[:])
-			caPrivateKey2, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
-			require.NoError(t, err)
-
-			sig2, err := caPrivateKey2.Sign(deterministicHKDFReader, b2[:], crypto.SHA256)
-			require.NoError(t, err)
-
-			keyBytes, err := x509.MarshalECPrivateKey(caPrivateKey)
-			require.NoError(t, err)
-			keyBytes2, err := x509.MarshalECPrivateKey(caPrivateKey2)
-			require.NoError(t, err)
-
-			require.Equal(t, sig, sig2)
-			require.Equal(t, keyBytes, keyBytes2)
-		})
+		zeroSeed := [32]byte{}
+		deterministicHKDFReader := newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo)
+		b := [1024]byte{}
+		io.ReadFull(deterministicHKDFReader, b[:])
+		caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
+		require.NoError(t, err)
+
+		sig, err := caPrivateKey.Sign(deterministicHKDFReader, b[:], crypto.SHA256)
+		require.NoError(t, err)
+
+		deterministicHKDFReader = newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo)
+		b2 := [1024]byte{}
+		io.ReadFull(deterministicHKDFReader, b2[:])
+		caPrivateKey2, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader)
+		require.NoError(t, err)
+
+		sig2, err := caPrivateKey2.Sign(deterministicHKDFReader, b2[:], crypto.SHA256)
+		require.NoError(t, err)
+
+		keyBytes, err := x509.MarshalECPrivateKey(caPrivateKey)
+		require.NoError(t, err)
+		keyBytes2, err := x509.MarshalECPrivateKey(caPrivateKey2)
+		require.NoError(t, err)
+
+		require.Equal(t, sig, sig2)
+		require.Equal(t, keyBytes, keyBytes2)
 	}
 }