github.com/number571/tendermint@v0.34.11-gost/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  // Example:
    14  //
    15  //     q, err := query.New("account.name='John'")
    16  //     if err != nil {
    17  //         return err
    18  //     }
    19  //     ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second)
    20  //     defer cancel()
    21  //     subscription, err := pubsub.Subscribe(ctx, "johns-transactions", q)
    22  //     if err != nil {
    23  //         return err
    24  //     }
    25  //
    26  //     for {
    27  //         select {
    28  //         case msg <- subscription.Out():
    29  //             // handle msg.Data() and msg.Events()
    30  //         case <-subscription.Canceled():
    31  //             return subscription.Err()
    32  //         }
    33  //     }
    34  //
    35  package pubsub
    36  
    37  import (
    38  	"context"
    39  	"errors"
    40  	"fmt"
    41  
    42  	"github.com/number571/tendermint/abci/types"
    43  	tmsync "github.com/number571/tendermint/internal/libs/sync"
    44  	"github.com/number571/tendermint/libs/pubsub/query"
    45  	"github.com/number571/tendermint/libs/service"
    46  )
    47  
    48  type operation int
    49  
    50  const (
    51  	sub operation = iota
    52  	pub
    53  	unsub
    54  	shutdown
    55  )
    56  
    57  var (
    58  	// ErrSubscriptionNotFound is returned when a client tries to unsubscribe
    59  	// from not existing subscription.
    60  	ErrSubscriptionNotFound = errors.New("subscription not found")
    61  
    62  	// ErrAlreadySubscribed is returned when a client tries to subscribe twice or
    63  	// more using the same query.
    64  	ErrAlreadySubscribed = errors.New("already subscribed")
    65  )
    66  
    67  // Query defines an interface for a query to be used for subscribing. A query
    68  // matches against a map of events. Each key in this map is a composite of the
    69  // even type and an attribute key (e.g. "{eventType}.{eventAttrKey}") and the
    70  // values are the event values that are contained under that relationship. This
    71  // allows event types to repeat themselves with the same set of keys and
    72  // different values.
    73  type Query interface {
    74  	Matches(events []types.Event) (bool, error)
    75  	String() string
    76  }
    77  
    78  type UnsubscribeArgs struct {
    79  	ID         string
    80  	Subscriber string
    81  	Query      Query
    82  }
    83  
    84  func (args UnsubscribeArgs) Validate() error {
    85  	if args.Subscriber == "" {
    86  		return errors.New("must specify a subscriber")
    87  	}
    88  
    89  	if args.ID == "" && args.Query == nil {
    90  		return fmt.Errorf("subscription is not fully defined [subscriber=%q]", args.Subscriber)
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  type cmd struct {
    97  	op operation
    98  
    99  	// subscribe, unsubscribe
   100  	query        Query
   101  	subscription *Subscription
   102  	clientID     string
   103  
   104  	// publish
   105  	msg    interface{}
   106  	events []types.Event
   107  }
   108  
   109  // Server allows clients to subscribe/unsubscribe for messages, publishing
   110  // messages with or without events, and manages internal state.
   111  type Server struct {
   112  	service.BaseService
   113  
   114  	cmds    chan cmd
   115  	cmdsCap int
   116  
   117  	// check if we have subscription before
   118  	// subscribing or unsubscribing
   119  	mtx tmsync.RWMutex
   120  
   121  	// subscriber -> [query->id (string) OR id->query (string))],
   122  	//   track connections both by ID (new) and query (legacy) to
   123  	//   avoid breaking the interface.
   124  	subscriptions map[string]map[string]string
   125  }
   126  
   127  // Option sets a parameter for the server.
   128  type Option func(*Server)
   129  
   130  // NewServer returns a new server. See the commentary on the Option functions
   131  // for a detailed description of how to configure buffering. If no options are
   132  // provided, the resulting server's queue is unbuffered.
   133  func NewServer(options ...Option) *Server {
   134  	s := &Server{
   135  		subscriptions: make(map[string]map[string]string),
   136  	}
   137  	s.BaseService = *service.NewBaseService(nil, "PubSub", s)
   138  
   139  	for _, option := range options {
   140  		option(s)
   141  	}
   142  
   143  	// if BufferCapacity option was not set, the channel is unbuffered
   144  	s.cmds = make(chan cmd, s.cmdsCap)
   145  
   146  	return s
   147  }
   148  
   149  // BufferCapacity allows you to specify capacity for the internal server's
   150  // queue. Since the server, given Y subscribers, could only process X messages,
   151  // this option could be used to survive spikes (e.g. high amount of
   152  // transactions during peak hours).
   153  func BufferCapacity(cap int) Option {
   154  	return func(s *Server) {
   155  		if cap > 0 {
   156  			s.cmdsCap = cap
   157  		}
   158  	}
   159  }
   160  
   161  // BufferCapacity returns capacity of the internal server's queue.
   162  func (s *Server) BufferCapacity() int {
   163  	return s.cmdsCap
   164  }
   165  
   166  // Subscribe creates a subscription for the given client.
   167  //
   168  // An error will be returned to the caller if the context is canceled or if
   169  // subscription already exist for pair clientID and query.
   170  //
   171  // outCapacity can be used to set a capacity for Subscription#Out channel (1 by
   172  // default). Panics if outCapacity is less than or equal to zero. If you want
   173  // an unbuffered channel, use SubscribeUnbuffered.
   174  func (s *Server) Subscribe(
   175  	ctx context.Context,
   176  	clientID string,
   177  	query Query,
   178  	outCapacity ...int) (*Subscription, error) {
   179  	outCap := 1
   180  	if len(outCapacity) > 0 {
   181  		if outCapacity[0] <= 0 {
   182  			panic("Negative or zero capacity. Use SubscribeUnbuffered if you want an unbuffered channel")
   183  		}
   184  		outCap = outCapacity[0]
   185  	}
   186  
   187  	return s.subscribe(ctx, clientID, query, outCap)
   188  }
   189  
   190  // SubscribeUnbuffered does the same as Subscribe, except it returns a
   191  // subscription with unbuffered channel. Use with caution as it can freeze the
   192  // server.
   193  func (s *Server) SubscribeUnbuffered(ctx context.Context, clientID string, query Query) (*Subscription, error) {
   194  	return s.subscribe(ctx, clientID, query, 0)
   195  }
   196  
   197  func (s *Server) subscribe(ctx context.Context, clientID string, query Query, outCapacity int) (*Subscription, error) {
   198  	s.mtx.RLock()
   199  	clientSubscriptions, ok := s.subscriptions[clientID]
   200  	if ok {
   201  		_, ok = clientSubscriptions[query.String()]
   202  	}
   203  	s.mtx.RUnlock()
   204  	if ok {
   205  		return nil, ErrAlreadySubscribed
   206  	}
   207  
   208  	subscription := NewSubscription(outCapacity)
   209  	select {
   210  	case s.cmds <- cmd{op: sub, clientID: clientID, query: query, subscription: subscription}:
   211  		s.mtx.Lock()
   212  		if _, ok = s.subscriptions[clientID]; !ok {
   213  			s.subscriptions[clientID] = make(map[string]string)
   214  		}
   215  		s.subscriptions[clientID][query.String()] = subscription.id
   216  		s.subscriptions[clientID][subscription.id] = query.String()
   217  		s.mtx.Unlock()
   218  		return subscription, nil
   219  	case <-ctx.Done():
   220  		return nil, ctx.Err()
   221  	case <-s.Quit():
   222  		return nil, nil
   223  	}
   224  }
   225  
   226  // Unsubscribe removes the subscription on the given query. An error will be
   227  // returned to the caller if the context is canceled or if subscription does
   228  // not exist.
   229  func (s *Server) Unsubscribe(ctx context.Context, args UnsubscribeArgs) error {
   230  	if err := args.Validate(); err != nil {
   231  		return err
   232  	}
   233  	var qs string
   234  	if args.Query != nil {
   235  		qs = args.Query.String()
   236  	}
   237  
   238  	s.mtx.RLock()
   239  	clientSubscriptions, ok := s.subscriptions[args.Subscriber]
   240  	if args.ID != "" {
   241  		qs, ok = clientSubscriptions[args.ID]
   242  
   243  		if ok && args.Query == nil {
   244  			var err error
   245  			args.Query, err = query.New(qs)
   246  			if err != nil {
   247  				return err
   248  			}
   249  		}
   250  	} else if qs != "" {
   251  		args.ID, ok = clientSubscriptions[qs]
   252  	}
   253  
   254  	s.mtx.RUnlock()
   255  	if !ok {
   256  		return ErrSubscriptionNotFound
   257  	}
   258  
   259  	select {
   260  	case s.cmds <- cmd{op: unsub, clientID: args.Subscriber, query: args.Query, subscription: &Subscription{id: args.ID}}:
   261  		s.mtx.Lock()
   262  
   263  		delete(clientSubscriptions, args.ID)
   264  		delete(clientSubscriptions, qs)
   265  
   266  		if len(clientSubscriptions) == 0 {
   267  			delete(s.subscriptions, args.Subscriber)
   268  		}
   269  		s.mtx.Unlock()
   270  		return nil
   271  	case <-ctx.Done():
   272  		return ctx.Err()
   273  	case <-s.Quit():
   274  		return nil
   275  	}
   276  }
   277  
   278  // UnsubscribeAll removes all client subscriptions. An error will be returned
   279  // to the caller if the context is canceled or if subscription does not exist.
   280  func (s *Server) UnsubscribeAll(ctx context.Context, clientID string) error {
   281  	s.mtx.RLock()
   282  	_, ok := s.subscriptions[clientID]
   283  	s.mtx.RUnlock()
   284  	if !ok {
   285  		return ErrSubscriptionNotFound
   286  	}
   287  
   288  	select {
   289  	case s.cmds <- cmd{op: unsub, clientID: clientID}:
   290  		s.mtx.Lock()
   291  		delete(s.subscriptions, clientID)
   292  		s.mtx.Unlock()
   293  		return nil
   294  	case <-ctx.Done():
   295  		return ctx.Err()
   296  	case <-s.Quit():
   297  		return nil
   298  	}
   299  }
   300  
   301  // NumClients returns the number of clients.
   302  func (s *Server) NumClients() int {
   303  	s.mtx.RLock()
   304  	defer s.mtx.RUnlock()
   305  	return len(s.subscriptions)
   306  }
   307  
   308  // NumClientSubscriptions returns the number of subscriptions the client has.
   309  func (s *Server) NumClientSubscriptions(clientID string) int {
   310  	s.mtx.RLock()
   311  	defer s.mtx.RUnlock()
   312  	return len(s.subscriptions[clientID]) / 2
   313  }
   314  
   315  // Publish publishes the given message. An error will be returned to the caller
   316  // if the context is canceled.
   317  func (s *Server) Publish(ctx context.Context, msg interface{}) error {
   318  	return s.PublishWithEvents(ctx, msg, []types.Event{})
   319  }
   320  
   321  // PublishWithEvents publishes the given message with the set of events. The set
   322  // is matched with clients queries. If there is a match, the message is sent to
   323  // the client.
   324  func (s *Server) PublishWithEvents(ctx context.Context, msg interface{}, events []types.Event) error {
   325  	select {
   326  	case s.cmds <- cmd{op: pub, msg: msg, events: events}:
   327  		return nil
   328  	case <-ctx.Done():
   329  		return ctx.Err()
   330  	case <-s.Quit():
   331  		return nil
   332  	}
   333  }
   334  
   335  // OnStop implements Service.OnStop by shutting down the server.
   336  func (s *Server) OnStop() {
   337  	s.cmds <- cmd{op: shutdown}
   338  }
   339  
   340  // NOTE: not goroutine safe
   341  type state struct {
   342  	// query string -> client -> subscription
   343  	subscriptions map[string]map[string]*Subscription
   344  	// query string -> queryPlusRefCount
   345  	queries map[string]*queryPlusRefCount
   346  }
   347  
   348  // queryPlusRefCount holds a pointer to a query and reference counter. When
   349  // refCount is zero, query will be removed.
   350  type queryPlusRefCount struct {
   351  	q        Query
   352  	refCount int
   353  }
   354  
   355  // OnStart implements Service.OnStart by starting the server.
   356  func (s *Server) OnStart() error {
   357  	go s.loop(state{
   358  		subscriptions: make(map[string]map[string]*Subscription),
   359  		queries:       make(map[string]*queryPlusRefCount),
   360  	})
   361  	return nil
   362  }
   363  
   364  // OnReset implements Service.OnReset
   365  func (s *Server) OnReset() error {
   366  	return nil
   367  }
   368  
   369  func (s *Server) loop(state state) {
   370  loop:
   371  	for cmd := range s.cmds {
   372  		switch cmd.op {
   373  		case unsub:
   374  			if cmd.query != nil {
   375  				state.remove(cmd.clientID, cmd.query.String(), cmd.subscription.id, ErrUnsubscribed)
   376  			} else {
   377  				state.removeClient(cmd.clientID, ErrUnsubscribed)
   378  			}
   379  		case shutdown:
   380  			state.removeAll(nil)
   381  			break loop
   382  		case sub:
   383  			state.add(cmd.clientID, cmd.query, cmd.subscription)
   384  		case pub:
   385  			if err := state.send(cmd.msg, cmd.events); err != nil {
   386  				s.Logger.Error("Error querying for events", "err", err)
   387  			}
   388  		}
   389  	}
   390  }
   391  
   392  func (state *state) add(clientID string, q Query, subscription *Subscription) {
   393  	qStr := q.String()
   394  
   395  	// initialize subscription for this client per query if needed
   396  	if _, ok := state.subscriptions[qStr]; !ok {
   397  		state.subscriptions[qStr] = make(map[string]*Subscription)
   398  	}
   399  
   400  	if _, ok := state.subscriptions[subscription.id]; !ok {
   401  		state.subscriptions[subscription.id] = make(map[string]*Subscription)
   402  	}
   403  
   404  	// create subscription
   405  	state.subscriptions[qStr][clientID] = subscription
   406  	state.subscriptions[subscription.id][clientID] = subscription
   407  
   408  	// initialize query if needed
   409  	if _, ok := state.queries[qStr]; !ok {
   410  		state.queries[qStr] = &queryPlusRefCount{q: q, refCount: 0}
   411  	}
   412  	// increment reference counter
   413  	state.queries[qStr].refCount++
   414  }
   415  
   416  func (state *state) remove(clientID string, qStr, id string, reason error) {
   417  	clientSubscriptions, ok := state.subscriptions[qStr]
   418  	if !ok {
   419  		return
   420  	}
   421  
   422  	subscription, ok := clientSubscriptions[clientID]
   423  	if !ok {
   424  		return
   425  	}
   426  
   427  	subscription.cancel(reason)
   428  
   429  	// remove client from query map.
   430  	// if query has no other clients subscribed, remove it.
   431  	delete(state.subscriptions[qStr], clientID)
   432  	delete(state.subscriptions[id], clientID)
   433  	if len(state.subscriptions[qStr]) == 0 {
   434  		delete(state.subscriptions, qStr)
   435  	}
   436  
   437  	// decrease ref counter in queries
   438  	if ref, ok := state.queries[qStr]; ok {
   439  		ref.refCount--
   440  		if ref.refCount == 0 {
   441  			// remove the query if nobody else is using it
   442  			delete(state.queries, qStr)
   443  		}
   444  	}
   445  }
   446  
   447  func (state *state) removeClient(clientID string, reason error) {
   448  	seen := map[string]struct{}{}
   449  	for qStr, clientSubscriptions := range state.subscriptions {
   450  		if sub, ok := clientSubscriptions[clientID]; ok {
   451  			if _, ok = seen[sub.id]; ok {
   452  				// all subscriptions are double indexed by ID and query, only
   453  				// process them once.
   454  				continue
   455  			}
   456  			state.remove(clientID, qStr, sub.id, reason)
   457  			seen[sub.id] = struct{}{}
   458  		}
   459  	}
   460  }
   461  
   462  func (state *state) removeAll(reason error) {
   463  	for qStr, clientSubscriptions := range state.subscriptions {
   464  		sub, ok := clientSubscriptions[qStr]
   465  		if !ok || ok && sub.id == qStr {
   466  			// all subscriptions are double indexed by ID and query, only
   467  			// process them once.
   468  			continue
   469  		}
   470  
   471  		for clientID := range clientSubscriptions {
   472  			state.remove(clientID, qStr, sub.id, reason)
   473  		}
   474  	}
   475  }
   476  
   477  func (state *state) send(msg interface{}, events []types.Event) error {
   478  	for qStr, clientSubscriptions := range state.subscriptions {
   479  		if sub, ok := clientSubscriptions[qStr]; ok && sub.id == qStr {
   480  			continue
   481  		}
   482  		var q Query
   483  		if qi, ok := state.queries[qStr]; ok {
   484  			q = qi.q
   485  		} else {
   486  			continue
   487  		}
   488  
   489  		match, err := q.Matches(events)
   490  		if err != nil {
   491  			return fmt.Errorf("failed to match against query %s: %w", q.String(), err)
   492  		}
   493  
   494  		if match {
   495  			for clientID, subscription := range clientSubscriptions {
   496  				if cap(subscription.out) == 0 {
   497  					// block on unbuffered channel
   498  					subscription.out <- NewMessage(subscription.id, msg, events)
   499  				} else {
   500  					// don't block on buffered channels
   501  					select {
   502  					case subscription.out <- NewMessage(subscription.id, msg, events):
   503  					default:
   504  						state.remove(clientID, qStr, subscription.id, ErrOutOfCapacity)
   505  					}
   506  				}
   507  			}
   508  		}
   509  	}
   510  
   511  	return nil
   512  }