cleanup: move caching resolvers from netx to netxlite (#799)

Now that we have properly refactored the caching resolvers we can
move them into netxlite as optional resolvers created using the
proper abstract factories we just added.

This diff reduces the complexity and the code size of netx.

See https://github.com/ooni/probe/issues/2121.
This commit is contained in:
Simone Basso
2022-06-05 21:58:34 +02:00
committed by GitHub
parent 6b85dfce88
commit 5d54aa9c5f
5 changed files with 34 additions and 36 deletions
-120
View File
@@ -1,120 +0,0 @@
package netx
import (
"context"
"net"
"sync"
"github.com/ooni/probe-cli/v3/internal/model"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
// MaybeWrapWithCachingResolver wraps the provided resolver with a resolver
// that remembers the result of previous successful resolutions, if the enabled
// argument is true. Otherwise, we return the unmodified provided resolver.
//
// Bug: the returned resolver only applies caching to LookupHost and any other
// lookup operation returns ErrNoDNSTransport to the caller.
func MaybeWrapWithCachingResolver(enabled bool, reso model.Resolver) model.Resolver {
if enabled {
reso = &cacheResolver{
cache: map[string][]string{},
mu: sync.Mutex{},
readOnly: false,
resolver: reso,
}
}
return reso
}
// MaybeWrapWithStaticDNSCache wraps the provided resolver with a resolver that
// checks the given cache before issuing queries to the underlying DNS resolver.
//
// Bug: the returned resolver only applies caching to LookupHost and any other
// lookup operation returns ErrNoDNSTransport to the caller.
func MaybeWrapWithStaticDNSCache(cache map[string][]string, reso model.Resolver) model.Resolver {
if len(cache) > 0 {
reso = &cacheResolver{
cache: cache,
mu: sync.Mutex{},
readOnly: true,
resolver: reso,
}
}
return reso
}
// cacheResolver implements CachingResolver and StaticDNSCache.
type cacheResolver struct {
// cache is the underlying DNS cache.
cache map[string][]string
// mu provides mutual exclusion.
mu sync.Mutex
// readOnly means that we won't cache the result of successful resolutions.
readOnly bool
// resolver is the underlying resolver.
resolver model.Resolver
}
var _ model.Resolver = &cacheResolver{}
// LookupHost implements model.Resolver.LookupHost
func (r *cacheResolver) LookupHost(
ctx context.Context, hostname string) ([]string, error) {
if entry := r.get(hostname); entry != nil {
return entry, nil
}
entry, err := r.resolver.LookupHost(ctx, hostname)
if err != nil {
return nil, err
}
if !r.readOnly {
r.set(hostname, entry)
}
return entry, nil
}
// get gets the currently configured entry for domain, or nil
func (r *cacheResolver) get(domain string) []string {
r.mu.Lock()
defer r.mu.Unlock()
return r.cache[domain]
}
// set sets a valid inside the cache iff readOnly is false.
func (r *cacheResolver) set(domain string, addresses []string) {
r.mu.Lock()
if r.cache == nil {
r.cache = make(map[string][]string)
}
r.cache[domain] = addresses
r.mu.Unlock()
}
// Address implements model.Resolver.Address.
func (r *cacheResolver) Address() string {
return r.resolver.Address()
}
// Network implements model.Resolver.Network.
func (r *cacheResolver) Network() string {
return r.resolver.Network()
}
// CloseIdleConnections implements model.Resolver.CloseIdleConnections.
func (r *cacheResolver) CloseIdleConnections() {
r.resolver.CloseIdleConnections()
}
// LookupHTTPS implements model.Resolver.LookupHTTPS.
func (r *cacheResolver) LookupHTTPS(ctx context.Context, domain string) (*model.HTTPSSvc, error) {
return nil, netxlite.ErrNoDNSTransport
}
// LookupNS implements model.Resolver.LookupNS.
func (r *cacheResolver) LookupNS(ctx context.Context, domain string) ([]*net.NS, error) {
return nil, netxlite.ErrNoDNSTransport
}
-206
View File
@@ -1,206 +0,0 @@
package netx
import (
"context"
"errors"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ooni/probe-cli/v3/internal/model/mocks"
"github.com/ooni/probe-cli/v3/internal/netxlite"
)
func TestMaybeWrapWithCachingResolver(t *testing.T) {
t.Run("with enable equal to true", func(t *testing.T) {
underlying := &mocks.Resolver{}
reso := MaybeWrapWithCachingResolver(true, underlying)
cachereso := reso.(*cacheResolver)
if cachereso.resolver != underlying {
t.Fatal("did not wrap correctly")
}
})
t.Run("with enable equal to false", func(t *testing.T) {
underlying := &mocks.Resolver{}
reso := MaybeWrapWithCachingResolver(false, underlying)
if reso != underlying {
t.Fatal("unexpected result")
}
})
}
func TestMaybeWrapWithStaticDNSCache(t *testing.T) {
t.Run("when the cache is not empty", func(t *testing.T) {
cachedDomain := "dns.google"
expectedEntry := []string{"8.8.8.8", "8.8.4.4"}
underlyingCache := make(map[string][]string)
underlyingCache[cachedDomain] = expectedEntry
underlyingReso := &mocks.Resolver{}
reso := MaybeWrapWithStaticDNSCache(underlyingCache, underlyingReso)
cachereso := reso.(*cacheResolver)
if diff := cmp.Diff(cachereso.cache, underlyingCache); diff != "" {
t.Fatal(diff)
}
if cachereso.resolver != underlyingReso {
t.Fatal("unexpected underlying resolver")
}
})
t.Run("when the cache is empty", func(t *testing.T) {
underlyingCache := make(map[string][]string)
underlyingReso := &mocks.Resolver{}
reso := MaybeWrapWithStaticDNSCache(underlyingCache, underlyingReso)
if reso != underlyingReso {
t.Fatal("unexpected result")
}
})
t.Run("when the cache is nil", func(t *testing.T) {
var underlyingCache map[string][]string
underlyingReso := &mocks.Resolver{}
reso := MaybeWrapWithStaticDNSCache(underlyingCache, underlyingReso)
if reso != underlyingReso {
t.Fatal("unexpected result")
}
})
}
func TestCacheResolver(t *testing.T) {
t.Run("LookupHost", func(t *testing.T) {
t.Run("cache miss and failure", func(t *testing.T) {
expected := errors.New("mocked error")
r := &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, expected
},
}
cache := &cacheResolver{resolver: r}
addrs, err := cache.LookupHost(context.Background(), "www.google.com")
if !errors.Is(err, expected) {
t.Fatal("not the error we expected")
}
if addrs != nil {
t.Fatal("expected nil addrs here")
}
if cache.get("www.google.com") != nil {
t.Fatal("expected empty cache here")
}
})
t.Run("cache hit", func(t *testing.T) {
expected := errors.New("mocked error")
r := &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return nil, expected
},
}
cache := &cacheResolver{resolver: r}
cache.set("dns.google.com", []string{"8.8.8.8"})
addrs, err := cache.LookupHost(context.Background(), "dns.google.com")
if err != nil {
t.Fatal(err)
}
if len(addrs) != 1 || addrs[0] != "8.8.8.8" {
t.Fatal("not the result we expected")
}
})
t.Run("cache miss and success with readwrite cache", func(t *testing.T) {
r := &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{"8.8.8.8"}, nil
},
}
cache := &cacheResolver{resolver: r}
addrs, err := cache.LookupHost(context.Background(), "dns.google.com")
if err != nil {
t.Fatal(err)
}
if len(addrs) != 1 || addrs[0] != "8.8.8.8" {
t.Fatal("not the result we expected")
}
if cache.get("dns.google.com")[0] != "8.8.8.8" {
t.Fatal("expected full cache here")
}
})
t.Run("cache miss and success with readonly cache", func(t *testing.T) {
r := &mocks.Resolver{
MockLookupHost: func(ctx context.Context, domain string) ([]string, error) {
return []string{"8.8.8.8"}, nil
},
}
cache := &cacheResolver{resolver: r, readOnly: true}
addrs, err := cache.LookupHost(context.Background(), "dns.google.com")
if err != nil {
t.Fatal(err)
}
if len(addrs) != 1 || addrs[0] != "8.8.8.8" {
t.Fatal("not the result we expected")
}
if cache.get("dns.google.com") != nil {
t.Fatal("expected empty cache here")
}
})
t.Run("Address", func(t *testing.T) {
underlying := &mocks.Resolver{
MockAddress: func() string {
return "x"
},
}
reso := &cacheResolver{resolver: underlying}
if reso.Address() != "x" {
t.Fatal("unexpected result")
}
})
t.Run("Network", func(t *testing.T) {
underlying := &mocks.Resolver{
MockNetwork: func() string {
return "x"
},
}
reso := &cacheResolver{resolver: underlying}
if reso.Network() != "x" {
t.Fatal("unexpected result")
}
})
t.Run("CloseIdleConnections", func(t *testing.T) {
var called bool
underlying := &mocks.Resolver{
MockCloseIdleConnections: func() {
called = true
},
}
reso := &cacheResolver{resolver: underlying}
reso.CloseIdleConnections()
if !called {
t.Fatal("not called")
}
})
t.Run("LookupHTTPS", func(t *testing.T) {
reso := &cacheResolver{}
https, err := reso.LookupHTTPS(context.Background(), "dns.google")
if !errors.Is(err, netxlite.ErrNoDNSTransport) {
t.Fatal("unexpected err", err)
}
if https != nil {
t.Fatal("expected nil")
}
})
t.Run("LookupNS", func(t *testing.T) {
reso := &cacheResolver{}
ns, err := reso.LookupNS(context.Background(), "dns.google")
if !errors.Is(err, netxlite.ErrNoDNSTransport) {
t.Fatal("unexpected err", err)
}
if len(ns) != 0 {
t.Fatal("expected zero length slice")
}
})
})
}
+2 -2
View File
@@ -67,8 +67,8 @@ func NewResolver(config Config) model.Resolver {
model.ValidLoggerOrDefault(config.Logger),
config.BaseResolver,
)
r = MaybeWrapWithCachingResolver(config.CacheResolutions, r)
r = MaybeWrapWithStaticDNSCache(config.DNSCache, r)
r = netxlite.MaybeWrapWithCachingResolver(config.CacheResolutions, r)
r = netxlite.MaybeWrapWithStaticDNSCache(config.DNSCache, r)
r = netxlite.MaybeWrapWithBogonResolver(config.BogonIsError, r)
return config.Saver.WrapResolver(r) // WAI when config.Saver==nil
}