Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement rendezvous protocol spec #1

Open
wants to merge 48 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f946163
rendezvous protobuf
vyzo Apr 18, 2018
0cbcbf6
client implementation
vyzo Apr 19, 2018
f94b0b4
update client interface
vyzo Apr 19, 2018
268abf3
more fine-grained rendezvous api
vyzo Apr 19, 2018
b7bc940
add error notification for background register/discover
vyzo Apr 19, 2018
a107e34
interface ergonomics
vyzo Apr 19, 2018
13f4c67
add E_INVALID_TTL to rendezvous.proto
vyzo Apr 20, 2018
e3d343f
include namespace in error logs
vyzo Apr 20, 2018
1506c04
simplify Rendezvous interface
vyzo Apr 20, 2018
7e5664c
use discriminated registration errors
vyzo Apr 20, 2018
6a1176f
annotate registration error
vyzo Apr 20, 2018
b7c304d
update protobuf
vyzo Apr 21, 2018
aeac2e2
update for response error changes in the protocol
vyzo Apr 21, 2018
e5a72b9
rendezvous interface should expose full registration information
vyzo Apr 21, 2018
7d72fc7
service implementation
vyzo Apr 23, 2018
dbe6b0d
rendezvous service sync: hook for federation
vyzo Apr 23, 2018
6c1d282
Registration records should have an actual peer ID
vyzo Apr 23, 2018
8181424
client: use larger batch in discovery, only poll immediately if full
vyzo Apr 23, 2018
fbaf21c
client: add TODO comment for adaptive backoff
vyzo Apr 23, 2018
4e3eaa7
refactor service constructor into two parts
vyzo Apr 23, 2018
aa3f46c
user parameter for ttl in Register
vyzo Apr 23, 2018
c703d37
database logic implementation
vyzo Apr 23, 2018
ae10cc6
implement binary packing details
vyzo Apr 24, 2018
8c12272
better logging for service i/o
vyzo Apr 24, 2018
53dfbc7
test address and cookie packing
vyzo Apr 24, 2018
f41fbba
test db functionality
vyzo Apr 24, 2018
cfbcdde
up MaxRegistrations to 1k
vyzo Apr 24, 2018
4788ef7
test db functionality with multiple namespaces
vyzo Apr 24, 2018
a47367d
basic service test
vyzo Apr 24, 2018
2b0995f
test service errors
vyzo Apr 24, 2018
9ab12ab
make db nonce 32 bytes
vyzo Apr 24, 2018
6c4fda5
test client specific functionality
vyzo Apr 24, 2018
e530204
use randomized exponential backoff in error retry for persistent clie…
vyzo Apr 24, 2018
aa7f9da
client: add TODO for robust discovery error recovery
vyzo Apr 25, 2018
c487c20
refactor database interface and implementation into db subpackage
vyzo Apr 26, 2018
baf1e4e
don't leak database error details in internal errors
vyzo Apr 26, 2018
c540724
two interfaces for client-side: RendezvousPoint and RendezvousClient
vyzo Apr 28, 2018
1ee2b55
update protobuf
vyzo Jan 18, 2019
8846a4b
include ttl in registration response
vyzo Jan 18, 2019
3c726d2
update gx deps
vyzo Jan 18, 2019
f2ee9b3
expose counter in register interface
vyzo Jan 18, 2019
2843bd3
update tests
vyzo Jan 18, 2019
91cdb88
Switched from gx to go mod and started using go-libp2p-core interfaces
aschmahmann May 28, 2019
7901280
Add stateful discovery client
aschmahmann May 31, 2019
9052b53
RendezvousPoint and RendezvousClient now return the server's TTL on R…
aschmahmann May 31, 2019
25d0082
fixed compile error from previous commit + code refactoring
aschmahmann Jun 5, 2019
0e771cd
replaced sync.Map with map + RW mutex. small refactors
aschmahmann Jun 21, 2019
7371441
Merge pull request #3 from aschmahmann/feat/add-discovery-client
vyzo Jul 8, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
287 changes: 287 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,287 @@
package rendezvous

import (
"context"
"fmt"
"math/rand"
"time"

pb "github.com/libp2p/go-libp2p-rendezvous/pb"

ggio "github.com/gogo/protobuf/io"
host "github.com/libp2p/go-libp2p-host"
inet "github.com/libp2p/go-libp2p-net"
peer "github.com/libp2p/go-libp2p-peer"
pstore "github.com/libp2p/go-libp2p-peerstore"
)

