github.com/onflow/flow-go@v0.33.17/engine/access/state_stream/backend/streamer.go (about) 1 package backend 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 "github.com/onflow/flow-go/engine/access/state_stream" 14 "github.com/onflow/flow-go/module/executiondatasync/execution_data" 15 "github.com/onflow/flow-go/storage" 16 ) 17 18 // Streamer 19 type Streamer struct { 20 log zerolog.Logger 21 sub state_stream.Streamable 22 broadcaster *engine.Broadcaster 23 sendTimeout time.Duration 24 limiter *rate.Limiter 25 } 26 27 func NewStreamer( 28 log zerolog.Logger, 29 broadcaster *engine.Broadcaster, 30 sendTimeout time.Duration, 31 limit float64, 32 sub state_stream.Streamable, 33 ) *Streamer { 34 var limiter *rate.Limiter 35 if limit > 0 { 36 // allows for 1 response per call, averaging `limit` responses per second over longer time frames 37 limiter = rate.NewLimiter(rate.Limit(limit), 1) 38 } 39 40 return &Streamer{ 41 log: log.With().Str("sub_id", sub.ID()).Logger(), 42 broadcaster: broadcaster, 43 sendTimeout: sendTimeout, 44 limiter: limiter, 45 sub: sub, 46 } 47 } 48 49 // Stream is a blocking method that streams data to the subscription until either the context is 50 // cancelled or it encounters an error. 51 func (s *Streamer) Stream(ctx context.Context) { 52 s.log.Debug().Msg("starting streaming") 53 defer s.log.Debug().Msg("finished streaming") 54 55 notifier := engine.NewNotifier() 56 s.broadcaster.Subscribe(notifier) 57 58 // always check the first time. This ensures that streaming continues to work even if the 59 // execution sync is not functioning (e.g. on a past spork network, or during an temporary outage) 60 notifier.Notify() 61 62 for { 63 select { 64 case <-ctx.Done(): 65 s.sub.Fail(fmt.Errorf("client disconnected: %w", ctx.Err())) 66 return 67 case <-notifier.Channel(): 68 s.log.Debug().Msg("received broadcast notification") 69 } 70 71 err := s.sendAllAvailable(ctx) 72 73 if err != nil { 74 s.log.Err(err).Msg("error sending response") 75 s.sub.Fail(err) 76 return 77 } 78 } 79 } 80 81 // sendAllAvailable reads data from the streamable and sends it to the client until no more data is available. 82 func (s *Streamer) sendAllAvailable(ctx context.Context) error { 83 for { 84 // blocking wait for the streamer's rate limit to have available capacity 85 if err := s.checkRateLimit(ctx); err != nil { 86 return fmt.Errorf("error waiting for response capacity: %w", err) 87 } 88 89 response, err := s.sub.Next(ctx) 90 91 if err != nil { 92 if errors.Is(err, storage.ErrNotFound) || errors.Is(err, storage.ErrHeightNotIndexed) || execution_data.IsBlobNotFoundError(err) { 93 // no more available 94 return nil 95 } 96 97 return fmt.Errorf("could not get response: %w", err) 98 } 99 100 if ssub, ok := s.sub.(*HeightBasedSubscription); ok { 101 s.log.Trace(). 102 Uint64("next_height", ssub.nextHeight). 103 Msg("sending response") 104 } 105 106 err = s.sub.Send(ctx, response, s.sendTimeout) 107 if err != nil { 108 return err 109 } 110 } 111 } 112 113 // checkRateLimit checks the stream's rate limit and blocks until there is room to send a response. 114 // An error is returned if the context is canceled or the expected wait time exceeds the context's 115 // deadline. 116 func (s *Streamer) checkRateLimit(ctx context.Context) error { 117 if s.limiter == nil { 118 return nil 119 } 120 121 return s.limiter.WaitN(ctx, 1) 122 }