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 }