// Package pubsub implements a pub-sub model with a single publisher (Server) // and multiple subscribers (clients). // // Though you can have multiple publishers by sharing a pointer to a server or // by giving the same channel to each publisher and publishing messages from // that channel (fan-in). // // Clients subscribe for messages, which could be of any type, using a query. // When some message is published, we match it with all queries. If there is a // match, this message will be pushed to all clients, subscribed to that query. // See query subpackage for our implementation. // // Example: // // q, err := query.New("account.name='John'") // if err != nil { // return err // } // ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second) // defer cancel() // subscription, err := pubsub.Subscribe(ctx, "johns-transactions", q) // if err != nil { // return err // } // // for { // select { // case msg <- subscription.Out(): // // handle msg.Data() and msg.Tags() // case <-subscription.Cancelled(): // return subscription.Err() // } // } // package pubsub import ( "context" "errors" "sync" cmn "github.com/tendermint/tendermint/libs/common" ) type operation int const ( sub operation = iota pub unsub shutdown ) var ( // ErrSubscriptionNotFound is returned when a client tries to unsubscribe // from not existing subscription. ErrSubscriptionNotFound = errors.New("subscription not found") // ErrAlreadySubscribed is returned when a client tries to subscribe twice or // more using the same query. ErrAlreadySubscribed = errors.New("already subscribed") ) // Query defines an interface for a query to be used for subscribing. type Query interface { Matches(tags map[string]string) bool String() string } type cmd struct { op operation // subscribe, unsubscribe query Query subscription *Subscription clientID string // publish msg interface{} tags map[string]string } // Server allows clients to subscribe/unsubscribe for messages, publishing // messages with or without tags, and manages internal state. type Server struct { cmn.BaseService cmds chan cmd cmdsCap int mtx sync.RWMutex subscriptions map[string]map[string]struct{} // subscriber -> query (string) -> empty struct } // Option sets a parameter for the server. type Option func(*Server) // NewServer returns a new server. See the commentary on the Option functions // for a detailed description of how to configure buffering. If no options are // provided, the resulting server's queue is unbuffered. func NewServer(options ...Option) *Server { s := &Server{ subscriptions: make(map[string]map[string]struct{}), } s.BaseService = *cmn.NewBaseService(nil, "PubSub", s) for _, option := range options { option(s) } // if BufferCapacity option was not set, the channel is unbuffered s.cmds = make(chan cmd, s.cmdsCap) return s } // BufferCapacity allows you to specify capacity for the internal server's // queue. Since the server, given Y subscribers, could only process X messages, // this option could be used to survive spikes (e.g. high amount of // transactions during peak hours). func BufferCapacity(cap int) Option { return func(s *Server) { if cap > 0 { s.cmdsCap = cap } } } // BufferCapacity returns capacity of the internal server's queue. func (s *Server) BufferCapacity() int { return s.cmdsCap } // Subscribe creates a subscription for the given client. // // An error will be returned to the caller if the context is canceled or if // subscription already exist for pair clientID and query. // // outCapacity can be used to set a capacity for Subscription#Out channel (1 by // default). Panics if outCapacity is less than or equal to zero. If you want // an unbuffered channel, use SubscribeUnbuffered. func (s *Server) Subscribe(ctx context.Context, clientID string, query Query, outCapacity ...int) (*Subscription, error) { outCap := 1 if len(outCapacity) > 0 { if outCapacity[0] <= 0 { panic("Negative or zero capacity. Use SubscribeUnbuffered if you want an unbuffered channel") } outCap = outCapacity[0] } return s.subscribe(ctx, clientID, query, outCap) } // SubscribeUnbuffered does the same as Subscribe, except it returns a // subscription with unbuffered channel. Use with caution as it can freeze the // server. func (s *Server) SubscribeUnbuffered(ctx context.Context, clientID string, query Query) (*Subscription, error) { return s.subscribe(ctx, clientID, query, 0) } func (s *Server) subscribe(ctx context.Context, clientID string, query Query, outCapacity int) (*Subscription, error) { s.mtx.RLock() clientSubscriptions, ok := s.subscriptions[clientID] if ok { _, ok = clientSubscriptions[query.String()] } s.mtx.RUnlock() if ok { return nil, ErrAlreadySubscribed } subscription := &Subscription{ out: make(chan Message, outCapacity), cancelled: make(chan struct{}), } select { case s.cmds <- cmd{op: sub, clientID: clientID, query: query, subscription: subscription}: s.mtx.Lock() if _, ok = s.subscriptions[clientID]; !ok { s.subscriptions[clientID] = make(map[string]struct{}) } s.subscriptions[clientID][query.String()] = struct{}{} s.mtx.Unlock() return subscription, nil case <-ctx.Done(): return nil, ctx.Err() case <-s.Quit(): return nil, nil } } // Unsubscribe removes the subscription on the given query. An error will be // returned to the caller if the context is canceled or if subscription does // not exist. func (s *Server) Unsubscribe(ctx context.Context, clientID string, query Query) error { s.mtx.RLock() clientSubscriptions, ok := s.subscriptions[clientID] if ok { _, ok = clientSubscriptions[query.String()] } s.mtx.RUnlock() if !ok { return ErrSubscriptionNotFound } select { case s.cmds <- cmd{op: unsub, clientID: clientID, query: query}: s.mtx.Lock() delete(clientSubscriptions, query.String()) if len(clientSubscriptions) == 0 { delete(s.subscriptions, clientID) } s.mtx.Unlock() return nil case <-ctx.Done(): return ctx.Err() case <-s.Quit(): return nil } } // UnsubscribeAll removes all client subscriptions. An error will be returned // to the caller if the context is canceled or if subscription does not exist. func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error { s.mtx.RLock() _, ok := s.subscriptions[clientID] s.mtx.RUnlock() if !ok { return ErrSubscriptionNotFound } select { case s.cmds <- cmd{op: unsub, clientID: clientID}: s.mtx.Lock() delete(s.subscriptions, clientID) s.mtx.Unlock() return nil case <-ctx.Done(): return ctx.Err() case <-s.Quit(): return nil } } // Publish publishes the given message. An error will be returned to the caller // if the context is canceled. func (s *Server) Publish(ctx context.Context, msg interface{}) error { return s.PublishWithTags(ctx, msg, make(map[string]string)) } // PublishWithTags publishes the given message with the set of tags. The set is // matched with clients queries. If there is a match, the message is sent to // the client. func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags map[string]string) error { select { case s.cmds <- cmd{op: pub, msg: msg, tags: tags}: return nil case <-ctx.Done(): return ctx.Err() case <-s.Quit(): return nil } } // OnStop implements Service.OnStop by shutting down the server. func (s *Server) OnStop() { s.cmds <- cmd{op: shutdown} } // NOTE: not goroutine safe type state struct { // query string -> client -> subscription subscriptions map[string]map[string]*Subscription // query string -> queryPlusRefCount queries map[string]*queryPlusRefCount } // queryPlusRefCount holds a pointer to a query and reference counter. When // refCount is zero, query will be removed. type queryPlusRefCount struct { q Query refCount int } // OnStart implements Service.OnStart by starting the server. func (s *Server) OnStart() error { go s.loop(state{ subscriptions: make(map[string]map[string]*Subscription), queries: make(map[string]*queryPlusRefCount), }) return nil } // OnReset implements Service.OnReset func (s *Server) OnReset() error { return nil } func (s *Server) loop(state state) { loop: for cmd := range s.cmds { switch cmd.op { case unsub: if cmd.query != nil { state.remove(cmd.clientID, cmd.query.String(), ErrUnsubscribed) } else { state.removeClient(cmd.clientID, ErrUnsubscribed) } case shutdown: state.removeAll(nil) break loop case sub: state.add(cmd.clientID, cmd.query, cmd.subscription) case pub: state.send(cmd.msg, cmd.tags) } } } func (state *state) add(clientID string, q Query, subscription *Subscription) { qStr := q.String() // initialize subscription for this client per query if needed if _, ok := state.subscriptions[qStr]; !ok { state.subscriptions[qStr] = make(map[string]*Subscription) } // create subscription state.subscriptions[qStr][clientID] = subscription // initialize query if needed if _, ok := state.queries[qStr]; !ok { state.queries[qStr] = &queryPlusRefCount{q: q, refCount: 0} } // increment reference counter state.queries[qStr].refCount++ } func (state *state) remove(clientID string, qStr string, reason error) { clientSubscriptions, ok := state.subscriptions[qStr] if !ok { return } subscription, ok := clientSubscriptions[clientID] if !ok { return } subscription.mtx.Lock() subscription.err = reason subscription.mtx.Unlock() close(subscription.cancelled) // remove client from query map. // if query has no other clients subscribed, remove it. delete(state.subscriptions[qStr], clientID) if len(state.subscriptions[qStr]) == 0 { delete(state.subscriptions, qStr) } // decrease ref counter in queries state.queries[qStr].refCount-- // remove the query if nobody else is using it if state.queries[qStr].refCount == 0 { delete(state.queries, qStr) } } func (state *state) removeClient(clientID string, reason error) { for qStr, clientSubscriptions := range state.subscriptions { if _, ok := clientSubscriptions[clientID]; ok { state.remove(clientID, qStr, reason) } } } func (state *state) removeAll(reason error) { for qStr, clientSubscriptions := range state.subscriptions { for clientID := range clientSubscriptions { state.remove(clientID, qStr, reason) } } } func (state *state) send(msg interface{}, tags map[string]string) { for qStr, clientSubscriptions := range state.subscriptions { q := state.queries[qStr].q if q.Matches(tags) { for clientID, subscription := range clientSubscriptions { if cap(subscription.out) == 0 { // block on unbuffered channel subscription.out <- Message{msg, tags} } else { // don't block on buffered channels select { case subscription.out <- Message{msg, tags}: default: state.remove(clientID, qStr, ErrOutOfCapacity) } } } } } }