go-libp2p-pubsub/floodsub.go

605 lines
14 KiB
Go
Raw Normal View History

2016-09-10 03:13:50 +00:00
package floodsub
import (
2016-09-10 23:03:53 +00:00
"context"
2016-09-11 03:47:12 +00:00
"encoding/binary"
"fmt"
"sync/atomic"
"time"
2016-09-13 02:59:24 +00:00
pb "github.com/libp2p/go-floodsub/pb"
2016-09-10 15:14:17 +00:00
2017-11-16 15:13:31 +00:00
logging "github.com/ipfs/go-log"
host "github.com/libp2p/go-libp2p-host"
inet "github.com/libp2p/go-libp2p-net"
peer "github.com/libp2p/go-libp2p-peer"
protocol "github.com/libp2p/go-libp2p-protocol"
timecache "github.com/whyrusleeping/timecache"
)
const (
2017-11-23 18:12:59 +00:00
ID = protocol.ID("/floodsub/1.0.0")
defaultMaxConcurrency = 10
2017-11-23 18:12:59 +00:00
defaultValidateTimeout = 150 * time.Millisecond
)
2016-09-10 03:13:50 +00:00
var log = logging.Logger("floodsub")
type PubSub struct {
host host.Host
2016-09-11 20:56:07 +00:00
// incoming messages from other peers
incoming chan *RPC
2016-09-11 20:56:07 +00:00
// messages we are publishing out to our peers
publish chan *Message
// addSub is a control channel for us to add and remove subscriptions
2016-10-19 23:01:06 +00:00
addSub chan *addSubReq
2016-09-11 20:56:07 +00:00
2016-10-19 23:01:06 +00:00
// get list of topics we are subscribed to
2016-09-14 22:11:41 +00:00
getTopics chan *topicReq
2016-10-19 23:01:06 +00:00
// get chan of peers we are connected to
getPeers chan *listPeerReq
2016-10-19 23:01:06 +00:00
// send subscription here to cancel it
cancelCh chan *Subscription
2016-09-14 21:12:20 +00:00
2016-09-11 20:56:07 +00:00
// a notification channel for incoming streams from other peers
newPeers chan inet.Stream
2016-09-11 20:56:07 +00:00
// a notification channel for when our peers die
peerDead chan peer.ID
2016-09-11 20:56:07 +00:00
// The set of topics we are subscribed to
2016-10-19 23:01:06 +00:00
myTopics map[string]map[*Subscription]struct{}
2016-09-11 20:56:07 +00:00
// topics tracks which topics each of our peers are subscribed to
topics map[string]map[peer.ID]struct{}
// sendMsg handles messages that have been validated
sendMsg chan sendReq
// throttleValidate bounds the number of goroutines concurrently validating messages
throttleValidate chan struct{}
2016-09-11 03:47:12 +00:00
peers map[peer.ID]chan *RPC
seenMessages *timecache.TimeCache
2016-09-10 23:03:53 +00:00
ctx context.Context
// atomic counter for seqnos
counter uint64
}
type Message struct {
2016-09-10 15:14:17 +00:00
*pb.Message
}
2016-09-10 15:14:17 +00:00
func (m *Message) GetFrom() peer.ID {
return peer.ID(m.Message.GetFrom())
}
type RPC struct {
2016-09-10 15:14:17 +00:00
pb.RPC
// unexported on purpose, not sending this over the wire
from peer.ID
}
type Option func(*PubSub) error
func WithMaxConcurrency(n int) Option {
return func(ps *PubSub) error {
ps.throttleValidate = make(chan struct{}, n)
return nil
}
}
2016-10-20 11:23:38 +00:00
// NewFloodSub returns a new FloodSub management object
func NewFloodSub(ctx context.Context, h host.Host, opts ...Option) (*PubSub, error) {
ps := &PubSub{
host: h,
ctx: ctx,
incoming: make(chan *RPC, 32),
publish: make(chan *Message),
newPeers: make(chan inet.Stream),
peerDead: make(chan peer.ID),
cancelCh: make(chan *Subscription),
getPeers: make(chan *listPeerReq),
addSub: make(chan *addSubReq),
getTopics: make(chan *topicReq),
sendMsg: make(chan sendReq),
myTopics: make(map[string]map[*Subscription]struct{}),
topics: make(map[string]map[peer.ID]struct{}),
peers: make(map[peer.ID]chan *RPC),
seenMessages: timecache.NewTimeCache(time.Second * 30),
counter: uint64(time.Now().UnixNano()),
2018-01-13 10:24:31 +00:00
throttleValidate: make(chan struct{}, defaultMaxConcurrency),
}
for _, opt := range opts {
err := opt(ps)
if err != nil {
return nil, err
}
}
h.SetStreamHandler(ID, ps.handleNewStream)
h.Network().Notify((*PubSubNotif)(ps))
2016-09-10 23:03:53 +00:00
go ps.processLoop(ctx)
return ps, nil
}
2016-10-20 11:23:38 +00:00
// processLoop handles all inputs arriving on the channels
2016-09-10 23:03:53 +00:00
func (p *PubSub) processLoop(ctx context.Context) {
defer func() {
// Clean up go routines.
for _, ch := range p.peers {
close(ch)
}
p.peers = nil
p.topics = nil
}()
for {
select {
case s := <-p.newPeers:
pid := s.Conn().RemotePeer()
ch, ok := p.peers[pid]
if ok {
log.Error("already have connection to peer: ", pid)
close(ch)
}
messages := make(chan *RPC, 32)
2016-09-10 23:03:53 +00:00
go p.handleSendingMessages(ctx, s, messages)
messages <- p.getHelloPacket()
p.peers[pid] = messages
case pid := <-p.peerDead:
ch, ok := p.peers[pid]
if ok {
close(ch)
}
delete(p.peers, pid)
for _, t := range p.topics {
delete(t, pid)
}
2016-09-14 22:11:41 +00:00
case treq := <-p.getTopics:
var out []string
for t := range p.myTopics {
out = append(out, t)
2016-09-14 22:11:41 +00:00
}
treq.resp <- out
2016-10-19 23:01:06 +00:00
case sub := <-p.cancelCh:
p.handleRemoveSubscription(sub)
case sub := <-p.addSub:
2016-10-19 23:01:06 +00:00
p.handleAddSubscription(sub)
case preq := <-p.getPeers:
tmap, ok := p.topics[preq.topic]
if preq.topic != "" && !ok {
preq.resp <- nil
continue
}
var peers []peer.ID
for p := range p.peers {
if preq.topic != "" {
_, ok := tmap[p]
if !ok {
continue
}
}
peers = append(peers, p)
}
preq.resp <- peers
case rpc := <-p.incoming:
err := p.handleIncomingRPC(rpc)
if err != nil {
log.Error("handling RPC: ", err)
2016-09-11 20:56:07 +00:00
continue
}
2016-09-11 03:47:12 +00:00
case msg := <-p.publish:
subs := p.getSubscriptions(msg) // call before goroutine!
select {
case p.throttleValidate <- struct{}{}:
2017-11-23 13:39:14 +00:00
go func(msg *Message) {
defer func() { <-p.throttleValidate }()
if p.validate(subs, msg) {
p.sendMsg <- sendReq{
from: p.host.ID(),
msg: msg,
}
}
2017-11-23 13:39:14 +00:00
}(msg)
default:
log.Warning("could not acquire validator; dropping message")
}
case req := <-p.sendMsg:
p.maybePublishMessage(req.from, req.msg.Message)
2016-09-10 23:03:53 +00:00
case <-ctx.Done():
log.Info("pubsub processloop shutting down")
return
}
}
}
2016-09-10 03:13:50 +00:00
2016-10-20 11:23:38 +00:00
// handleRemoveSubscription removes Subscription sub from bookeeping.
// If this was the last Subscription for a given topic, it will also announce
// that this node is not subscribing to this topic anymore.
// Only called from processLoop.
2016-10-19 23:01:06 +00:00
func (p *PubSub) handleRemoveSubscription(sub *Subscription) {
subs := p.myTopics[sub.topic]
if subs == nil {
return
2016-09-11 03:47:12 +00:00
}
2016-09-10 03:13:50 +00:00
2016-10-19 23:01:06 +00:00
sub.err = fmt.Errorf("subscription cancelled by calling sub.Cancel()")
close(sub.ch)
delete(subs, sub)
2016-09-10 03:13:50 +00:00
2016-10-19 23:01:06 +00:00
if len(subs) == 0 {
delete(p.myTopics, sub.topic)
2016-10-19 23:01:06 +00:00
p.announce(sub.topic, false)
}
}
2016-09-11 03:47:12 +00:00
2016-10-20 11:23:38 +00:00
// handleAddSubscription adds a Subscription for a particular topic. If it is
// the first Subscription for the topic, it will announce that this node
// subscribes to the topic.
// Only called from processLoop.
2016-10-19 23:01:06 +00:00
func (p *PubSub) handleAddSubscription(req *addSubReq) {
sub := req.sub
subs := p.myTopics[sub.topic]
2016-10-19 23:01:06 +00:00
// announce we want this topic
if len(subs) == 0 {
p.announce(sub.topic, true)
2016-10-19 23:01:06 +00:00
}
// make new if not there
if subs == nil {
p.myTopics[sub.topic] = make(map[*Subscription]struct{})
subs = p.myTopics[sub.topic]
2016-10-19 23:01:06 +00:00
}
sub.ch = make(chan *Message, 32)
sub.cancelCh = p.cancelCh
2016-10-19 23:01:06 +00:00
p.myTopics[sub.topic][sub] = struct{}{}
req.resp <- sub
}
2016-10-20 11:23:38 +00:00
// announce announces whether or not this node is interested in a given topic
// Only called from processLoop.
2016-10-19 23:01:06 +00:00
func (p *PubSub) announce(topic string, sub bool) {
subopt := &pb.RPC_SubOpts{
Topicid: &topic,
Subscribe: &sub,
2016-09-11 03:47:12 +00:00
}
2016-09-11 20:56:07 +00:00
out := rpcWithSubs(subopt)
for pid, peer := range p.peers {
select {
case peer <- out:
default:
log.Infof("dropping announce message to peer %s: queue full", pid)
}
2016-09-11 03:47:12 +00:00
}
2016-09-10 03:13:50 +00:00
}
2016-10-20 11:23:38 +00:00
// notifySubs sends a given message to all corresponding subscribbers.
// Only called from processLoop.
2016-09-11 20:56:07 +00:00
func (p *PubSub) notifySubs(msg *pb.Message) {
for _, topic := range msg.GetTopicIDs() {
2016-10-19 23:01:06 +00:00
subs := p.myTopics[topic]
for f := range subs {
f.ch <- &Message{msg}
2016-09-11 20:56:07 +00:00
}
}
}
2016-10-20 11:23:38 +00:00
// seenMessage returns whether we already saw this message before
2016-09-11 03:47:12 +00:00
func (p *PubSub) seenMessage(id string) bool {
return p.seenMessages.Has(id)
}
2016-10-20 11:23:38 +00:00
// markSeen marks a message as seen such that seenMessage returns `true' for the given id
2016-09-11 03:47:12 +00:00
func (p *PubSub) markSeen(id string) {
p.seenMessages.Add(id)
}
2016-10-20 11:23:38 +00:00
// subscribedToMessage returns whether we are subscribed to one of the topics
// of a given message
2016-09-11 20:56:07 +00:00
func (p *PubSub) subscribedToMsg(msg *pb.Message) bool {
if len(p.myTopics) == 0 {
return false
}
2016-09-11 20:56:07 +00:00
for _, t := range msg.GetTopicIDs() {
if _, ok := p.myTopics[t]; ok {
return true
}
}
return false
}
func (p *PubSub) handleIncomingRPC(rpc *RPC) error {
2016-09-11 03:47:12 +00:00
for _, subopt := range rpc.GetSubscriptions() {
t := subopt.GetTopicid()
if subopt.GetSubscribe() {
tmap, ok := p.topics[t]
if !ok {
tmap = make(map[peer.ID]struct{})
p.topics[t] = tmap
}
tmap[rpc.from] = struct{}{}
2016-09-11 03:47:12 +00:00
} else {
tmap, ok := p.topics[t]
if !ok {
2016-09-11 03:47:12 +00:00
continue
}
delete(tmap, rpc.from)
}
2016-09-11 03:47:12 +00:00
}
2016-09-10 03:13:50 +00:00
2016-09-11 03:47:12 +00:00
for _, pmsg := range rpc.GetPublish() {
2016-09-11 20:56:07 +00:00
if !p.subscribedToMsg(pmsg) {
log.Warning("received message we didn't subscribe to. Dropping.")
2016-09-11 03:47:12 +00:00
continue
}
subs := p.getSubscriptions(&Message{pmsg}) // call before goroutine!
select {
case p.throttleValidate <- struct{}{}:
go func(pmsg *pb.Message) {
defer func() { <-p.throttleValidate }()
if p.validate(subs, &Message{pmsg}) {
p.sendMsg <- sendReq{
from: rpc.from,
msg: &Message{pmsg},
}
}
}(pmsg)
default:
log.Warning("could not acquire validator; dropping message")
}
2016-09-11 20:56:07 +00:00
}
return nil
}
2016-10-20 11:23:38 +00:00
// msgID returns a unique ID of the passed Message
func msgID(pmsg *pb.Message) string {
return string(pmsg.GetFrom()) + string(pmsg.GetSeqno())
}
// validate is called in a goroutine and calls the validate functions of all subs with msg as parameter.
func (p *PubSub) validate(subs []*Subscription, msg *Message) bool {
results := make([]chan bool, len(subs))
ctxs := make([]context.Context, len(subs))
for i, sub := range subs {
result := make(chan bool)
2017-11-23 18:12:59 +00:00
ctx, cancel := context.WithTimeout(p.ctx, sub.validateTimeout)
defer cancel()
ctxs[i] = ctx
results[i] = result
2017-11-23 13:39:14 +00:00
go func(sub *Subscription) {
result <- sub.validate == nil || sub.validate(ctx, msg)
}(sub)
}
for i, sub := range subs {
ctx := ctxs[i]
result := results[i]
2017-11-23 13:39:14 +00:00
select {
case valid := <-result:
if !valid {
log.Debugf("validator for topic %s returned false", sub.topic)
return false
}
case <-ctx.Done():
log.Debugf("validator for topic %s timed out. msg: %s", sub.topic, msg)
return false
}
}
return true
}
2016-09-11 20:56:07 +00:00
func (p *PubSub) maybePublishMessage(from peer.ID, pmsg *pb.Message) {
id := msgID(pmsg)
2016-09-11 20:56:07 +00:00
if p.seenMessage(id) {
return
}
p.markSeen(id)
p.notifySubs(pmsg)
err := p.publishMessage(from, pmsg)
if err != nil {
log.Error("publish message: ", err)
}
}
2016-09-11 03:47:12 +00:00
func (p *PubSub) publishMessage(from peer.ID, msg *pb.Message) error {
tosend := make(map[peer.ID]struct{})
for _, topic := range msg.GetTopicIDs() {
tmap, ok := p.topics[topic]
if !ok {
continue
}
2016-09-11 03:47:12 +00:00
2017-11-23 13:39:14 +00:00
for p := range tmap {
tosend[p] = struct{}{}
}
}
out := rpcWithMessages(msg)
for pid := range tosend {
2016-09-11 03:47:12 +00:00
if pid == from || pid == peer.ID(msg.GetFrom()) {
continue
}
mch, ok := p.peers[pid]
if !ok {
continue
}
select {
case mch <- out:
default:
2017-08-30 02:42:33 +00:00
log.Infof("dropping message to peer %s: queue full", pid)
// Drop it. The peer is too slow.
}
}
return nil
}
// getSubscriptions returns all subscriptions the would receive the given message.
func (p *PubSub) getSubscriptions(msg *Message) []*Subscription {
var subs []*Subscription
for _, topic := range msg.GetTopicIDs() {
tSubs, ok := p.myTopics[topic]
if !ok {
continue
}
for sub := range tSubs {
subs = append(subs, sub)
}
}
return subs
}
2016-10-19 23:01:06 +00:00
type addSubReq struct {
sub *Subscription
resp chan *Subscription
}
type SubOpt func(*Subscription) error
type Validator func(context.Context, *Message) bool
// WithValidator is an option that can be supplied to Subscribe. The argument is a function that returns whether or not a given message should be propagated further.
func WithValidator(validate Validator) SubOpt {
return func(sub *Subscription) error {
sub.validate = validate
return nil
}
2017-11-23 18:12:59 +00:00
}
2017-11-23 18:12:59 +00:00
// WithValidatorTimeout is an option that can be supplied to Subscribe. The argument is a duration after which long-running validators are canceled.
func WithValidatorTimeout(timeout time.Duration) SubOpt {
2017-11-23 18:12:59 +00:00
return func(sub *Subscription) error {
sub.validateTimeout = timeout
return nil
}
}
2016-10-20 11:23:38 +00:00
// Subscribe returns a new Subscription for the given topic
func (p *PubSub) Subscribe(topic string, opts ...SubOpt) (*Subscription, error) {
td := pb.TopicDescriptor{Name: &topic}
return p.SubscribeByTopicDescriptor(&td, opts...)
}
// SubscribeByTopicDescriptor lets you subscribe a topic using a pb.TopicDescriptor
func (p *PubSub) SubscribeByTopicDescriptor(td *pb.TopicDescriptor, opts ...SubOpt) (*Subscription, error) {
if td.GetAuth().GetMode() != pb.TopicDescriptor_AuthOpts_NONE {
return nil, fmt.Errorf("auth mode not yet supported")
}
if td.GetEnc().GetMode() != pb.TopicDescriptor_EncOpts_NONE {
return nil, fmt.Errorf("encryption mode not yet supported")
}
sub := &Subscription{
2017-11-23 18:12:59 +00:00
topic: td.GetName(),
validateTimeout: defaultValidateTimeout,
}
for _, opt := range opts {
err := opt(sub)
if err != nil {
return nil, err
}
}
out := make(chan *Subscription, 1)
p.addSub <- &addSubReq{
sub: sub,
resp: out,
}
return <-out, nil
}
2016-10-19 23:01:06 +00:00
type topicReq struct {
resp chan []string
}
2016-10-20 11:23:38 +00:00
// GetTopics returns the topics this node is subscribed to
2016-10-19 23:01:06 +00:00
func (p *PubSub) GetTopics() []string {
out := make(chan []string, 1)
p.getTopics <- &topicReq{resp: out}
return <-out
}
2016-10-20 11:23:38 +00:00
// Publish publishes data under the given topic
func (p *PubSub) Publish(topic string, data []byte) error {
seqno := make([]byte, 8)
counter := atomic.AddUint64(&p.counter, 1)
binary.BigEndian.PutUint64(seqno, counter)
2016-09-11 03:47:12 +00:00
p.publish <- &Message{
&pb.Message{
Data: data,
TopicIDs: []string{topic},
From: []byte(p.host.ID()),
2016-09-11 03:47:12 +00:00
Seqno: seqno,
},
}
return nil
}
type listPeerReq struct {
resp chan []peer.ID
topic string
}
// sendReq is a request to call maybePublishMessage. It is issued after the subscription verification is done.
type sendReq struct {
from peer.ID
msg *Message
}
2016-10-20 11:23:38 +00:00
// ListPeers returns a list of peers we are connected to.
func (p *PubSub) ListPeers(topic string) []peer.ID {
out := make(chan []peer.ID)
p.getPeers <- &listPeerReq{
resp: out,
topic: topic,
}
return <-out
}