github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/rpc/client/eventstream/eventstream.go (about)

     1  // Package eventstream implements a convenience client for the Events method
     2  // of the Tendermint RPC service, allowing clients to observe a resumable
     3  // stream of events matching a query.
     4  package eventstream
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  	"time"
    11  
    12  	"github.com/ari-anchor/sei-tendermint/rpc/coretypes"
    13  )
    14  
    15  // Client is the subset of the RPC client interface consumed by Stream.
    16  type Client interface {
    17  	Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error)
    18  }
    19  
    20  // ErrStopRunning is returned by a Run callback to signal that no more events
    21  // are wanted and that Run should return.
    22  var ErrStopRunning = errors.New("stop accepting events")
    23  
    24  // A Stream cpatures the state of a streaming event subscription.
    25  type Stream struct {
    26  	filter     *coretypes.EventFilter // the query being streamed
    27  	batchSize  int                    // request batch size
    28  	newestSeen string                 // from the latest item matching our query
    29  	waitTime   time.Duration          // the long-polling interval
    30  	client     Client
    31  }
    32  
    33  // New constructs a new stream for the given query and options.
    34  // If opts == nil, the stream uses default values as described by
    35  // StreamOptions.  This function will panic if cli == nil.
    36  func New(cli Client, query string, opts *StreamOptions) *Stream {
    37  	if cli == nil {
    38  		panic("eventstream: nil client")
    39  	}
    40  	return &Stream{
    41  		filter:     &coretypes.EventFilter{Query: query},
    42  		batchSize:  opts.batchSize(),
    43  		newestSeen: opts.resumeFrom(),
    44  		waitTime:   opts.waitTime(),
    45  		client:     cli,
    46  	}
    47  }
    48  
    49  // Run polls the service for events matching the query, and calls accept for
    50  // each such event. Run handles pagination transparently, and delivers events
    51  // to accept in order of publication.
    52  //
    53  // Run continues until ctx ends or accept reports an error.  If accept returns
    54  // ErrStopRunning, Run returns nil; otherwise Run returns the error reported by
    55  // accept or ctx.  Run also returns an error if the server reports an error
    56  // from the Events method.
    57  //
    58  // If the stream falls behind the event log on the server, Run will stop and
    59  // report an error of concrete type *MissedItemsError.  Call Reset to reset the
    60  // stream to the head of the log, and call Run again to resume.
    61  func (s *Stream) Run(ctx context.Context, accept func(*coretypes.EventItem) error) error {
    62  	for {
    63  		items, err := s.fetchPages(ctx)
    64  		if err != nil {
    65  			return err
    66  		}
    67  
    68  		// Deliver events from the current batch to the receiver.  We visit the
    69  		// batch in reverse order so the receiver sees them in forward order.
    70  		for i := len(items) - 1; i >= 0; i-- {
    71  			if err := ctx.Err(); err != nil {
    72  				return err
    73  			}
    74  
    75  			itm := items[i]
    76  			err := accept(itm)
    77  			if itm.Cursor > s.newestSeen {
    78  				s.newestSeen = itm.Cursor // update the latest delivered
    79  			}
    80  			if errors.Is(err, ErrStopRunning) {
    81  				return nil
    82  			} else if err != nil {
    83  				return err
    84  			}
    85  		}
    86  	}
    87  }
    88  
    89  // Reset updates the stream's current cursor position to the head of the log.
    90  // This method may safely be called only when Run is not executing.
    91  func (s *Stream) Reset() { s.newestSeen = "" }
    92  
    93  // fetchPages fetches the next batch of matching results. If there are multiple
    94  // pages, all the matching pages are retrieved. An error is reported if the
    95  // current scan position falls out of the event log window.
    96  func (s *Stream) fetchPages(ctx context.Context) ([]*coretypes.EventItem, error) {
    97  	var pageCursor string // if non-empty, page through items before this
    98  	var items []*coretypes.EventItem
    99  
   100  	// Fetch the next paginated batch of matching responses.
   101  	for {
   102  		rsp, err := s.client.Events(ctx, &coretypes.RequestEvents{
   103  			Filter:   s.filter,
   104  			MaxItems: s.batchSize,
   105  			After:    s.newestSeen,
   106  			Before:   pageCursor,
   107  			WaitTime: s.waitTime,
   108  		})
   109  		if err != nil {
   110  			return nil, err
   111  		}
   112  
   113  		// If the oldest item in the log is newer than our most recent item,
   114  		// it means we might have missed some events matching our query.
   115  		if s.newestSeen != "" && s.newestSeen < rsp.Oldest {
   116  			return nil, &MissedItemsError{
   117  				Query:         s.filter.Query,
   118  				NewestSeen:    s.newestSeen,
   119  				OldestPresent: rsp.Oldest,
   120  			}
   121  		}
   122  		items = append(items, rsp.Items...)
   123  
   124  		if rsp.More {
   125  			// There are more results matching this request, leave the baseline
   126  			// where it is and set the page cursor so that subsequent requests
   127  			// will get the next chunk.
   128  			pageCursor = items[len(items)-1].Cursor
   129  		} else if len(items) != 0 {
   130  			// We got everything matching so far.
   131  			return items, nil
   132  		}
   133  	}
   134  }
   135  
   136  // StreamOptions are optional settings for a Stream value. A nil *StreamOptions
   137  // is ready for use and provides default values as described.
   138  type StreamOptions struct {
   139  	// How many items to request per call to the service.  The stream may pin
   140  	// this value to a minimum default batch size.
   141  	BatchSize int
   142  
   143  	// If set, resume streaming from this cursor. Typically this is set to the
   144  	// cursor of the most recently-received matching value. If empty, streaming
   145  	// begins at the head of the log (the default).
   146  	ResumeFrom string
   147  
   148  	// Specifies the long poll interval. The stream may pin this value to a
   149  	// minimum default poll interval.
   150  	WaitTime time.Duration
   151  }
   152  
   153  func (o *StreamOptions) batchSize() int {
   154  	const minBatchSize = 16
   155  	if o == nil || o.BatchSize < minBatchSize {
   156  		return minBatchSize
   157  	}
   158  	return o.BatchSize
   159  }
   160  
   161  func (o *StreamOptions) resumeFrom() string {
   162  	if o == nil {
   163  		return ""
   164  	}
   165  	return o.ResumeFrom
   166  }
   167  
   168  func (o *StreamOptions) waitTime() time.Duration {
   169  	const minWaitTime = 5 * time.Second
   170  	if o == nil || o.WaitTime < minWaitTime {
   171  		return minWaitTime
   172  	}
   173  	return o.WaitTime
   174  }
   175  
   176  // MissedItemsError is an error that indicates the stream missed (lost) some
   177  // number of events matching the specified query.
   178  type MissedItemsError struct {
   179  	// The cursor of the newest matching item the stream has observed.
   180  	NewestSeen string
   181  
   182  	// The oldest cursor in the log at the point the miss was detected.
   183  	// Any matching events between NewestSeen and OldestPresent are lost.
   184  	OldestPresent string
   185  
   186  	// The active query.
   187  	Query string
   188  }
   189  
   190  // Error satisfies the error interface.
   191  func (e *MissedItemsError) Error() string {
   192  	return fmt.Sprintf("missed events matching %q between %q and %q", e.Query, e.NewestSeen, e.OldestPresent)
   193  }