|
|
- // Package eventstream implements a convenience client for the Events method
- // of the Tendermint RPC service, allowing clients to observe a resumable
- // stream of events matching a query.
- package eventstream
-
- import (
- "context"
- "errors"
- "fmt"
- "time"
-
- "github.com/tendermint/tendermint/rpc/coretypes"
- )
-
- // Client is the subset of the RPC client interface consumed by Stream.
- type Client interface {
- Events(ctx context.Context, req *coretypes.RequestEvents) (*coretypes.ResultEvents, error)
- }
-
- // ErrStopRunning is returned by a Run callback to signal that no more events
- // are wanted and that Run should return.
- var ErrStopRunning = errors.New("stop accepting events")
-
- // A Stream cpatures the state of a streaming event subscription.
- type Stream struct {
- filter *coretypes.EventFilter // the query being streamed
- batchSize int // request batch size
- newestSeen string // from the latest item matching our query
- waitTime time.Duration // the long-polling interval
- client Client
- }
-
- // New constructs a new stream for the given query and options.
- // If opts == nil, the stream uses default values as described by
- // StreamOptions. This function will panic if cli == nil.
- func New(cli Client, query string, opts *StreamOptions) *Stream {
- if cli == nil {
- panic("eventstream: nil client")
- }
- return &Stream{
- filter: &coretypes.EventFilter{Query: query},
- batchSize: opts.batchSize(),
- newestSeen: opts.resumeFrom(),
- waitTime: opts.waitTime(),
- client: cli,
- }
- }
-
- // Run polls the service for events matching the query, and calls accept for
- // each such event. Run handles pagination transparently, and delivers events
- // to accept in order of publication.
- //
- // Run continues until ctx ends or accept reports an error. If accept returns
- // ErrStopRunning, Run returns nil; otherwise Run returns the error reported by
- // accept or ctx. Run also returns an error if the server reports an error
- // from the Events method.
- //
- // If the stream falls behind the event log on the server, Run will stop and
- // report an error of concrete type *MissedItemsError. Call Reset to reset the
- // stream to the head of the log, and call Run again to resume.
- func (s *Stream) Run(ctx context.Context, accept func(*coretypes.EventItem) error) error {
- for {
- items, err := s.fetchPages(ctx)
- if err != nil {
- return err
- }
-
- // Deliver events from the current batch to the receiver. We visit the
- // batch in reverse order so the receiver sees them in forward order.
- for i := len(items) - 1; i >= 0; i-- {
- if err := ctx.Err(); err != nil {
- return err
- }
-
- itm := items[i]
- err := accept(itm)
- if itm.Cursor > s.newestSeen {
- s.newestSeen = itm.Cursor // update the latest delivered
- }
- if errors.Is(err, ErrStopRunning) {
- return nil
- } else if err != nil {
- return err
- }
- }
- }
- }
-
- // Reset updates the stream's current cursor position to the head of the log.
- // This method may safely be called only when Run is not executing.
- func (s *Stream) Reset() { s.newestSeen = "" }
-
- // fetchPages fetches the next batch of matching results. If there are multiple
- // pages, all the matching pages are retrieved. An error is reported if the
- // current scan position falls out of the event log window.
- func (s *Stream) fetchPages(ctx context.Context) ([]*coretypes.EventItem, error) {
- var pageCursor string // if non-empty, page through items before this
- var items []*coretypes.EventItem
-
- // Fetch the next paginated batch of matching responses.
- for {
- rsp, err := s.client.Events(ctx, &coretypes.RequestEvents{
- Filter: s.filter,
- MaxItems: s.batchSize,
- After: s.newestSeen,
- Before: pageCursor,
- WaitTime: s.waitTime,
- })
- if err != nil {
- return nil, err
- }
-
- // If the oldest item in the log is newer than our most recent item,
- // it means we might have missed some events matching our query.
- if s.newestSeen != "" && s.newestSeen < rsp.Oldest {
- return nil, &MissedItemsError{
- Query: s.filter.Query,
- NewestSeen: s.newestSeen,
- OldestPresent: rsp.Oldest,
- }
- }
- items = append(items, rsp.Items...)
-
- if rsp.More {
- // There are more results matching this request, leave the baseline
- // where it is and set the page cursor so that subsequent requests
- // will get the next chunk.
- pageCursor = items[len(items)-1].Cursor
- } else if len(items) != 0 {
- // We got everything matching so far.
- return items, nil
- }
- }
- }
-
- // StreamOptions are optional settings for a Stream value. A nil *StreamOptions
- // is ready for use and provides default values as described.
- type StreamOptions struct {
- // How many items to request per call to the service. The stream may pin
- // this value to a minimum default batch size.
- BatchSize int
-
- // If set, resume streaming from this cursor. Typically this is set to the
- // cursor of the most recently-received matching value. If empty, streaming
- // begins at the head of the log (the default).
- ResumeFrom string
-
- // Specifies the long poll interval. The stream may pin this value to a
- // minimum default poll interval.
- WaitTime time.Duration
- }
-
- func (o *StreamOptions) batchSize() int {
- const minBatchSize = 16
- if o == nil || o.BatchSize < minBatchSize {
- return minBatchSize
- }
- return o.BatchSize
- }
-
- func (o *StreamOptions) resumeFrom() string {
- if o == nil {
- return ""
- }
- return o.ResumeFrom
- }
-
- func (o *StreamOptions) waitTime() time.Duration {
- const minWaitTime = 5 * time.Second
- if o == nil || o.WaitTime < minWaitTime {
- return minWaitTime
- }
- return o.WaitTime
- }
-
- // MissedItemsError is an error that indicates the stream missed (lost) some
- // number of events matching the specified query.
- type MissedItemsError struct {
- // The cursor of the newest matching item the stream has observed.
- NewestSeen string
-
- // The oldest cursor in the log at the point the miss was detected.
- // Any matching events between NewestSeen and OldestPresent are lost.
- OldestPresent string
-
- // The active query.
- Query string
- }
-
- // Error satisfies the error interface.
- func (e *MissedItemsError) Error() string {
- return fmt.Sprintf("missed events matching %q between %q and %q",
- e.Query, e.NewestSeen, e.OldestPresent)
- }
|