-
Notifications
You must be signed in to change notification settings - Fork 191
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
Message Validators #55
Changes from 15 commits
647bb98
930f264
1945f89
7dd4e0b
197a598
89e6a06
02877cd
6e8b9f2
fe09d1e
88274db
4241241
d2f6a00
982c4de
fba445b
c95ed28
5ef13c7
bf2151b
856a25c
edcb251
473a5d2
f6081fb
145a84a
f1be0f1
cb365a5
fceb00d
bbdec3f
3f4fc21
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,7 +17,11 @@ import ( | |
timecache "github.com/whyrusleeping/timecache" | ||
) | ||
|
||
const ID = protocol.ID("/floodsub/1.0.0") | ||
const ( | ||
ID = protocol.ID("/floodsub/1.0.0") | ||
defaultMaxConcurrency = 10 | ||
defaultValidateTimeout = 150 * time.Millisecond | ||
) | ||
|
||
var log = logging.Logger("floodsub") | ||
|
||
|
@@ -54,6 +58,9 @@ type PubSub struct { | |
// 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 | ||
|
||
peers map[peer.ID]chan *RPC | ||
seenMessages *timecache.TimeCache | ||
|
||
|
@@ -78,8 +85,10 @@ type RPC struct { | |
from peer.ID | ||
} | ||
|
||
type Option func(*PubSub) error | ||
|
||
// NewFloodSub returns a new FloodSub management object | ||
func NewFloodSub(ctx context.Context, h host.Host) *PubSub { | ||
func NewFloodSub(ctx context.Context, h host.Host, opts ...Option) (*PubSub, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we could avoid the interface change if we don't use any options -- do we want this for future proofing? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep it, it's a nice interface; also we may want to pass an option for global validation throttle. |
||
ps := &PubSub{ | ||
host: h, | ||
ctx: ctx, | ||
|
@@ -91,19 +100,27 @@ func NewFloodSub(ctx context.Context, h host.Host) *PubSub { | |
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()), | ||
} | ||
|
||
for _, opt := range opts { | ||
err := opt(ps) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
h.SetStreamHandler(ID, ps.handleNewStream) | ||
h.Network().Notify((*PubSubNotif)(ps)) | ||
|
||
go ps.processLoop(ctx) | ||
|
||
return ps | ||
return ps, nil | ||
} | ||
|
||
// processLoop handles all inputs arriving on the channels | ||
|
@@ -176,7 +193,12 @@ func (p *PubSub) processLoop(ctx context.Context) { | |
continue | ||
} | ||
case msg := <-p.publish: | ||
p.maybePublishMessage(p.host.ID(), msg.Message) | ||
subs := p.getSubscriptions(msg) | ||
p.pushMsg(subs, p.host.ID(), msg) | ||
|
||
case req := <-p.sendMsg: | ||
p.maybePublishMessage(req.from, req.msg.Message) | ||
|
||
case <-ctx.Done(): | ||
log.Info("pubsub processloop shutting down") | ||
return | ||
|
@@ -210,24 +232,22 @@ func (p *PubSub) handleRemoveSubscription(sub *Subscription) { | |
// subscribes to the topic. | ||
// Only called from processLoop. | ||
func (p *PubSub) handleAddSubscription(req *addSubReq) { | ||
subs := p.myTopics[req.topic] | ||
sub := req.sub | ||
subs := p.myTopics[sub.topic] | ||
|
||
// announce we want this topic | ||
if len(subs) == 0 { | ||
p.announce(req.topic, true) | ||
p.announce(sub.topic, true) | ||
} | ||
|
||
// make new if not there | ||
if subs == nil { | ||
p.myTopics[req.topic] = make(map[*Subscription]struct{}) | ||
subs = p.myTopics[req.topic] | ||
p.myTopics[sub.topic] = make(map[*Subscription]struct{}) | ||
subs = p.myTopics[sub.topic] | ||
} | ||
|
||
sub := &Subscription{ | ||
ch: make(chan *Message, 32), | ||
topic: req.topic, | ||
cancelCh: p.cancelCh, | ||
} | ||
sub.ch = make(chan *Message, 32) | ||
sub.cancelCh = p.cancelCh | ||
|
||
p.myTopics[sub.topic][sub] = struct{}{} | ||
|
||
|
@@ -314,8 +334,11 @@ func (p *PubSub) handleIncomingRPC(rpc *RPC) error { | |
continue | ||
} | ||
|
||
p.maybePublishMessage(rpc.from, pmsg) | ||
msg := &Message{pmsg} | ||
subs := p.getSubscriptions(msg) | ||
p.pushMsg(subs, rpc.from, msg) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
|
@@ -324,6 +347,85 @@ func msgID(pmsg *pb.Message) string { | |
return string(pmsg.GetFrom()) + string(pmsg.GetSeqno()) | ||
} | ||
|
||
// pushMsg pushes a message to a number of subscriptions, performing validation | ||
// as necessary | ||
func (p *PubSub) pushMsg(subs []*Subscription, src peer.ID, msg *Message) { | ||
// we perform validation if _any_ of the subscriptions has a validator | ||
// because the message is sent once for all topics | ||
needval := false | ||
for _, sub := range subs { | ||
if sub.validate != nil { | ||
needval = true | ||
break | ||
} | ||
} | ||
|
||
if needval { | ||
// validation is asynchronous | ||
// XXX vyzo: do we want a global validation throttle here? | ||
go p.validate(subs, src, msg) | ||
return | ||
} | ||
|
||
go func() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hrm... spawning a goroutine for each message being sent is a bit wonky. Maybe: select {
case p.sendMsg <- .....:
default:
go func() {
p.sendMsg <- ....
}()
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point, we only have to spawn a new goroutine if it's a publish, in which case the push happens within the event loop. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we are always in the event loop actually. I added a buffer to |
||
p.sendMsg <- sendReq{ | ||
from: src, | ||
msg: msg, | ||
} | ||
}() | ||
} | ||
|
||
// validate performs validation and only sends the message if all validators succeed | ||
func (p *PubSub) validate(subs []*Subscription, src peer.ID, msg *Message) { | ||
ctx, cancel := context.WithCancel(p.ctx) | ||
defer cancel() | ||
|
||
results := make([]chan bool, 0, len(subs)) | ||
throttle := false | ||
|
||
loop: | ||
for _, sub := range subs { | ||
if sub.validate == nil { | ||
continue | ||
} | ||
|
||
rch := make(chan bool, 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd just use a single channel and count results from it. Allocating channels isn't that expensive but it's certainly not free. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fair enough; it was really convenient to write this way though :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. alright, implemented! |
||
results = append(results, rch) | ||
|
||
select { | ||
case sub.validateThrottle <- struct{}{}: | ||
go func(sub *Subscription, rch chan bool) { | ||
rch <- sub.validateMsg(ctx, msg) | ||
<-sub.validateThrottle | ||
}(sub, rch) | ||
|
||
default: | ||
log.Debugf("validation throttled for topic %s", sub.topic) | ||
throttle = true | ||
break loop | ||
} | ||
} | ||
|
||
if throttle { | ||
log.Warningf("message validation throttled; dropping message from %s", src) | ||
return | ||
} | ||
|
||
for _, rch := range results { | ||
valid := <-rch | ||
if !valid { | ||
log.Warningf("message validation failed; dropping message from %s", src) | ||
return | ||
} | ||
} | ||
|
||
// all validators were successful, send the message | ||
p.sendMsg <- sendReq{ | ||
from: src, | ||
msg: msg, | ||
} | ||
} | ||
|
||
func (p *PubSub) maybePublishMessage(from peer.ID, pmsg *pb.Message) { | ||
id := msgID(pmsg) | ||
if p.seenMessage(id) { | ||
|
@@ -348,7 +450,7 @@ func (p *PubSub) publishMessage(from peer.ID, msg *pb.Message) error { | |
continue | ||
} | ||
|
||
for p, _ := range tmap { | ||
for p := range tmap { | ||
tosend[p] = struct{}{} | ||
} | ||
} | ||
|
@@ -375,20 +477,64 @@ func (p *PubSub) publishMessage(from peer.ID, msg *pb.Message) error { | |
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 | ||
} | ||
|
||
type addSubReq struct { | ||
topic string | ||
resp chan *Subscription | ||
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 | ||
} | ||
} | ||
|
||
// 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 { | ||
return func(sub *Subscription) error { | ||
sub.validateTimeout = timeout | ||
return nil | ||
} | ||
} | ||
|
||
func WithMaxConcurrency(n int) SubOpt { | ||
return func(sub *Subscription) error { | ||
sub.validateThrottle = make(chan struct{}, n) | ||
return nil | ||
} | ||
} | ||
|
||
// Subscribe returns a new Subscription for the given topic | ||
func (p *PubSub) Subscribe(topic string) (*Subscription, error) { | ||
func (p *PubSub) Subscribe(topic string, opts ...SubOpt) (*Subscription, error) { | ||
td := pb.TopicDescriptor{Name: &topic} | ||
|
||
return p.SubscribeByTopicDescriptor(&td) | ||
return p.SubscribeByTopicDescriptor(&td, opts...) | ||
} | ||
|
||
// SubscribeByTopicDescriptor lets you subscribe a topic using a pb.TopicDescriptor | ||
func (p *PubSub) SubscribeByTopicDescriptor(td *pb.TopicDescriptor) (*Subscription, error) { | ||
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") | ||
} | ||
|
@@ -397,10 +543,26 @@ func (p *PubSub) SubscribeByTopicDescriptor(td *pb.TopicDescriptor) (*Subscripti | |
return nil, fmt.Errorf("encryption mode not yet supported") | ||
} | ||
|
||
sub := &Subscription{ | ||
topic: td.GetName(), | ||
validateTimeout: defaultValidateTimeout, | ||
} | ||
|
||
for _, opt := range opts { | ||
err := opt(sub) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
if sub.validate != nil && sub.validateThrottle == nil { | ||
sub.validateThrottle = make(chan struct{}, defaultMaxConcurrency) | ||
} | ||
|
||
out := make(chan *Subscription, 1) | ||
p.addSub <- &addSubReq{ | ||
topic: td.GetName(), | ||
resp: out, | ||
sub: sub, | ||
resp: out, | ||
} | ||
|
||
return <-out, nil | ||
|
@@ -439,6 +601,12 @@ type listPeerReq struct { | |
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 | ||
} | ||
|
||
// ListPeers returns a list of peers we are connected to. | ||
func (p *PubSub) ListPeers(topic string) []peer.ID { | ||
out := make(chan []peer.ID) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am thinking that this should be a
chan *sendReq
for consistency with the other channels.