Limit number of proxied records and proxy only recently observed nodes (#1227)

* Limit number of proxied records and proxy only recently observed nodes

* Explicitly verify that limit is preserved and records removed eventually
This commit is contained in:
Dmitry Shulyak 2018-10-08 08:24:39 +03:00 committed by GitHub
parent 9f8f0089a3
commit fbd3c9ca80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 141 additions and 33 deletions

View File

@ -5,8 +5,10 @@ import (
"fmt"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discv5"
"github.com/ethereum/go-ethereum/params"
@ -22,7 +24,9 @@ var (
bootnodes = StringSlice{}
topics = StringSlice{}
les = IntSlice{}
useEthereum = flag.Bool("use-ethereum-boot", false, "If true ethereum bootnodes will be used")
useEthereum = flag.Bool("use-ethereum-boot", false, "If true ethereum bootnodes will be used.")
limit = flag.Int("limit", 100, "Limit the number of proxied nodes.")
livenessWindow = flag.Duration("liveness-window", 10*time.Minute, "Stop proxying record if it wasn't found again during specified window.")
)
func main() {
@ -65,7 +69,12 @@ func main() {
t := t
wg.Add(1)
go func() {
if err := discovery.ProxyToRendezvous(v5, rendezvousServers, t, stop); err != nil {
if err := discovery.ProxyToRendezvous(v5, stop, &event.Feed{}, discovery.ProxyOptions{
Topic: t,
Servers: rendezvousServers,
Limit: *limit,
LivenessWindow: *livenessWindow,
}); err != nil {
log.Error("proxying to rendezvous servers failed", "servers", rendezvousNodes, "topic", t, "error", err)
}
wg.Done()

View File

@ -6,26 +6,50 @@ import (
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/discv5"
"github.com/ethereum/go-ethereum/p2p/enr"
ma "github.com/multiformats/go-multiaddr"
)
const (
proxyStart = "start"
proxyStop = "stop"
)
type proxyEvent struct {
ID discv5.NodeID
Type string
Time time.Time
}
type ProxyOptions struct {
Topic string
Servers []ma.Multiaddr
Limit int
LivenessWindow time.Duration
}
// ProxyToRendezvous proxies records discovered using original to rendezvous servers for specified topic.
func ProxyToRendezvous(original Discovery, servers []ma.Multiaddr, topic string, stop chan struct{}) error {
func ProxyToRendezvous(original Discovery, stop chan struct{}, feed *event.Feed, opts ProxyOptions) error {
var (
identities = map[discv5.NodeID]*Rendezvous{}
period = make(chan time.Duration, 1)
found = make(chan *discv5.Node, 10)
lookup = make(chan bool)
wg sync.WaitGroup
identities = map[discv5.NodeID]*Rendezvous{}
lastSeen = map[discv5.NodeID]time.Time{}
closers = map[discv5.NodeID]chan struct{}{}
period = make(chan time.Duration, 1)
found = make(chan *discv5.Node, 10)
lookup = make(chan bool)
total = 0
livenessWatcher = time.NewTicker(opts.LivenessWindow / 10)
wg sync.WaitGroup
)
defer livenessWatcher.Stop()
period <- 1 * time.Second
wg.Add(1)
go func() {
if err := original.Discover(topic, period, found, lookup); err != nil {
log.Error("discover request failed", "topic", topic, "error", err)
if err := original.Discover(opts.Topic, period, found, lookup); err != nil {
log.Error("discover request failed", "topic", opts.Topic, "error", err)
}
wg.Done()
}()
@ -36,24 +60,52 @@ func ProxyToRendezvous(original Discovery, servers []ma.Multiaddr, topic string,
wg.Wait()
return nil
case <-lookup:
case <-livenessWatcher.C:
for n := range identities {
if _, exist := lastSeen[n]; !exist {
continue
}
// closeRequest is sent every time window after record was seen.
// record must be discovered again during same time window otherwise it will be removed.
if time.Since(lastSeen[n]) >= opts.LivenessWindow {
close(closers[n])
_ = identities[n].Stop()
delete(identities, n)
delete(lastSeen, n)
delete(closers, n)
total--
log.Info("proxy for a record was removed", "identity", n.String(), "total", total)
feed.Send(proxyEvent{n, proxyStop, time.Now()})
}
}
case n := <-found:
if _, exist := identities[n.ID]; exist {
_, exist := identities[n.ID]
// skip new record if we reached a limit.
if !exist && total == opts.Limit {
continue
}
log.Info("proxying new record", "topic", topic, "identity", n.String())
lastSeen[n.ID] = time.Now()
if exist {
log.Debug("received an update for existing identity", "identity", n.String())
continue
}
feed.Send(proxyEvent{n.ID, proxyStart, lastSeen[n.ID]})
total++
log.Info("proxying new record", "topic", opts.Topic, "identity", n.String(), "total", total)
record, err := makeProxiedENR(n)
if err != nil {
log.Error("error converting discovered node to ENR", "node", n.String(), "error", err)
}
r := NewRendezvousWithENR(servers, record)
r := NewRendezvousWithENR(opts.Servers, record)
identities[n.ID] = r
closers[n.ID] = make(chan struct{})
if err := r.Start(); err != nil {
log.Error("unable to start rendezvous proxying", "servers", servers, "error", err)
log.Error("unable to start rendezvous proxying", "servers", opts.Servers, "error", err)
}
wg.Add(1)
go func() {
if err := r.Register(topic, stop); err != nil {
log.Error("register error", "topic", topic, "error", err)
if err := r.Register(opts.Topic, closers[n.ID]); err != nil {
log.Error("register error", "topic", opts.Topic, "error", err)
}
wg.Done()
}()

View File

@ -2,10 +2,12 @@ package discovery
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/event"
ma "github.com/multiformats/go-multiaddr"
"github.com/status-im/rendezvous"
"github.com/stretchr/testify/require"
@ -15,42 +17,87 @@ func TestProxyToRendezvous(t *testing.T) {
var (
topic = "test"
id = 101
limited = 102
limit = 1
reg = newRegistry()
original = &fake{id: 110, registry: reg, started: true}
srv = makeTestRendezvousServer(t, "/ip4/127.0.0.1/tcp/7788")
stop = make(chan struct{})
feed = &event.Feed{}
liveness = 100 * time.Millisecond
wg sync.WaitGroup
)
client, err := rendezvous.NewEphemeral()
require.NoError(t, err)
reg.Add(topic, id)
reg.Add(topic, limited)
wg.Add(1)
events := make(chan proxyEvent, 10)
sub := feed.Subscribe(events)
defer sub.Unsubscribe()
go func() {
defer wg.Done()
require.NoError(t, ProxyToRendezvous(original, []ma.Multiaddr{srv.Addr()}, topic, stop))
require.NoError(t, ProxyToRendezvous(original, stop, feed, ProxyOptions{
Topic: topic,
Servers: []ma.Multiaddr{srv.Addr()},
Limit: limit,
LivenessWindow: liveness,
}))
}()
timer := time.After(3 * time.Second)
ticker := time.Tick(100 * time.Millisecond)
require.NoError(t, Consistently(func() (bool, error) {
records, err := client.Discover(context.TODO(), srv.Addr(), topic, 10)
if err != nil && len(records) < limit {
return true, nil
}
if len(records) > limit {
return false, fmt.Errorf("more records than expected: %d != %d", len(records), limit)
}
var proxied Proxied
if err := records[0].Load(&proxied); err != nil {
return false, err
}
if proxied[0] != byte(id) {
return false, fmt.Errorf("returned %v instead of %v", proxied[0], id)
}
return true, nil
}, time.Second, 100*time.Millisecond))
close(stop)
wg.Wait()
eventSlice := []proxyEvent{}
func() {
for {
select {
case e := <-events:
eventSlice = append(eventSlice, e)
default:
return
}
}
}()
require.Len(t, eventSlice, 2)
require.Equal(t, byte(id), eventSlice[0].ID[0])
require.Equal(t, proxyStart, eventSlice[0].Type)
require.Equal(t, byte(id), eventSlice[1].ID[0])
require.Equal(t, proxyStop, eventSlice[1].Type)
require.True(t, eventSlice[1].Time.Sub(eventSlice[0].Time) > liveness)
}
func Consistently(f func() (bool, error), timeout, period time.Duration) (err error) {
timer := time.After(timeout)
ticker := time.Tick(period)
var cont bool
for {
select {
case <-timer:
close(stop)
wg.Wait()
require.FailNow(t, "failed waiting for record to be proxied")
return err
case <-ticker:
records, err := client.Discover(context.TODO(), srv.Addr(), topic, 10)
cont, err = f()
if cont {
continue
}
if err != nil {
continue
return err
}
if len(records) != 1 {
continue
}
var proxied Proxied
require.NoError(t, records[0].Load(&proxied))
require.Equal(t, proxied[0], byte(id))
close(stop)
wg.Wait()
return
}
}
}