p2p/dnsdisc: add enode.Iterator API (#20437)

* p2p/dnsdisc: add support for enode.Iterator

This changes the dnsdisc.Client API to support the enode.Iterator
interface.

* p2p/dnsdisc: rate-limit DNS requests

* p2p/dnsdisc: preserve linked trees across root updates

This improves the way links are handled when the link root changes.
Previously, sync would simply remove all links from the current tree and
garbage-collect all unreachable trees before syncing the new list of
links.

This behavior isn't great in certain cases: Consider a structure where
trees A, B, and C reference each other and D links to A. If D's link
root changed, the sync code would first remove trees A, B and C, only to
re-sync them later when the link to A was found again.

The fix for this problem is to track the current set of links in each
clientTree and removing old links only AFTER all links are synced.

* p2p/dnsdisc: deflake iterator test

* cmd/devp2p: adapt dnsClient to new p2p/dnsdisc API

* p2p/dnsdisc: tiny comment fix
This commit is contained in:
Felix Lange
2019-12-12 10:15:36 +01:00
committed by Péter Szilágyi
parent d90d1db609
commit 191364c350
8 changed files with 394 additions and 220 deletions

View File

@ -54,7 +54,7 @@ func TestClientSyncTree(t *testing.T) {
wantSeq = uint(1)
)
c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
c := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
stree, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
if err != nil {
t.Fatal("sync error:", err)
@ -68,9 +68,6 @@ func TestClientSyncTree(t *testing.T) {
if stree.Seq() != wantSeq {
t.Errorf("synced tree has wrong seq: %d", stree.Seq())
}
if len(c.trees) > 0 {
t.Errorf("tree from SyncTree added to client")
}
}
// In this test, syncing the tree fails because it contains an invalid ENR entry.
@ -91,7 +88,7 @@ func TestClientSyncTreeBadNode(t *testing.T) {
"C7HRFPF3BLGF3YR4DY5KX3SMBE.n": "enrtree://AM5FCQLWIZX2QFPNJAP7VUERCCRNGRHWZG3YYHIUV7BVDQ5FDPRT2@morenodes.example.org",
"INDMVBZEEQ4ESVYAKGIYU74EAA.n": "enr:-----",
}
c, _ := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
c := NewClient(Config{Resolver: r, Logger: testlog.Logger(t, log.LvlTrace)})
_, err := c.SyncTree("enrtree://AKPYQIUQIL7PSIACI32J7FGZW56E5FKHEFCCOFHILBIMW3M6LWXS2@n")
wantErr := nameError{name: "INDMVBZEEQ4ESVYAKGIYU74EAA.n", err: entryError{typ: "enr", err: errInvalidENR}}
if err != wantErr {
@ -99,57 +96,89 @@ func TestClientSyncTreeBadNode(t *testing.T) {
}
}
// This test checks that RandomNode hits all entries.
func TestClientRandomNode(t *testing.T) {
// This test checks that randomIterator finds all entries.
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)})
if err := c.AddTree(url); err != nil {
c := NewClient(Config{
Resolver: r,
Logger: testlog.Logger(t, log.LvlTrace),
RateLimit: 500,
})
it, err := c.NewIterator(url)
if err != nil {
t.Fatal(err)
}
checkRandomNode(t, c, nodes)
checkIterator(t, it, nodes)
}
// This test checks that RandomNode traverses linked trees as well as explicitly added trees.
func TestClientRandomNodeLinks(t *testing.T) {
// This test checks if closing randomIterator races.
func TestIteratorClose(t *testing.T) {
nodes := testNodes(nodesSeed1, 500)
tree1, url1 := makeTestTree("t1", nodes, nil)
c := NewClient(Config{Resolver: newMapResolver(tree1.ToTXT("t1"))})
it, err := c.NewIterator(url1)
if err != nil {
t.Fatal(err)
}
done := make(chan struct{})
go func() {
for it.Next() {
_ = it.Node()
}
close(done)
}()
time.Sleep(50 * time.Millisecond)
it.Close()
<-done
}
// This test checks that randomIterator traverses linked trees as well as explicitly added trees.
func TestIteratorLinks(t *testing.T) {
nodes := testNodes(nodesSeed1, 40)
tree1, url1 := makeTestTree("t1", nodes[:10], nil)
tree2, url2 := makeTestTree("t2", nodes[10:], []string{url1})
cfg := Config{
Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
Logger: testlog.Logger(t, log.LvlTrace),
}
c, _ := NewClient(cfg)
if err := c.AddTree(url2); err != nil {
c := NewClient(Config{
Resolver: newMapResolver(tree1.ToTXT("t1"), tree2.ToTXT("t2")),
Logger: testlog.Logger(t, log.LvlTrace),
RateLimit: 500,
})
it, err := c.NewIterator(url2)
if err != nil {
t.Fatal(err)
}
checkRandomNode(t, c, nodes)
checkIterator(t, it, nodes)
}
// This test verifies that RandomNode re-checks the root of the tree to catch
// This test verifies that randomIterator re-checks the root of the tree to catch
// updates to nodes.
func TestClientRandomNodeUpdates(t *testing.T) {
func TestIteratorNodeUpdates(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
cfg = Config{
c = NewClient(Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
}
c, _ = NewClient(cfg)
RateLimit: 500,
})
)
c.clock = clock
tree1, url := makeTestTree("n", nodes[:25], nil)
it, err := c.NewIterator(url)
if err != nil {
t.Fatal(err)
}
// Sync the original tree.
// sync the original tree.
resolver.add(tree1.ToTXT("n"))
c.AddTree(url)
checkRandomNode(t, c, nodes[:25])
checkIterator(t, it, nodes[:25])
// Update some nodes and ensure RandomNode returns the new nodes as well.
keys := testKeys(nodesSeed1, len(nodes))
@ -162,25 +191,25 @@ func TestClientRandomNodeUpdates(t *testing.T) {
nodes[i] = n2
}
tree2, _ := makeTestTree("n", nodes, nil)
clock.Run(cfg.RecheckInterval + 1*time.Second)
clock.Run(c.cfg.RecheckInterval + 1*time.Second)
resolver.clear()
resolver.add(tree2.ToTXT("n"))
checkRandomNode(t, c, nodes)
checkIterator(t, it, nodes)
}
// This test verifies that RandomNode re-checks the root of the tree to catch
// This test verifies that randomIterator re-checks the root of the tree to catch
// updates to links.
func TestClientRandomNodeLinkUpdates(t *testing.T) {
func TestIteratorLinkUpdates(t *testing.T) {
var (
clock = new(mclock.Simulated)
nodes = testNodes(nodesSeed1, 30)
resolver = newMapResolver()
cfg = Config{
c = NewClient(Config{
Resolver: resolver,
Logger: testlog.Logger(t, log.LvlTrace),
RecheckInterval: 20 * time.Minute,
}
c, _ = NewClient(cfg)
RateLimit: 500,
})
)
c.clock = clock
tree3, url3 := makeTestTree("t3", nodes[20:30], nil)
@ -190,49 +219,53 @@ func TestClientRandomNodeLinkUpdates(t *testing.T) {
resolver.add(tree2.ToTXT("t2"))
resolver.add(tree3.ToTXT("t3"))
it, err := c.NewIterator(url1)
if err != nil {
t.Fatal(err)
}
// Sync tree1 using RandomNode.
c.AddTree(url1)
checkRandomNode(t, c, nodes[:20])
checkIterator(t, it, nodes[:20])
// Add link to tree3, remove link to tree2.
tree1, _ = makeTestTree("t1", nodes[:10], []string{url3})
resolver.add(tree1.ToTXT("t1"))
clock.Run(cfg.RecheckInterval + 1*time.Second)
clock.Run(c.cfg.RecheckInterval + 1*time.Second)
t.Log("tree1 updated")
var wantNodes []*enode.Node
wantNodes = append(wantNodes, tree1.Nodes()...)
wantNodes = append(wantNodes, tree3.Nodes()...)
checkRandomNode(t, c, wantNodes)
checkIterator(t, it, wantNodes)
// Check that linked trees are GCed when they're no longer referenced.
if len(c.trees) != 2 {
t.Errorf("client knows %d trees, want 2", len(c.trees))
knownTrees := it.(*randomIterator).trees
if len(knownTrees) != 2 {
t.Errorf("client knows %d trees, want 2", len(knownTrees))
}
}
func checkRandomNode(t *testing.T, c *Client, wantNodes []*enode.Node) {
func checkIterator(t *testing.T, it enode.Iterator, wantNodes []*enode.Node) {
t.Helper()
var (
want = make(map[enode.ID]*enode.Node)
maxCalls = len(wantNodes) * 2
maxCalls = len(wantNodes) * 3
calls = 0
ctx = context.Background()
)
for _, n := range wantNodes {
want[n.ID()] = n
}
for ; len(want) > 0 && calls < maxCalls; calls++ {
n := c.RandomNode(ctx)
if n == nil {
t.Fatalf("RandomNode returned nil (call %d)", calls)
if !it.Next() {
t.Fatalf("Next returned false (call %d)", calls)
}
n := it.Node()
delete(want, n.ID())
}
t.Logf("checkRandomNode called RandomNode %d times to find %d nodes", calls, len(wantNodes))
t.Logf("checkIterator called Next %d times to find %d nodes", calls, len(wantNodes))
for _, n := range want {
t.Errorf("RandomNode didn't discover node %v", n.ID())
t.Errorf("iterator didn't discover node %v", n.ID())
}
}