github.com/datachainlab/burrow@v0.25.0/event/pubsub/pubsub.go (about)

     1  // This package was extracted from Tendermint
     2  //
     3  // Package pubsub implements a pub-sub model with a single publisher (Server)
     4  // and multiple subscribers (clients).
     5  //
     6  // Though you can have multiple publishers by sharing a pointer to a server or
     7  // by giving the same channel to each publisher and publishing messages from
     8  // that channel (fan-in).
     9  //
    10  // Clients subscribe for messages, which could be of any type, using a query.
    11  // When some message is published, we match it with all queries. If there is a
    12  // match, this message will be pushed to all clients, subscribed to that query.
    13  // See query subpackage for our implementation.
    14  package pubsub
    15  
    16  import (
    17  	"context"
    18  	"errors"
    19  	"sync"
    20  
    21  	"github.com/hyperledger/burrow/event/query"
    22  	"github.com/tendermint/tendermint/libs/common"
    23  )
    24  
    25  type operation int
    26  
    27  const (
    28  	sub operation = iota
    29  	pub
    30  	unsub
    31  	shutdown
    32  )
    33  
    34  var (
    35  	// ErrSubscriptionNotFound is returned when a client tries to unsubscribe
    36  	// from not existing subscription.
    37  	ErrSubscriptionNotFound = errors.New("subscription not found")
    38  
    39  	// ErrAlreadySubscribed is returned when a client tries to subscribe twice or
    40  	// more using the same query.
    41  	ErrAlreadySubscribed = errors.New("already subscribed")
    42  )
    43  
    44  type cmd struct {
    45  	op       operation
    46  	query    query.Query
    47  	ch       chan interface{}
    48  	clientID string
    49  	msg      interface{}
    50  	tags     query.Tagged
    51  }
    52  
    53  // Server allows clients to subscribe/unsubscribe for messages, publishing
    54  // messages with or without tags, and manages internal state.
    55  type Server struct {
    56  	common.BaseService
    57  
    58  	cmds    chan cmd
    59  	cmdsCap int
    60  
    61  	mtx           sync.RWMutex
    62  	subscriptions map[string]map[string]query.Query // subscriber -> query (string) -> query.Query
    63  }
    64  
    65  // Option sets a parameter for the server.
    66  type Option func(*Server)
    67  
    68  // NewServer returns a new server. See the commentary on the Option functions
    69  // for a detailed description of how to configure buffering. If no options are
    70  // provided, the resulting server's queue is unbuffered.
    71  func NewServer(options ...Option) *Server {
    72  	s := &Server{
    73  		subscriptions: make(map[string]map[string]query.Query),
    74  	}
    75  	s.BaseService = *common.NewBaseService(nil, "PubSub", s)
    76  
    77  	for _, option := range options {
    78  		option(s)
    79  	}
    80  
    81  	// if BufferCapacity option was not set, the channel is unbuffered
    82  	s.cmds = make(chan cmd, s.cmdsCap)
    83  
    84  	return s
    85  }
    86  
    87  // BufferCapacity allows you to specify capacity for the internal server's
    88  // queue. Since the server, given Y subscribers, could only process X messages,
    89  // this option could be used to survive spikes (e.g. high amount of
    90  // transactions during peak hours).
    91  func BufferCapacity(cap int) Option {
    92  	return func(s *Server) {
    93  		if cap > 0 {
    94  			s.cmdsCap = cap
    95  		}
    96  	}
    97  }
    98  
    99  // BufferCapacity returns capacity of the internal server's queue.
   100  func (s *Server) BufferCapacity() int {
   101  	return s.cmdsCap
   102  }
   103  
   104  // Subscribe creates a subscription for the given client. It accepts a channel
   105  // on which messages matching the given query can be received. An error will be
   106  // returned to the caller if the context is canceled or if subscription already
   107  // exist for pair clientID and query.
   108  func (s *Server) Subscribe(ctx context.Context, clientID string, qry query.Query, outBuffer int) (<-chan interface{}, error) {
   109  	s.mtx.RLock()
   110  	clientSubscriptions, ok := s.subscriptions[clientID]
   111  	if ok {
   112  		_, ok = clientSubscriptions[qry.String()]
   113  	}
   114  	s.mtx.RUnlock()
   115  	if ok {
   116  		return nil, ErrAlreadySubscribed
   117  	}
   118  	// We are responsible for closing this channel so we create it
   119  	out := make(chan interface{}, outBuffer)
   120  	select {
   121  	case s.cmds <- cmd{op: sub, clientID: clientID, query: qry, ch: out}:
   122  		s.mtx.Lock()
   123  		if _, ok = s.subscriptions[clientID]; !ok {
   124  			s.subscriptions[clientID] = make(map[string]query.Query)
   125  		}
   126  		// preserve original query
   127  		// see Unsubscribe
   128  		s.subscriptions[clientID][qry.String()] = qry
   129  		s.mtx.Unlock()
   130  		return out, nil
   131  	case <-ctx.Done():
   132  		return nil, ctx.Err()
   133  	}
   134  }
   135  
   136  // Unsubscribe removes the subscription on the given query. An error will be
   137  // returned to the caller if the context is canceled or if subscription does
   138  // not exist.
   139  func (s *Server) Unsubscribe(ctx context.Context, clientID string, qry query.Query) error {
   140  	var origQuery query.Query
   141  	s.mtx.RLock()
   142  	clientSubscriptions, ok := s.subscriptions[clientID]
   143  	if ok {
   144  		origQuery, ok = clientSubscriptions[qry.String()]
   145  	}
   146  	s.mtx.RUnlock()
   147  	if !ok {
   148  		return ErrSubscriptionNotFound
   149  	}
   150  
   151  	// original query is used here because we're using pointers as map keys
   152  	select {
   153  	case s.cmds <- cmd{op: unsub, clientID: clientID, query: origQuery}:
   154  		s.mtx.Lock()
   155  		delete(clientSubscriptions, qry.String())
   156  		s.mtx.Unlock()
   157  		return nil
   158  	case <-ctx.Done():
   159  		return ctx.Err()
   160  	}
   161  }
   162  
   163  // UnsubscribeAll removes all client subscriptions. An error will be returned
   164  // to the caller if the context is canceled or if subscription does not exist.
   165  func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error {
   166  	s.mtx.RLock()
   167  	_, ok := s.subscriptions[clientID]
   168  	s.mtx.RUnlock()
   169  	if !ok {
   170  		return ErrSubscriptionNotFound
   171  	}
   172  
   173  	select {
   174  	case s.cmds <- cmd{op: unsub, clientID: clientID}:
   175  		s.mtx.Lock()
   176  		delete(s.subscriptions, clientID)
   177  		s.mtx.Unlock()
   178  		return nil
   179  	case <-ctx.Done():
   180  		return ctx.Err()
   181  	}
   182  }
   183  
   184  // Publish publishes the given message. An error will be returned to the caller
   185  // if the context is canceled.
   186  func (s *Server) Publish(ctx context.Context, msg interface{}) error {
   187  	return s.PublishWithTags(ctx, msg, query.TagMap(make(map[string]interface{})))
   188  }
   189  
   190  // PublishWithTags publishes the given message with the set of tags. The set is
   191  // matched with clients queries. If there is a match, the message is sent to
   192  // the client.
   193  func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags query.Tagged) error {
   194  	select {
   195  	case s.cmds <- cmd{op: pub, msg: msg, tags: tags}:
   196  		return nil
   197  	case <-ctx.Done():
   198  		return ctx.Err()
   199  	}
   200  }
   201  
   202  // OnStop implements Service.OnStop by shutting down the server.
   203  func (s *Server) OnStop() {
   204  	s.cmds <- cmd{op: shutdown}
   205  }
   206  
   207  // NOTE: not goroutine safe
   208  type state struct {
   209  	// query -> client -> ch
   210  	queries map[query.Query]map[string]chan interface{}
   211  	// client -> query -> struct{}
   212  	clients map[string]map[query.Query]struct{}
   213  }
   214  
   215  // OnStart implements Service.OnStart by starting the server.
   216  func (s *Server) OnStart() error {
   217  	go s.loop(state{
   218  		queries: make(map[query.Query]map[string]chan interface{}),
   219  		clients: make(map[string]map[query.Query]struct{}),
   220  	})
   221  	return nil
   222  }
   223  
   224  // OnReset implements Service.OnReset
   225  func (s *Server) OnReset() error {
   226  	return nil
   227  }
   228  
   229  func (s *Server) loop(state state) {
   230  loop:
   231  	for cmd := range s.cmds {
   232  		switch cmd.op {
   233  		case unsub:
   234  			if cmd.query != nil {
   235  				state.remove(cmd.clientID, cmd.query)
   236  			} else {
   237  				state.removeAll(cmd.clientID)
   238  			}
   239  		case shutdown:
   240  			for clientID := range state.clients {
   241  				state.removeAll(clientID)
   242  			}
   243  			break loop
   244  		case sub:
   245  			state.add(cmd.clientID, cmd.query, cmd.ch)
   246  		case pub:
   247  			state.send(cmd.msg, cmd.tags)
   248  		}
   249  	}
   250  }
   251  
   252  func (state *state) add(clientID string, q query.Query, ch chan interface{}) {
   253  	// add query if needed
   254  	if _, ok := state.queries[q]; !ok {
   255  		state.queries[q] = make(map[string]chan interface{})
   256  	}
   257  
   258  	// create subscription
   259  	state.queries[q][clientID] = ch
   260  
   261  	// add client if needed
   262  	if _, ok := state.clients[clientID]; !ok {
   263  		state.clients[clientID] = make(map[query.Query]struct{})
   264  	}
   265  	state.clients[clientID][q] = struct{}{}
   266  }
   267  
   268  func (state *state) remove(clientID string, q query.Query) {
   269  	clientToChannelMap, ok := state.queries[q]
   270  	if !ok {
   271  		return
   272  	}
   273  
   274  	ch, ok := clientToChannelMap[clientID]
   275  	if ok {
   276  		closeAndDrain(ch)
   277  
   278  		delete(state.clients[clientID], q)
   279  
   280  		// if it not subscribed to anything else, remove the client
   281  		if len(state.clients[clientID]) == 0 {
   282  			delete(state.clients, clientID)
   283  		}
   284  
   285  		delete(state.queries[q], clientID)
   286  		if len(state.queries[q]) == 0 {
   287  			delete(state.queries, q)
   288  		}
   289  	}
   290  }
   291  
   292  func (state *state) removeAll(clientID string) {
   293  	queryMap, ok := state.clients[clientID]
   294  	if !ok {
   295  		return
   296  	}
   297  
   298  	for q := range queryMap {
   299  		ch := state.queries[q][clientID]
   300  		closeAndDrain(ch)
   301  
   302  		delete(state.queries[q], clientID)
   303  		if len(state.queries[q]) == 0 {
   304  			delete(state.queries, q)
   305  		}
   306  	}
   307  	delete(state.clients, clientID)
   308  }
   309  
   310  func closeAndDrain(ch chan interface{}) {
   311  	close(ch)
   312  	for range ch {
   313  	}
   314  }
   315  
   316  func (state *state) send(msg interface{}, tags query.Tagged) {
   317  	for q, clientToChannelMap := range state.queries {
   318  		if q.Matches(tags) {
   319  			for _, ch := range clientToChannelMap {
   320  				select {
   321  				case ch <- msg:
   322  				default:
   323  					// It's difficult to do anything sensible here with retries/times outs since we may reorder a client's
   324  					// view of events by sending a later message before an earlier message we retry. If per-client order
   325  					// matters then we need a queue per client. Possible for us it does not...
   326  				}
   327  			}
   328  		}
   329  	}
   330  }