var (
DiscoverAsyncInterval = 2 * time.Minute
)

type Rendezvous interface {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments on the interface would be helpful

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes indeed! Part of the "docstrings" checkbox.

Register(ctx context.Context, ns string, ttl int) error
Unregister(ctx context.Context, ns string) error
Discover(ctx context.Context, ns string, limit int, cookie []byte) ([]Registration, []byte, error)
DiscoverAsync(ctx context.Context, ns string) (<-chan Registration, error)
}

type Registration struct {
Peer pstore.PeerInfo
Ns string
Ttl int
}

func NewRendezvousClient(host host.Host, rp peer.ID) Rendezvous {
return &client{
host: host,
rp: rp,
}
}

type client struct {
host host.Host
rp peer.ID
}

func (cli *client) Register(ctx context.Context, ns string, ttl int) error {
s, err := cli.host.NewStream(ctx, cli.rp, RendezvousProto)
if err != nil {
return err
}
defer s.Close()

r := ggio.NewDelimitedReader(s, inet.MessageSizeMax)
w := ggio.NewDelimitedWriter(s)

req := newRegisterMessage(ns, pstore.PeerInfo{ID: cli.host.ID(), Addrs: cli.host.Addrs()}, ttl)
err = w.WriteMsg(req)
if err != nil {
return err
}

var res pb.Message
err = r.ReadMsg(&res)
if err != nil {
return err
}

if res.GetType() != pb.Message_REGISTER_RESPONSE {
return fmt.Errorf("Unexpected response: %s", res.GetType().String())
}

status := res.GetRegisterResponse().GetStatus()
if status != pb.Message_OK {
return RendezvousError{Status: status, Text: res.GetRegisterResponse().GetStatusText()}
}

return nil
}

func Register(ctx context.Context, rz Rendezvous, ns string, ttl int) error {
if ttl < 120 {
return fmt.Errorf("registration TTL is too short")
}

err := rz.Register(ctx, ns, ttl)
if err != nil {
return err
}

go registerRefresh(ctx, rz, ns, ttl)
return nil
}

func registerRefresh(ctx context.Context, rz Rendezvous, ns string, ttl int) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesnt appear that this gets closed down when unregister is called. So if i call register then unregister, this routine would re-register me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrm, yes, the two are not correlated; you'd have to cancel the context to stop registration refresh.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this is a utility function to augment the interface with persistent registrations.

Copy link
Contributor Author

@vyzo vyzo Apr 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should make a more complicated stateful client contraption that handles the registration/unregistration correlation?

I think we can have two interfaces, a low level RendezvousPoint interface that has single stateless operations and a high level Rendezvous interface that incorporates the module level utility functions.

var refresh time.Duration
errcount := 0

for {
if errcount > 0 {
// do randomized exponential backoff, up to ~4 hours
if errcount > 7 {
errcount = 7
}
backoff := 2 << uint(errcount)
refresh = 5*time.Minute + time.Duration(rand.Intn(backoff*60000))*time.Millisecond
} else {
refresh = time.Duration(ttl-30) * time.Second
}

select {
case <-time.After(refresh):
case <-ctx.Done():
return
}

err := rz.Register(ctx, ns, ttl)
if err != nil {
log.Errorf("Error registering [%s]: %s", ns, err.Error())
errcount++
} else {
errcount = 0
}
}
}

func (cli *client) Unregister(ctx context.Context, ns string) error {
s, err := cli.host.NewStream(ctx, cli.rp, RendezvousProto)
if err != nil {
return err
}
defer s.Close()

w := ggio.NewDelimitedWriter(s)
req := newUnregisterMessage(ns, cli.host.ID())
return w.WriteMsg(req)
}

func (cli *client) Discover(ctx context.Context, ns string, limit int, cookie []byte) ([]Registration, []byte, error) {
s, err := cli.host.NewStream(ctx, cli.rp, RendezvousProto)
if err != nil {
return nil, nil, err
}
defer s.Close()

r := ggio.NewDelimitedReader(s, inet.MessageSizeMax)
w := ggio.NewDelimitedWriter(s)

return discoverQuery(ns, limit, cookie, r, w)
}

