diff --git a/go.mod b/go.mod index d4b4208336..a280949e94 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( golang.org/x/sync v0.0.0-20181108010431-42b317875d0f golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 golang.org/x/text v0.3.2 + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce gopkg.in/olebedev/go-duktape.v3 v3.0.0-20190213234257-ec84240a7772 gopkg.in/sourcemap.v1 v1.0.5 // indirect diff --git a/p2p/dnsdisc/client.go b/p2p/dnsdisc/client.go index b3f6174b9d..9aadd2d979 100644 --- a/p2p/dnsdisc/client.go +++ b/p2p/dnsdisc/client.go @@ -32,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enr" lru "github.com/hashicorp/golang-lru" + "golang.org/x/time/rate" ) // Client discovers nodes by querying DNS servers. @@ -46,6 +47,7 @@ type Config struct { Timeout time.Duration // timeout used for DNS lookups (default 5s) RecheckInterval time.Duration // time between tree root update checks (default 30min) CacheLimit int // maximum number of cached records (default 1000) + RateLimit float64 // maximum DNS requests / second (default 4) ValidSchemes enr.IdentityScheme // acceptable ENR identity schemes (default enode.ValidSchemes) Resolver Resolver // the DNS resolver to use (defaults to system DNS) Logger log.Logger // destination of client log messages (defaults to root logger) @@ -58,9 +60,10 @@ type Resolver interface { func (cfg Config) withDefaults() Config { const ( - defaultTimeout = 5 * time.Second - defaultRecheck = 30 * time.Minute - defaultCache = 1000 + defaultTimeout = 5 * time.Second + defaultRecheck = 30 * time.Minute + defaultRateLimit = 3 + defaultCache = 1000 ) if cfg.Timeout == 0 { cfg.Timeout = defaultTimeout @@ -71,6 +74,9 @@ func (cfg Config) withDefaults() Config { if cfg.CacheLimit == 0 { cfg.CacheLimit = defaultCache } + if cfg.RateLimit == 0 { + cfg.RateLimit = defaultRateLimit + } if cfg.ValidSchemes == nil { cfg.ValidSchemes = enode.ValidSchemes } @@ -85,15 +91,14 @@ func (cfg Config) withDefaults() Config { // NewClient creates a client. func NewClient(cfg Config) *Client { - c := &Client{ - cfg: cfg.withDefaults(), - clock: mclock.System{}, - } - var err error - if c.entries, err = lru.New(c.cfg.CacheLimit); err != nil { + cfg = cfg.withDefaults() + cache, err := lru.New(cfg.CacheLimit) + if err != nil { panic(err) } - return c + rlimit := rate.NewLimiter(rate.Limit(cfg.RateLimit), 10) + cfg.Resolver = &rateLimitResolver{cfg.Resolver, rlimit} + return &Client{cfg: cfg, entries: cache, clock: mclock.System{}} } // SyncTree downloads the entire node tree at the given URL. @@ -191,6 +196,19 @@ func (c *Client) doResolveEntry(ctx context.Context, domain, hash string) (entry return nil, nameError{name, errNoEntry} } +// rateLimitResolver applies a rate limit to a Resolver. +type rateLimitResolver struct { + r Resolver + limiter *rate.Limiter +} + +func (r *rateLimitResolver) LookupTXT(ctx context.Context, domain string) ([]string, error) { + if err := r.limiter.Wait(ctx); err != nil { + return nil, err + } + return r.r.LookupTXT(ctx, domain) +} + // randomIterator traverses a set of trees and returns nodes found in them. type randomIterator struct { cur *enode.Node diff --git a/p2p/dnsdisc/client_test.go b/p2p/dnsdisc/client_test.go index 34bdb01d92..a2de2ee7a2 100644 --- a/p2p/dnsdisc/client_test.go +++ b/p2p/dnsdisc/client_test.go @@ -101,7 +101,11 @@ func TestIterator(t *testing.T) { nodes := testNodes(nodesSeed1, 30) tree, url := makeTestTree("n", nodes, nil) r := mapResolver(tree.ToTXT("n")) - c := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)}) + c := NewClient(Config{ + Resolver: r, + Logger: testlog.Logger(t, log.LvlTrace), + RateLimit: 500, + }) it, err := c.NewIterator(url) if err != nil { t.Fatal(err) @@ -139,8 +143,9 @@ func TestIteratorLinks(t *testing.T) { tree1, url1 := makeTestTree("t1", nodes[:10], nil) tree2, url2 := makeTestTree("t2", nodes[10:], []string{url1}) c := NewClient(Config{ - Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")), - Logger: testlog.Logger(t, log.LvlTrace), + Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")), + Logger: testlog.Logger(t, log.LvlTrace), + RateLimit: 500, }) it, err := c.NewIterator(url2) if err != nil { @@ -161,6 +166,7 @@ func TestIteratorNodeUpdates(t *testing.T) { Resolver: resolver, Logger: testlog.Logger(t, log.LvlTrace), RecheckInterval: 20 * time.Minute, + RateLimit: 500, }) ) c.clock = clock @@ -202,6 +208,7 @@ func TestIteratorLinkUpdates(t *testing.T) { Resolver: resolver, Logger: testlog.Logger(t, log.LvlTrace), RecheckInterval: 20 * time.Minute, + RateLimit: 500, }) ) c.clock = clock