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
commit 5d54aa9c5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 34 additions and 36 deletions

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
}

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")
}
})
})
}

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
}