Skip to content

Commit

Permalink
feat(netcore): use *net.Dialer *net.Resolver singletons (#65)
Browse files Browse the repository at this point in the history
Rather than creating a new instance for each dialing attempt, which may
create unnecessary GC pressure, allow for using singletons. However,
also allow for customising the `*net.Dialer` and/or `*net.Resolver` to
use, via optional functions.
  • Loading branch information
bassosimone authored Feb 10, 2025
1 parent 23cf20f commit 4927232
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 28 deletions.
17 changes: 12 additions & 5 deletions netcore/dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,18 +84,25 @@ func (nx *Network) dialLog(ctx context.Context, network, address string) (net.Co
return conn, err
}

// defaultDialer is the default [*net.Dialer] we use.
var defaultDialer = func() *net.Dialer {
dialer := &net.Dialer{}
dialer.SetMultipathTCP(false)
return dialer
}()

// dialNet dials using the net package or the configured dialing override.
func (nx *Network) dialNet(ctx context.Context, network, address string) (net.Conn, error) {
// if there's an user provided dialer func, use it
if nx.DialContextFunc != nil {
return nx.DialContextFunc(ctx, network, address)
}

// otherwise use the net package
// TODO(bassosimone): either make multipath TCP configurable
// or document that we disable it by default
child := &net.Dialer{}
child.SetMultipathTCP(false)
// otherwise fallback to a default dialer
child := defaultDialer
if nx.NewDialerOrSingleton != nil {
child = nx.NewDialerOrSingleton()
}
return child.DialContext(ctx, network, address)
}

Expand Down
24 changes: 22 additions & 2 deletions netcore/dialer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func TestNetwork_dialLog(t *testing.T) {
}

func TestNetwork_dialNet(t *testing.T) {
t.Run("using custom dialer", func(t *testing.T) {
t.Run("using custom DialContextFunc", func(t *testing.T) {
mockConn := &mocks.Conn{}
nx := &Network{
DialContextFunc: func(ctx context.Context, network, address string) (net.Conn, error) {
Expand All @@ -369,7 +369,27 @@ func TestNetwork_dialNet(t *testing.T) {
assert.Equal(t, mockConn, conn)
})

t.Run("using net package", func(t *testing.T) {
t.Run("using custom NewDialerOrSingleton", func(t *testing.T) {
// create a server using localhost to test against
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

nx := &Network{
NewDialerOrSingleton: func() *net.Dialer {
d := &net.Dialer{}
return d
},
}
parsed := runtimex.Try1(url.Parse(server.URL))
conn, err := nx.dialNet(context.Background(), "tcp", parsed.Host)
assert.NoError(t, err)
assert.NotNil(t, conn)
conn.Close()
})

t.Run("using the default dialer", func(t *testing.T) {
// create a server using localhost to test against
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down
16 changes: 16 additions & 0 deletions netcore/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ type Network struct {
// DialContextTimeout is the optional timeout to use for limiting
// the maximum time spent creating a single connection.
DialContextTimeout time.Duration

// NewResolverOrSingleton is the optional function that returns
// the [*net.Resolver] to use when LookupHostFunc is not set. As the
// name suggests, this function may either create a new [*net.Resolver]
// for each call or just return a singleton instance. When this method
// is not set, we use an internal zero-initialized, static [*net.Resolver].
NewResolverOrSingleton func() *net.Resolver

// NewDialerOrSingleton is the optional function that returns
// the [*net.Dialer] to use when DialContextFunc is not set. As the
// name suggests, this function may either create a new [*net.Dialer]
// for each call or just return a singleton instance. When this method
// is not set, we use an internal, static [*net.Dialer] where
// support for Multipath TCP has been disabled. We disable Multipath
// TCP because we focus on precise internet measurements.
NewDialerOrSingleton func() *net.Dialer
}

// DefaultNetwork is the default [*Network] used by this package.
Expand Down
17 changes: 8 additions & 9 deletions netcore/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,8 @@ func (nx *Network) maybeLookupHost(ctx context.Context, domain string) ([]string
return addrs, err
}

// avoidEditingResolver is the default [maybeEditResolver] implementation.
func avoidEditingResolver(reso *net.Resolver) *net.Resolver {
return reso
}

// maybeEditResolver allows editing the [*net.Resolver] used by unit tests.
var maybeEditResolver = avoidEditingResolver
// defaultResolver is the [*net.Resolver] we use by default.
var defaultResolver = &net.Resolver{}

// doLookupHost performs the DNS lookup.
func (nx *Network) doLookupHost(ctx context.Context, domain string) ([]string, error) {
Expand All @@ -81,8 +76,12 @@ func (nx *Network) doLookupHost(ctx context.Context, domain string) ([]string, e
return nx.LookupHostFunc(ctx, domain)
}

// otherwise fallback to the system resolver
reso := maybeEditResolver(&net.Resolver{})
// otherwise either use the default [*net.Resolver] or the
// default override through NewResolverOrSingleton
reso := defaultResolver
if nx.NewResolverOrSingleton != nil {
reso = nx.NewResolverOrSingleton()
}
return reso.LookupHost(ctx, domain)
}

Expand Down
21 changes: 9 additions & 12 deletions netcore/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,16 @@ func TestNetwork_maybeLookupHost(t *testing.T) {
})

t.Run("system resolver error", func(t *testing.T) {
// Temporarily override maybeEditResolver and restore it when done
maybeEditResolver = func(reso *net.Resolver) *net.Resolver {
reso.PreferGo = true
reso.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, errors.New("mocked dial error")
}
return reso
nx := &Network{
NewResolverOrSingleton: func() *net.Resolver {
reso := &net.Resolver{}
reso.PreferGo = true
reso.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
return nil, errors.New("mocked dial error")
}
return reso
},
}
defer func() {
maybeEditResolver = avoidEditingResolver
}()

nx := &Network{}
_, err := nx.maybeLookupHost(context.Background(), "example.com")
assert.Error(t, err)
})
Expand Down

0 comments on commit 4927232

Please sign in to comment.