You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

194 lines
6.1 KiB

// 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)
}