github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/internal/rpc/core/events.go (about)

     1  package core
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/ari-anchor/sei-tendermint/internal/eventlog"
    10  	"github.com/ari-anchor/sei-tendermint/internal/eventlog/cursor"
    11  	"github.com/ari-anchor/sei-tendermint/internal/jsontypes"
    12  	tmpubsub "github.com/ari-anchor/sei-tendermint/internal/pubsub"
    13  	tmquery "github.com/ari-anchor/sei-tendermint/internal/pubsub/query"
    14  	"github.com/ari-anchor/sei-tendermint/rpc/coretypes"
    15  	rpctypes "github.com/ari-anchor/sei-tendermint/rpc/jsonrpc/types"
    16  )
    17  
    18  const (
    19  	// Buffer on the Tendermint (server) side to allow some slowness in clients.
    20  	subBufferSize = 100
    21  
    22  	// maxQueryLength is the maximum length of a query string that will be
    23  	// accepted. This is just a safety check to avoid outlandish queries.
    24  	maxQueryLength = 512
    25  )
    26  
    27  // Subscribe for events via WebSocket.
    28  // More: https://docs.tendermint.com/master/rpc/#/Websocket/subscribe
    29  func (env *Environment) Subscribe(ctx context.Context, req *coretypes.RequestSubscribe) (*coretypes.ResultSubscribe, error) {
    30  	callInfo := rpctypes.GetCallInfo(ctx)
    31  	addr := callInfo.RemoteAddr()
    32  
    33  	if env.EventBus.NumClients() >= env.Config.MaxSubscriptionClients {
    34  		return nil, fmt.Errorf("max_subscription_clients %d reached", env.Config.MaxSubscriptionClients)
    35  	} else if env.EventBus.NumClientSubscriptions(addr) >= env.Config.MaxSubscriptionsPerClient {
    36  		return nil, fmt.Errorf("max_subscriptions_per_client %d reached", env.Config.MaxSubscriptionsPerClient)
    37  	} else if len(req.Query) > maxQueryLength {
    38  		return nil, errors.New("maximum query length exceeded")
    39  	}
    40  
    41  	env.Logger.Info("WARNING: Websocket subscriptions are deprecated and will be removed " +
    42  		"in Tendermint v0.37. See https://tinyurl.com/adr075 for more information.")
    43  	env.Logger.Info("Subscribe to query", "remote", addr, "query", req.Query)
    44  
    45  	q, err := tmquery.New(req.Query)
    46  	if err != nil {
    47  		return nil, fmt.Errorf("failed to parse query: %w", err)
    48  	}
    49  
    50  	subCtx, cancel := context.WithTimeout(ctx, SubscribeTimeout)
    51  	defer cancel()
    52  
    53  	sub, err := env.EventBus.SubscribeWithArgs(subCtx, tmpubsub.SubscribeArgs{
    54  		ClientID: addr,
    55  		Query:    q,
    56  		Limit:    subBufferSize,
    57  	})
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	// Capture the current ID, since it can change in the future.
    63  	subscriptionID := callInfo.RPCRequest.ID
    64  	go func() {
    65  		opctx, opcancel := context.WithCancel(context.TODO())
    66  		defer opcancel()
    67  
    68  		for {
    69  			msg, err := sub.Next(opctx)
    70  			if errors.Is(err, tmpubsub.ErrUnsubscribed) {
    71  				// The subscription was removed by the client.
    72  				return
    73  			} else if errors.Is(err, tmpubsub.ErrTerminated) {
    74  				// The subscription was terminated by the publisher.
    75  				resp := callInfo.RPCRequest.MakeError(err)
    76  				ok := callInfo.WSConn.TryWriteRPCResponse(opctx, resp)
    77  				if !ok {
    78  					env.Logger.Info("Unable to write response (slow client)",
    79  						"to", addr, "subscriptionID", subscriptionID, "err", err)
    80  				}
    81  				return
    82  			}
    83  
    84  			// We have a message to deliver to the client.
    85  			resp := callInfo.RPCRequest.MakeResponse(&coretypes.ResultEvent{
    86  				Query:  req.Query,
    87  				Data:   msg.LegacyData(),
    88  				Events: msg.Events(),
    89  			})
    90  			wctx, cancel := context.WithTimeout(opctx, 10*time.Second)
    91  			err = callInfo.WSConn.WriteRPCResponse(wctx, resp)
    92  			cancel()
    93  			if err != nil {
    94  				env.Logger.Info("Unable to write response (slow client)",
    95  					"to", addr, "subscriptionID", subscriptionID, "err", err)
    96  			}
    97  		}
    98  	}()
    99  
   100  	return &coretypes.ResultSubscribe{}, nil
   101  }
   102  
   103  // Unsubscribe from events via WebSocket.
   104  // More: https://docs.tendermint.com/master/rpc/#/Websocket/unsubscribe
   105  func (env *Environment) Unsubscribe(ctx context.Context, req *coretypes.RequestUnsubscribe) (*coretypes.ResultUnsubscribe, error) {
   106  	args := tmpubsub.UnsubscribeArgs{Subscriber: rpctypes.GetCallInfo(ctx).RemoteAddr()}
   107  	env.Logger.Info("Unsubscribe from query", "remote", args.Subscriber, "subscription", req.Query)
   108  
   109  	var err error
   110  	args.Query, err = tmquery.New(req.Query)
   111  
   112  	if err != nil {
   113  		args.ID = req.Query
   114  	}
   115  
   116  	err = env.EventBus.Unsubscribe(ctx, args)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  	return &coretypes.ResultUnsubscribe{}, nil
   121  }
   122  
   123  // UnsubscribeAll from all events via WebSocket.
   124  // More: https://docs.tendermint.com/master/rpc/#/Websocket/unsubscribe_all
   125  func (env *Environment) UnsubscribeAll(ctx context.Context) (*coretypes.ResultUnsubscribe, error) {
   126  	addr := rpctypes.GetCallInfo(ctx).RemoteAddr()
   127  	env.Logger.Info("Unsubscribe from all", "remote", addr)
   128  	err := env.EventBus.UnsubscribeAll(ctx, addr)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	return &coretypes.ResultUnsubscribe{}, nil
   133  }
   134  
   135  // Events applies a query to the event log. If an event log is not enabled,
   136  // Events reports an error. Otherwise, it filters the current contents of the
   137  // log to return matching events.
   138  //
   139  // Events returns up to maxItems of the newest eligible event items. An item is
   140  // eligible if it is older than before (or before is zero), it is newer than
   141  // after (or after is zero), and its data matches the filter. A nil filter
   142  // matches all event data.
   143  //
   144  // If before is zero and no eligible event items are available, Events waits
   145  // for up to waitTime for a matching item to become available. The wait is
   146  // terminated early if ctx ends.
   147  //
   148  // If maxItems ≤ 0, a default positive number of events is chosen. The values
   149  // of maxItems and waitTime may be capped to sensible internal maxima without
   150  // reporting an error to the caller.
   151  func (env *Environment) Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error) {
   152  	if env.EventLog == nil {
   153  		return nil, errors.New("the event log is not enabled")
   154  	}
   155  
   156  	// Parse and validate parameters.
   157  	maxItems := req.MaxItems
   158  	if maxItems <= 0 {
   159  		maxItems = 10
   160  	} else if maxItems > 100 {
   161  		maxItems = 100
   162  	}
   163  
   164  	const minWaitTime = 1 * time.Second
   165  	const maxWaitTime = 30 * time.Second
   166  
   167  	waitTime := req.WaitTime
   168  	if waitTime < minWaitTime {
   169  		waitTime = minWaitTime
   170  	} else if waitTime > maxWaitTime {
   171  		waitTime = maxWaitTime
   172  	}
   173  
   174  	query := tmquery.All
   175  	if req.Filter != nil && req.Filter.Query != "" {
   176  		q, err := tmquery.New(req.Filter.Query)
   177  		if err != nil {
   178  			return nil, fmt.Errorf("invalid filter query: %w", err)
   179  		}
   180  		query = q
   181  	}
   182  
   183  	var before, after cursor.Cursor
   184  	if err := before.UnmarshalText([]byte(req.Before)); err != nil {
   185  		return nil, fmt.Errorf("invalid cursor %q: %w", req.Before, err)
   186  	}
   187  	if err := after.UnmarshalText([]byte(req.After)); err != nil {
   188  		return nil, fmt.Errorf("invalid cursor %q: %w", req.After, err)
   189  	}
   190  
   191  	var info eventlog.Info
   192  	var items []*eventlog.Item
   193  	var err error
   194  	accept := func(itm *eventlog.Item) error {
   195  		// N.B. We accept up to one item more than requested, so we can tell how
   196  		// to set the "more" flag in the response.
   197  		if len(items) > maxItems || itm.Cursor.Before(after) {
   198  			return eventlog.ErrStopScan
   199  		}
   200  		if cursorInRange(itm.Cursor, before, after) && query.Matches(itm.Events) {
   201  			items = append(items, itm)
   202  		}
   203  		return nil
   204  	}
   205  
   206  	if before.IsZero() {
   207  		ctx, cancel := context.WithTimeout(ctx, waitTime)
   208  		defer cancel()
   209  
   210  		// Long poll. The loop here is because new items may not match the query,
   211  		// and we want to keep waiting until we have relevant results (or time out).
   212  		cur := after
   213  		for len(items) == 0 {
   214  			info, err = env.EventLog.WaitScan(ctx, cur, accept)
   215  			if err != nil {
   216  				// Don't report a timeout as a request failure.
   217  				if errors.Is(err, context.DeadlineExceeded) {
   218  					err = nil
   219  				}
   220  				break
   221  			}
   222  			cur = info.Newest
   223  		}
   224  	} else {
   225  		// Quick poll, return only what is already available.
   226  		info, err = env.EventLog.Scan(accept)
   227  	}
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	more := len(items) > maxItems
   233  	if more {
   234  		items = items[:len(items)-1]
   235  	}
   236  	enc, err := marshalItems(items)
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  	return &coretypes.ResultEvents{
   241  		Items:  enc,
   242  		More:   more,
   243  		Oldest: cursorString(info.Oldest),
   244  		Newest: cursorString(info.Newest),
   245  	}, nil
   246  }
   247  
   248  func cursorString(c cursor.Cursor) string {
   249  	if c.IsZero() {
   250  		return ""
   251  	}
   252  	return c.String()
   253  }
   254  
   255  func cursorInRange(c, before, after cursor.Cursor) bool {
   256  	return (before.IsZero() || c.Before(before)) && (after.IsZero() || after.Before(c))
   257  }
   258  
   259  func marshalItems(items []*eventlog.Item) ([]*coretypes.EventItem, error) {
   260  	out := make([]*coretypes.EventItem, len(items))
   261  	for i, itm := range items {
   262  		v, err := jsontypes.Marshal(itm.Data)
   263  		if err != nil {
   264  			return nil, fmt.Errorf("encoding event data: %w", err)
   265  		}
   266  		out[i] = &coretypes.EventItem{Cursor: itm.Cursor.String(), Event: itm.Type}
   267  		out[i].Data = v
   268  	}
   269  	return out, nil
   270  }