func discoverQuery(ns string, limit int, cookie []byte, r ggio.Reader, w ggio.Writer) ([]Registration, []byte, error) {

req := newDiscoverMessage(ns, limit, cookie)
err := w.WriteMsg(req)
if err != nil {
return nil, nil, err
}

var res pb.Message
err = r.ReadMsg(&res)
if err != nil {
return nil, nil, err
}

if res.GetType() != pb.Message_DISCOVER_RESPONSE {
return nil, nil, fmt.Errorf("Unexpected response: %s", res.GetType().String())
}

status := res.GetDiscoverResponse().GetStatus()
if status != pb.Message_OK {
return nil, nil, RendezvousError{Status: status, Text: res.GetDiscoverResponse().GetStatusText()}
}

regs := res.GetDiscoverResponse().GetRegistrations()
result := make([]Registration, 0, len(regs))
for _, reg := range regs {
pi, err := pbToPeerInfo(reg.GetPeer())
if err != nil {
log.Errorf("Invalid peer info: %s", err.Error())
continue
}
result = append(result, Registration{Peer: pi, Ns: reg.GetNs(), Ttl: int(reg.GetTtl())})
}

return result, res.GetDiscoverResponse().GetCookie(), nil
}

func (cli *client) DiscoverAsync(ctx context.Context, ns string) (<-chan Registration, error) {
s, err := cli.host.NewStream(ctx, cli.rp, RendezvousProto)
if err != nil {
return nil, err
}

ch := make(chan Registration)
go discoverAsync(ctx, ns, s, ch)
return ch, nil
}

func discoverAsync(ctx context.Context, ns string, s inet.Stream, ch chan Registration) {
defer s.Close()
defer close(ch)

r := ggio.NewDelimitedReader(s, inet.MessageSizeMax)
w := ggio.NewDelimitedWriter(s)

const batch = 200

var (
cookie []byte
regs []Registration
err error
)

for {
regs, cookie, err = discoverQuery(ns, batch, cookie, r, w)
if err != nil {
// TODO robust error recovery
// - handle closed streams with backoff + new stream, preserving the cookie
// - handle E_INVALID_COOKIE errors in that case to restart the discovery
log.Errorf("Error in discovery [%s]: %s", ns, err.Error())
return
}

for _, reg := range regs {
select {
case ch <- reg:
case <-ctx.Done():
return
}
}

if len(regs) < batch {
// TODO adaptive backoff for heavily loaded rendezvous points

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the service respond something like "load too high, try again in a bit" ? Seems like a nice DoS mitigation feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if a load indicator would help, as the clients can simply ignore it.
I am in favour of load inference from the rate of registration responses.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the other hand, we could make this an Error response -- E_TEMPORARILY_UNAVAILABLE perhaps.

Now that could help, as it would force the clients to back off and possibly create a new stream.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking have it be an error response. So they don't get any records back, and the error tells them to just wait and try again in a bit

select {
case <-time.After(DiscoverAsyncInterval):
case <-ctx.Done():
return
}
}
}
}

func DiscoverPeers(ctx context.Context, rz Rendezvous, ns string, limit int, cookie []byte) ([]pstore.PeerInfo, []byte, error) {
regs, cookie, err := rz.Discover(ctx, ns, limit, cookie)
if err != nil {
return nil, nil, err
}

pinfos := make([]pstore.PeerInfo, len(regs))
for i, reg := range regs {
pinfos[i] = reg.Peer
}

return pinfos, cookie, nil
}

func DiscoverPeersAsync(ctx context.Context, rz Rendezvous, ns string) (<-chan pstore.PeerInfo, error) {
rch, err := rz.DiscoverAsync(ctx, ns)
if err != nil {
return nil, err
}

ch := make(chan pstore.PeerInfo)
go discoverPeersAsync(ctx, rch, ch)
return ch, nil
}

func discoverPeersAsync(ctx context.Context, rch <-chan Registration, ch chan pstore.PeerInfo) {
defer close(ch)
for {
select {
case reg, ok := <-rch:
if !ok {
return
}

select {
case ch <- reg.Peer:
case <-ctx.Done():
return
}
case <-ctx.Done():
return
}
}
}
Loading