github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/subscription/streamer.go (about)

     1  package subscription
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"time"
     8  
     9  	"github.com/rs/zerolog"
    10  	"golang.org/x/time/rate"
    11  
    12  	"github.com/onflow/flow-go/engine"
    13  )
    14  
    15  // ErrBlockNotReady represents an error indicating that a block is not yet available or ready.
    16  var ErrBlockNotReady = errors.New("block not ready")
    17  
    18  // ErrEndOfData represents an error indicating that no more data available for streaming.
    19  var ErrEndOfData = errors.New("end of data")
    20  
    21  // Streamer represents a streaming subscription that delivers data to clients.
    22  type Streamer struct {
    23  	log         zerolog.Logger
    24  	sub         Streamable
    25  	broadcaster *engine.Broadcaster
    26  	sendTimeout time.Duration
    27  	limiter     *rate.Limiter
    28  }
    29  
    30  // NewStreamer creates a new Streamer instance.
    31  func NewStreamer(
    32  	log zerolog.Logger,
    33  	broadcaster *engine.Broadcaster,
    34  	sendTimeout time.Duration,
    35  	limit float64,
    36  	sub Streamable,
    37  ) *Streamer {
    38  	var limiter *rate.Limiter
    39  	if limit > 0 {
    40  		// allows for 1 response per call, averaging `limit` responses per second over longer time frames
    41  		limiter = rate.NewLimiter(rate.Limit(limit), 1)
    42  	}
    43  
    44  	return &Streamer{
    45  		log:         log.With().Str("sub_id", sub.ID()).Logger(),
    46  		broadcaster: broadcaster,
    47  		sendTimeout: sendTimeout,
    48  		limiter:     limiter,
    49  		sub:         sub,
    50  	}
    51  }
    52  
    53  // Stream is a blocking method that streams data to the subscription until either the context is
    54  // cancelled or it encounters an error.
    55  func (s *Streamer) Stream(ctx context.Context) {
    56  	s.log.Debug().Msg("starting streaming")
    57  	defer s.log.Debug().Msg("finished streaming")
    58  
    59  	notifier := engine.NewNotifier()
    60  	s.broadcaster.Subscribe(notifier)
    61  
    62  	// always check the first time. This ensures that streaming continues to work even if the
    63  	// execution sync is not functioning (e.g. on a past spork network, or during an temporary outage)
    64  	notifier.Notify()
    65  
    66  	for {
    67  		select {
    68  		case <-ctx.Done():
    69  			s.sub.Fail(fmt.Errorf("client disconnected: %w", ctx.Err()))
    70  			return
    71  		case <-notifier.Channel():
    72  			s.log.Debug().Msg("received broadcast notification")
    73  		}
    74  
    75  		err := s.sendAllAvailable(ctx)
    76  
    77  		if err != nil {
    78  			//TODO: The functionality to graceful shutdown on demand should be improved with https://github.com/onflow/flow-go/issues/5561
    79  			if errors.Is(err, ErrEndOfData) {
    80  				s.sub.Close()
    81  				return
    82  			}
    83  
    84  			s.log.Err(err).Msg("error sending response")
    85  			s.sub.Fail(err)
    86  			return
    87  		}
    88  	}
    89  }
    90  
    91  // sendAllAvailable reads data from the streamable and sends it to the client until no more data is available.
    92  func (s *Streamer) sendAllAvailable(ctx context.Context) error {
    93  	for {
    94  		// blocking wait for the streamer's rate limit to have available capacity
    95  		if err := s.checkRateLimit(ctx); err != nil {
    96  			return fmt.Errorf("error waiting for response capacity: %w", err)
    97  		}
    98  
    99  		response, err := s.sub.Next(ctx)
   100  
   101  		if response == nil && err == nil {
   102  			continue
   103  		}
   104  
   105  		if err != nil {
   106  			if errors.Is(err, ErrBlockNotReady) {
   107  				// no more available
   108  				return nil
   109  			}
   110  
   111  			return fmt.Errorf("could not get response: %w", err)
   112  		}
   113  
   114  		if ssub, ok := s.sub.(*HeightBasedSubscription); ok {
   115  			s.log.Trace().
   116  				Uint64("next_height", ssub.nextHeight).
   117  				Msg("sending response")
   118  		}
   119  
   120  		err = s.sub.Send(ctx, response, s.sendTimeout)
   121  		if err != nil {
   122  			return err
   123  		}
   124  	}
   125  }
   126  
   127  // checkRateLimit checks the stream's rate limit and blocks until there is room to send a response.
   128  // An error is returned if the context is canceled or the expected wait time exceeds the context's
   129  // deadline.
   130  func (s *Streamer) checkRateLimit(ctx context.Context) error {
   131  	if s.limiter == nil {
   132  		return nil
   133  	}
   134  
   135  	return s.limiter.WaitN(ctx, 1)
   136  }