github.com/supragya/TendermintConnector@v0.0.0-20210619045051-113e32b84fb1/_deprecated_chains/cosmos/libs/pubsub/pubsub.go (about)

     1  // Package pubsub implements a pub-sub model with a single publisher (Server)
     2  // and multiple subscribers (clients).
     3  //
     4  // Though you can have multiple publishers by sharing a pointer to a server or
     5  // by giving the same channel to each publisher and publishing messages from
     6  // that channel (fan-in).
     7  //
     8  // Clients subscribe for messages, which could be of any type, using a query.
     9  // When some message is published, we match it with all queries. If there is a
    10  // match, this message will be pushed to all clients, subscribed to that query.
    11  // See query subpackage for our implementation.
    12  //
    13  // Due to the blocking send implementation, a single subscriber can freeze an
    14  // entire server by not reading messages before it unsubscribes. To avoid such
    15  // scenario, subscribers must either:
    16  //
    17  // a) make sure they continue to read from the out channel until
    18  // Unsubscribe(All) is called
    19  //
    20  //     s.Subscribe(ctx, sub, qry, out)
    21  //     go func() {
    22  //         for msg := range out {
    23  //             // handle msg
    24  //             // will exit automatically when out is closed by Unsubscribe(All)
    25  //         }
    26  //     }()
    27  //     s.UnsubscribeAll(ctx, sub)
    28  //
    29  // b) drain the out channel before calling Unsubscribe(All)
    30  //
    31  //     s.Subscribe(ctx, sub, qry, out)
    32  //     defer func() {
    33  //         // drain out to make sure we don't block
    34  //     LOOP:
    35  //		     for {
    36  // 		     	   select {
    37  // 		     	   case <-out:
    38  // 		     	   default:
    39  // 		     	   	   break LOOP
    40  // 		     	   }
    41  // 		     }
    42  //         s.UnsubscribeAll(ctx, sub)
    43  //     }()
    44  //     for msg := range out {
    45  //         // handle msg
    46  //         if err != nil {
    47  //            return err
    48  //         }
    49  //     }
    50  //
    51  package pubsub
    52  
    53  import (
    54  	"context"
    55  	"errors"
    56  	"sync"
    57  
    58  	cmn "github.com/tendermint/tendermint/libs/common"
    59  )
    60  
    61  type operation int
    62  
    63  const (
    64  	sub operation = iota
    65  	pub
    66  	unsub
    67  	shutdown
    68  )
    69  
    70  var (
    71  	// ErrSubscriptionNotFound is returned when a client tries to unsubscribe
    72  	// from not existing subscription.
    73  	ErrSubscriptionNotFound = errors.New("subscription not found")
    74  
    75  	// ErrAlreadySubscribed is returned when a client tries to subscribe twice or
    76  	// more using the same query.
    77  	ErrAlreadySubscribed = errors.New("already subscribed")
    78  )
    79  
    80  type cmd struct {
    81  	op       operation
    82  	query    Query
    83  	ch       chan<- interface{}
    84  	clientID string
    85  	msg      interface{}
    86  	tags     TagMap
    87  }
    88  
    89  // Query defines an interface for a query to be used for subscribing.
    90  type Query interface {
    91  	Matches(tags TagMap) bool
    92  	String() string
    93  }
    94  
    95  // Server allows clients to subscribe/unsubscribe for messages, publishing
    96  // messages with or without tags, and manages internal state.
    97  type Server struct {
    98  	cmn.BaseService
    99  
   100  	cmds    chan cmd
   101  	cmdsCap int
   102  
   103  	// check if we have subscription before
   104  	// subscribing or unsubscribing
   105  	mtx           sync.RWMutex
   106  	subscriptions map[string]map[string]Query // subscriber -> query (string) -> Query
   107  }
   108  
   109  // Option sets a parameter for the server.
   110  type Option func(*Server)
   111  
   112  // TagMap is used to associate tags to a message.
   113  // They can be queried by subscribers to choose messages they will received.
   114  type TagMap interface {
   115  	// Get returns the value for a key, or nil if no value is present.
   116  	// The ok result indicates whether value was found in the tags.
   117  	Get(key string) (value string, ok bool)
   118  	// Len returns the number of tags.
   119  	Len() int
   120  }
   121  
   122  type tagMap map[string]string
   123  
   124  var _ TagMap = (*tagMap)(nil)
   125  
   126  // NewTagMap constructs a new immutable tag set from a map.
   127  func NewTagMap(data map[string]string) TagMap {
   128  	return tagMap(data)
   129  }
   130  
   131  // Get returns the value for a key, or nil if no value is present.
   132  // The ok result indicates whether value was found in the tags.
   133  func (ts tagMap) Get(key string) (value string, ok bool) {
   134  	value, ok = ts[key]
   135  	return
   136  }
   137  
   138  // Len returns the number of tags.
   139  func (ts tagMap) Len() int {
   140  	return len(ts)
   141  }
   142  
   143  // NewServer returns a new server. See the commentary on the Option functions
   144  // for a detailed description of how to configure buffering. If no options are
   145  // provided, the resulting server's queue is unbuffered.
   146  func NewServer(options ...Option) *Server {
   147  	s := &Server{
   148  		subscriptions: make(map[string]map[string]Query),
   149  	}
   150  	s.BaseService = *cmn.NewBaseService(nil, "PubSub", s)
   151  
   152  	for _, option := range options {
   153  		option(s)
   154  	}
   155  
   156  	// if BufferCapacity option was not set, the channel is unbuffered
   157  	s.cmds = make(chan cmd, s.cmdsCap)
   158  
   159  	return s
   160  }
   161  
   162  // BufferCapacity allows you to specify capacity for the internal server's
   163  // queue. Since the server, given Y subscribers, could only process X messages,
   164  // this option could be used to survive spikes (e.g. high amount of
   165  // transactions during peak hours).
   166  func BufferCapacity(cap int) Option {
   167  	return func(s *Server) {
   168  		if cap > 0 {
   169  			s.cmdsCap = cap
   170  		}
   171  	}
   172  }
   173  
   174  // BufferCapacity returns capacity of the internal server's queue.
   175  func (s *Server) BufferCapacity() int {
   176  	return s.cmdsCap
   177  }
   178  
   179  // Subscribe creates a subscription for the given client. It accepts a channel
   180  // on which messages matching the given query can be received. An error will be
   181  // returned to the caller if the context is canceled or if subscription already
   182  // exist for pair clientID and query.
   183  func (s *Server) Subscribe(ctx context.Context, clientID string, query Query, out chan<- interface{}) error {
   184  	s.mtx.RLock()
   185  	clientSubscriptions, ok := s.subscriptions[clientID]
   186  	if ok {
   187  		_, ok = clientSubscriptions[query.String()]
   188  	}
   189  	s.mtx.RUnlock()
   190  	if ok {
   191  		return ErrAlreadySubscribed
   192  	}
   193  
   194  	select {
   195  	case s.cmds <- cmd{op: sub, clientID: clientID, query: query, ch: out}:
   196  		s.mtx.Lock()
   197  		if _, ok = s.subscriptions[clientID]; !ok {
   198  			s.subscriptions[clientID] = make(map[string]Query)
   199  		}
   200  		// preserve original query
   201  		// see Unsubscribe
   202  		s.subscriptions[clientID][query.String()] = query
   203  		s.mtx.Unlock()
   204  		return nil
   205  	case <-ctx.Done():
   206  		return ctx.Err()
   207  	case <-s.Quit():
   208  		return nil
   209  	}
   210  }
   211  
   212  // Unsubscribe removes the subscription on the given query. An error will be
   213  // returned to the caller if the context is canceled or if subscription does
   214  // not exist.
   215  func (s *Server) Unsubscribe(ctx context.Context, clientID string, query Query) error {
   216  	var origQuery Query
   217  	s.mtx.RLock()
   218  	clientSubscriptions, ok := s.subscriptions[clientID]
   219  	if ok {
   220  		origQuery, ok = clientSubscriptions[query.String()]
   221  	}
   222  	s.mtx.RUnlock()
   223  	if !ok {
   224  		return ErrSubscriptionNotFound
   225  	}
   226  
   227  	// original query is used here because we're using pointers as map keys
   228  	select {
   229  	case s.cmds <- cmd{op: unsub, clientID: clientID, query: origQuery}:
   230  		s.mtx.Lock()
   231  		delete(clientSubscriptions, query.String())
   232  		s.mtx.Unlock()
   233  		return nil
   234  	case <-ctx.Done():
   235  		return ctx.Err()
   236  	case <-s.Quit():
   237  		return nil
   238  	}
   239  }
   240  
   241  // UnsubscribeAll removes all client subscriptions. An error will be returned
   242  // to the caller if the context is canceled or if subscription does not exist.
   243  func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error {
   244  	s.mtx.RLock()
   245  	_, ok := s.subscriptions[clientID]
   246  	s.mtx.RUnlock()
   247  	if !ok {
   248  		return ErrSubscriptionNotFound
   249  	}
   250  
   251  	select {
   252  	case s.cmds <- cmd{op: unsub, clientID: clientID}:
   253  		s.mtx.Lock()
   254  		delete(s.subscriptions, clientID)
   255  		s.mtx.Unlock()
   256  		return nil
   257  	case <-ctx.Done():
   258  		return ctx.Err()
   259  	case <-s.Quit():
   260  		return nil
   261  	}
   262  }
   263  
   264  // Publish publishes the given message. An error will be returned to the caller
   265  // if the context is canceled.
   266  func (s *Server) Publish(ctx context.Context, msg interface{}) error {
   267  	return s.PublishWithTags(ctx, msg, NewTagMap(make(map[string]string)))
   268  }
   269  
   270  // PublishWithTags publishes the given message with the set of tags. The set is
   271  // matched with clients queries. If there is a match, the message is sent to
   272  // the client.
   273  func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags TagMap) error {
   274  	select {
   275  	case s.cmds <- cmd{op: pub, msg: msg, tags: tags}:
   276  		return nil
   277  	case <-ctx.Done():
   278  		return ctx.Err()
   279  	case <-s.Quit():
   280  		return nil
   281  	}
   282  }
   283  
   284  // OnStop implements Service.OnStop by shutting down the server.
   285  func (s *Server) OnStop() {
   286  	s.cmds <- cmd{op: shutdown}
   287  }
   288  
   289  // NOTE: not goroutine safe
   290  type state struct {
   291  	// query -> client -> ch
   292  	queries map[Query]map[string]chan<- interface{}
   293  	// client -> query -> struct{}
   294  	clients map[string]map[Query]struct{}
   295  }
   296  
   297  // OnStart implements Service.OnStart by starting the server.
   298  func (s *Server) OnStart() error {
   299  	go s.loop(state{
   300  		queries: make(map[Query]map[string]chan<- interface{}),
   301  		clients: make(map[string]map[Query]struct{}),
   302  	})
   303  	return nil
   304  }
   305  
   306  // OnReset implements Service.OnReset
   307  func (s *Server) OnReset() error {
   308  	return nil
   309  }
   310  
   311  func (s *Server) loop(state state) {
   312  loop:
   313  	for cmd := range s.cmds {
   314  		switch cmd.op {
   315  		case unsub:
   316  			if cmd.query != nil {
   317  				state.remove(cmd.clientID, cmd.query)
   318  			} else {
   319  				state.removeAll(cmd.clientID)
   320  			}
   321  		case shutdown:
   322  			for clientID := range state.clients {
   323  				state.removeAll(clientID)
   324  			}
   325  			break loop
   326  		case sub:
   327  			state.add(cmd.clientID, cmd.query, cmd.ch)
   328  		case pub:
   329  			state.send(cmd.msg, cmd.tags)
   330  		}
   331  	}
   332  }
   333  
   334  func (state *state) add(clientID string, q Query, ch chan<- interface{}) {
   335  
   336  	// initialize clientToChannelMap per query if needed
   337  	if _, ok := state.queries[q]; !ok {
   338  		state.queries[q] = make(map[string]chan<- interface{})
   339  	}
   340  
   341  	// create subscription
   342  	state.queries[q][clientID] = ch
   343  
   344  	// add client if needed
   345  	if _, ok := state.clients[clientID]; !ok {
   346  		state.clients[clientID] = make(map[Query]struct{})
   347  	}
   348  	state.clients[clientID][q] = struct{}{}
   349  }
   350  
   351  func (state *state) remove(clientID string, q Query) {
   352  	clientToChannelMap, ok := state.queries[q]
   353  	if !ok {
   354  		return
   355  	}
   356  
   357  	ch, ok := clientToChannelMap[clientID]
   358  	if ok {
   359  		close(ch)
   360  
   361  		delete(state.clients[clientID], q)
   362  
   363  		// if it not subscribed to anything else, remove the client
   364  		if len(state.clients[clientID]) == 0 {
   365  			delete(state.clients, clientID)
   366  		}
   367  
   368  		delete(state.queries[q], clientID)
   369  		if len(state.queries[q]) == 0 {
   370  			delete(state.queries, q)
   371  		}
   372  	}
   373  }
   374  
   375  func (state *state) removeAll(clientID string) {
   376  	queryMap, ok := state.clients[clientID]
   377  	if !ok {
   378  		return
   379  	}
   380  
   381  	for q := range queryMap {
   382  		ch := state.queries[q][clientID]
   383  		close(ch)
   384  
   385  		delete(state.queries[q], clientID)
   386  		if len(state.queries[q]) == 0 {
   387  			delete(state.queries, q)
   388  		}
   389  	}
   390  	delete(state.clients, clientID)
   391  }
   392  
   393  func (state *state) send(msg interface{}, tags TagMap) {
   394  	for q, clientToChannelMap := range state.queries {
   395  		if q.Matches(tags) {
   396  			for _, ch := range clientToChannelMap {
   397  				ch <- msg
   398  			}
   399  		}
   400  	}
   401  }