github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/subscription/subscription.go (about) 1 package subscription 2 3 import ( 4 "context" 5 "fmt" 6 "sync" 7 "time" 8 9 "github.com/google/uuid" 10 "google.golang.org/grpc/status" 11 ) 12 13 const ( 14 // DefaultSendBufferSize is the default buffer size for the subscription's send channel. 15 // The size is chosen to balance memory overhead from each subscription with performance when 16 // streaming existing data. 17 DefaultSendBufferSize = 10 18 19 // DefaultMaxGlobalStreams defines the default max number of streams that can be open at the same time. 20 DefaultMaxGlobalStreams = 1000 21 22 // DefaultCacheSize defines the default max number of objects for the execution data cache. 23 DefaultCacheSize = 100 24 25 // DefaultSendTimeout is the default timeout for sending a message to the client. After the timeout 26 // expires, the connection is closed. 27 DefaultSendTimeout = 30 * time.Second 28 29 // DefaultResponseLimit is default max responses per second allowed on a stream. After exceeding 30 // the limit, the stream is paused until more capacity is available. 31 DefaultResponseLimit = float64(0) 32 33 // DefaultHeartbeatInterval specifies the block interval at which heartbeat messages should be sent. 34 DefaultHeartbeatInterval = 1 35 ) 36 37 // GetDataByHeightFunc is a callback used by subscriptions to retrieve data for a given height. 38 // Expected errors: 39 // - storage.ErrNotFound 40 // - execution_data.BlobNotFoundError 41 // All other errors are considered exceptions 42 type GetDataByHeightFunc func(ctx context.Context, height uint64) (interface{}, error) 43 44 // Subscription represents a streaming request, and handles the communication between the grpc handler 45 // and the backend implementation. 46 type Subscription interface { 47 // ID returns the unique identifier for this subscription used for logging 48 ID() string 49 50 // Channel returns the channel from which subscription data can be read 51 Channel() <-chan interface{} 52 53 // Err returns the error that caused the subscription to fail 54 Err() error 55 } 56 57 // Streamable represents a subscription that can be streamed. 58 type Streamable interface { 59 // ID returns the subscription ID 60 // Note: this is not a cryptographic hash 61 ID() string 62 // Close is called when a subscription ends gracefully, and closes the subscription channel 63 Close() 64 // Fail registers an error and closes the subscription channel 65 Fail(error) 66 // Send sends a value to the subscription channel or returns an error 67 // Expected errors: 68 // - context.DeadlineExceeded if send timed out 69 // - context.Canceled if the client disconnected 70 Send(context.Context, interface{}, time.Duration) error 71 // Next returns the value for the next height from the subscription 72 Next(context.Context) (interface{}, error) 73 } 74 75 var _ Subscription = (*SubscriptionImpl)(nil) 76 77 type SubscriptionImpl struct { 78 id string 79 80 // ch is the channel used to pass data to the receiver 81 ch chan interface{} 82 83 // err is the error that caused the subscription to fail 84 err error 85 86 // once is used to ensure that the channel is only closed once 87 once sync.Once 88 89 // closed tracks whether or not the subscription has been closed 90 closed bool 91 } 92 93 func NewSubscription(bufferSize int) *SubscriptionImpl { 94 return &SubscriptionImpl{ 95 id: uuid.New().String(), 96 ch: make(chan interface{}, bufferSize), 97 } 98 } 99 100 // ID returns the subscription ID 101 // Note: this is not a cryptographic hash 102 func (sub *SubscriptionImpl) ID() string { 103 return sub.id 104 } 105 106 // Channel returns the channel from which subscription data can be read 107 func (sub *SubscriptionImpl) Channel() <-chan interface{} { 108 return sub.ch 109 } 110 111 // Err returns the error that caused the subscription to fail 112 func (sub *SubscriptionImpl) Err() error { 113 return sub.err 114 } 115 116 // Fail registers an error and closes the subscription channel 117 func (sub *SubscriptionImpl) Fail(err error) { 118 sub.err = err 119 sub.Close() 120 } 121 122 // Close is called when a subscription ends gracefully, and closes the subscription channel 123 func (sub *SubscriptionImpl) Close() { 124 sub.once.Do(func() { 125 close(sub.ch) 126 sub.closed = true 127 }) 128 } 129 130 // Send sends a value to the subscription channel or returns an error 131 // Expected errors: 132 // - context.DeadlineExceeded if send timed out 133 // - context.Canceled if the client disconnected 134 func (sub *SubscriptionImpl) Send(ctx context.Context, v interface{}, timeout time.Duration) error { 135 if sub.closed { 136 return fmt.Errorf("subscription closed") 137 } 138 139 waitCtx, cancel := context.WithTimeout(ctx, timeout) 140 defer cancel() 141 142 select { 143 case <-waitCtx.Done(): 144 return waitCtx.Err() 145 case sub.ch <- v: 146 return nil 147 } 148 } 149 150 // NewFailedSubscription returns a new subscription that has already failed with the given error and 151 // message. This is useful to return an error that occurred during subscription setup. 152 func NewFailedSubscription(err error, msg string) *SubscriptionImpl { 153 sub := NewSubscription(0) 154 155 // if error is a grpc error, wrap it to preserve the error code 156 if st, ok := status.FromError(err); ok { 157 sub.Fail(status.Errorf(st.Code(), "%s: %s", msg, st.Message())) 158 return sub 159 } 160 161 // otherwise, return wrap the message normally 162 sub.Fail(fmt.Errorf("%s: %w", msg, err)) 163 return sub 164 } 165 166 var _ Subscription = (*HeightBasedSubscription)(nil) 167 var _ Streamable = (*HeightBasedSubscription)(nil) 168 169 // HeightBasedSubscription is a subscription that retrieves data sequentially by block height 170 type HeightBasedSubscription struct { 171 *SubscriptionImpl 172 nextHeight uint64 173 getData GetDataByHeightFunc 174 } 175 176 func NewHeightBasedSubscription(bufferSize int, firstHeight uint64, getData GetDataByHeightFunc) *HeightBasedSubscription { 177 return &HeightBasedSubscription{ 178 SubscriptionImpl: NewSubscription(bufferSize), 179 nextHeight: firstHeight, 180 getData: getData, 181 } 182 } 183 184 // Next returns the value for the next height from the subscription 185 func (s *HeightBasedSubscription) Next(ctx context.Context) (interface{}, error) { 186 v, err := s.getData(ctx, s.nextHeight) 187 if err != nil { 188 return nil, fmt.Errorf("could not get data for height %d: %w", s.nextHeight, err) 189 } 190 s.nextHeight++ 191 return v, nil 192 }