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