diff --git a/.mockery.yaml b/.mockery.yaml index bc58015f196..69cb258b25e 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -35,7 +35,8 @@ packages: github.com/onflow/flow-go/engine/access/rpc/connection: github.com/onflow/flow-go/engine/access/state_stream: github.com/onflow/flow-go/engine/access/subscription: - github.com/onflow/flow-go/engine/access/subscription/tracker: + github.com/onflow/flow-go/engine/access/subscription_old: + github.com/onflow/flow-go/engine/access/subscription_old/tracker: github.com/onflow/flow-go/engine/access/wrapper: config: dir: "engine/access/mock" diff --git a/access/api.go b/access/api.go index 41b63c7ce80..32b59842104 100644 --- a/access/api.go +++ b/access/api.go @@ -76,7 +76,7 @@ type TransactionStreamAPI interface { ctx context.Context, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion, - ) subscription.Subscription + ) subscription.Subscription[[]*accessmodel.TransactionResult] // SendAndSubscribeTransactionStatuses sends a transaction to the execution node and subscribes to its status updates. // Monitoring begins from the reference block saved in the transaction itself and streams status updates until the transaction @@ -88,7 +88,140 @@ type TransactionStreamAPI interface { ctx context.Context, tx *flow.TransactionBody, requiredEventEncodingVersion entities.EventEncodingVersion, - ) subscription.Subscription + ) subscription.Subscription[[]*accessmodel.TransactionResult] +} + +type BlockStreamAPI interface { + // SubscribeBlocksFromStartBlockID subscribes to the finalized or sealed blocks starting at the + // requested start block id, up until the latest available block. Once the latest is reached, + // the stream will remain open and responses are sent for each new block as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlocksFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Block] + + // SubscribeBlocksFromStartHeight subscribes to the finalized or sealed blocks starting at the requested + // start block height, up until the latest available block. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlocksFromStartHeight( + ctx context.Context, + startHeight uint64, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Block] + + // SubscribeBlocksFromLatest subscribes to the finalized or sealed blocks starting at the latest sealed block, + // up until the latest available block. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlocksFromLatest( + ctx context.Context, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Block] + + // SubscribeBlockHeadersFromStartBlockID streams finalized or sealed block headers starting at the requested + // start block id, up until the latest available block header. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block header as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlockHeadersFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Header] + + // SubscribeBlockHeadersFromStartHeight streams finalized or sealed block headers starting at the requested + // start block height, up until the latest available block header. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block header as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlockHeadersFromStartHeight( + ctx context.Context, + startHeight uint64, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Header] + + // SubscribeBlockHeadersFromLatest streams finalized or sealed block headers starting at the latest sealed block, + // up until the latest available block header. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block header as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlockHeadersFromLatest( + ctx context.Context, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Header] + + // SubscribeBlockDigestsFromStartBlockID streams finalized or sealed lightweight block starting at the requested + // start block id, up until the latest available block. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlockDigestsFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.BlockDigest] + + // SubscribeBlockDigestsFromStartHeight streams finalized or sealed lightweight block starting at the requested + // start block height, up until the latest available block. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlockDigestsFromStartHeight( + ctx context.Context, + startHeight uint64, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.BlockDigest] + + // SubscribeBlockDigestsFromLatest streams finalized or sealed lightweight block starting at the latest sealed block, + // up until the latest available block. Once the latest is + // reached, the stream will remain open and responses are sent for each new + // block as it becomes available. + // + // Each block is filtered by the provided block status, and only blocks that match blockStatus + // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. + // + // If invalid parameters are supplied, a failed subscription is returned. + SubscribeBlockDigestsFromLatest( + ctx context.Context, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.BlockDigest] } // API provides all public-facing functionality of the Flow Access API. @@ -103,6 +236,7 @@ type API interface { ScriptsAPI TransactionsAPI TransactionStreamAPI + BlockStreamAPI // Ping responds to requests when the server is up. // @@ -249,102 +383,4 @@ type API interface { // Expected sentinel errors providing details to clients about failed requests: // - access.DataNotFoundError - No execution result with the given ID was found GetExecutionResultByID(ctx context.Context, id flow.Identifier) (*flow.ExecutionResult, error) - - // SubscribeBlocksFromStartBlockID subscribes to the finalized or sealed blocks starting at the - // requested start block id, up until the latest available block. Once the latest is reached, - // the stream will remain open and responses are sent for each new block as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlocksFromStartHeight subscribes to the finalized or sealed blocks starting at the requested - // start block height, up until the latest available block. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlocksFromLatest subscribes to the finalized or sealed blocks starting at the latest sealed block, - // up until the latest available block. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlockHeadersFromStartBlockID streams finalized or sealed block headers starting at the requested - // start block id, up until the latest available block header. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block header as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlockHeadersFromStartHeight streams finalized or sealed block headers starting at the requested - // start block height, up until the latest available block header. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block header as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlockHeadersFromLatest streams finalized or sealed block headers starting at the latest sealed block, - // up until the latest available block header. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block header as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlockDigestsFromStartBlockID streams finalized or sealed lightweight block starting at the requested - // start block id, up until the latest available block. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlockDigestsFromStartHeight streams finalized or sealed lightweight block starting at the requested - // start block height, up until the latest available block. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription - - // SubscribeBlockDigestsFromLatest streams finalized or sealed lightweight block starting at the latest sealed block, - // up until the latest available block. Once the latest is - // reached, the stream will remain open and responses are sent for each new - // block as it becomes available. - // - // Each block is filtered by the provided block status, and only blocks that match blockStatus - // are returned. blockStatus must be BlockStatusSealed or BlockStatusFinalized. - // - // If invalid parameters are supplied, a failed subscription is returned. - SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription } diff --git a/access/mock/api.go b/access/mock/api.go index 0ca6071e26e..fe545995c49 100644 --- a/access/mock/api.go +++ b/access/mock/api.go @@ -1165,19 +1165,19 @@ func (_m *API) Ping(ctx context.Context) error { } // SendAndSubscribeTransactionStatuses provides a mock function with given fields: ctx, tx, requiredEventEncodingVersion -func (_m *API) SendAndSubscribeTransactionStatuses(ctx context.Context, tx *flow.TransactionBody, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription { +func (_m *API) SendAndSubscribeTransactionStatuses(ctx context.Context, tx *flow.TransactionBody, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription[[]*modelaccess.TransactionResult] { ret := _m.Called(ctx, tx, requiredEventEncodingVersion) if len(ret) == 0 { panic("no return value specified for SendAndSubscribeTransactionStatuses") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, *flow.TransactionBody, entities.EventEncodingVersion) subscription.Subscription); ok { + var r0 subscription.Subscription[[]*modelaccess.TransactionResult] + if rf, ok := ret.Get(0).(func(context.Context, *flow.TransactionBody, entities.EventEncodingVersion) subscription.Subscription[[]*modelaccess.TransactionResult]); ok { r0 = rf(ctx, tx, requiredEventEncodingVersion) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[[]*modelaccess.TransactionResult]) } } @@ -1203,19 +1203,19 @@ func (_m *API) SendTransaction(ctx context.Context, tx *flow.TransactionBody) er } // SubscribeBlockDigestsFromLatest provides a mock function with given fields: ctx, blockStatus -func (_m *API) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription[*flow.BlockDigest] { ret := _m.Called(ctx, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlockDigestsFromLatest") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.BlockDigest] + if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription[*flow.BlockDigest]); ok { r0 = rf(ctx, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.BlockDigest]) } } @@ -1223,19 +1223,19 @@ func (_m *API) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus } // SubscribeBlockDigestsFromStartBlockID provides a mock function with given fields: ctx, startBlockID, blockStatus -func (_m *API) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription[*flow.BlockDigest] { ret := _m.Called(ctx, startBlockID, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlockDigestsFromStartBlockID") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.BlockDigest] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription[*flow.BlockDigest]); ok { r0 = rf(ctx, startBlockID, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.BlockDigest]) } } @@ -1243,19 +1243,19 @@ func (_m *API) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startB } // SubscribeBlockDigestsFromStartHeight provides a mock function with given fields: ctx, startHeight, blockStatus -func (_m *API) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription[*flow.BlockDigest] { ret := _m.Called(ctx, startHeight, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlockDigestsFromStartHeight") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.BlockDigest] + if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription[*flow.BlockDigest]); ok { r0 = rf(ctx, startHeight, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.BlockDigest]) } } @@ -1263,19 +1263,19 @@ func (_m *API) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHe } // SubscribeBlockHeadersFromLatest provides a mock function with given fields: ctx, blockStatus -func (_m *API) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Header] { ret := _m.Called(ctx, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlockHeadersFromLatest") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.Header] + if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription[*flow.Header]); ok { r0 = rf(ctx, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.Header]) } } @@ -1283,19 +1283,19 @@ func (_m *API) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus } // SubscribeBlockHeadersFromStartBlockID provides a mock function with given fields: ctx, startBlockID, blockStatus -func (_m *API) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Header] { ret := _m.Called(ctx, startBlockID, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlockHeadersFromStartBlockID") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.Header] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription[*flow.Header]); ok { r0 = rf(ctx, startBlockID, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.Header]) } } @@ -1303,19 +1303,19 @@ func (_m *API) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startB } // SubscribeBlockHeadersFromStartHeight provides a mock function with given fields: ctx, startHeight, blockStatus -func (_m *API) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Header] { ret := _m.Called(ctx, startHeight, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlockHeadersFromStartHeight") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.Header] + if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription[*flow.Header]); ok { r0 = rf(ctx, startHeight, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.Header]) } } @@ -1323,19 +1323,19 @@ func (_m *API) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHe } // SubscribeBlocksFromLatest provides a mock function with given fields: ctx, blockStatus -func (_m *API) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { ret := _m.Called(ctx, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlocksFromLatest") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.Block] + if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription[*flow.Block]); ok { r0 = rf(ctx, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.Block]) } } @@ -1343,19 +1343,19 @@ func (_m *API) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.B } // SubscribeBlocksFromStartBlockID provides a mock function with given fields: ctx, startBlockID, blockStatus -func (_m *API) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { ret := _m.Called(ctx, startBlockID, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlocksFromStartBlockID") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.Block] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription[*flow.Block]); ok { r0 = rf(ctx, startBlockID, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.Block]) } } @@ -1363,19 +1363,19 @@ func (_m *API) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID } // SubscribeBlocksFromStartHeight provides a mock function with given fields: ctx, startHeight, blockStatus -func (_m *API) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { +func (_m *API) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { ret := _m.Called(ctx, startHeight, blockStatus) if len(ret) == 0 { panic("no return value specified for SubscribeBlocksFromStartHeight") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription); ok { + var r0 subscription.Subscription[*flow.Block] + if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription[*flow.Block]); ok { r0 = rf(ctx, startHeight, blockStatus) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*flow.Block]) } } @@ -1383,19 +1383,19 @@ func (_m *API) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight u } // SubscribeTransactionStatuses provides a mock function with given fields: ctx, txID, requiredEventEncodingVersion -func (_m *API) SubscribeTransactionStatuses(ctx context.Context, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription { +func (_m *API) SubscribeTransactionStatuses(ctx context.Context, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription[[]*modelaccess.TransactionResult] { ret := _m.Called(ctx, txID, requiredEventEncodingVersion) if len(ret) == 0 { panic("no return value specified for SubscribeTransactionStatuses") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, entities.EventEncodingVersion) subscription.Subscription); ok { + var r0 subscription.Subscription[[]*modelaccess.TransactionResult] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, entities.EventEncodingVersion) subscription.Subscription[[]*modelaccess.TransactionResult]); ok { r0 = rf(ctx, txID, requiredEventEncodingVersion) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[[]*modelaccess.TransactionResult]) } } diff --git a/access/mock/block_stream_api.go b/access/mock/block_stream_api.go new file mode 100644 index 00000000000..af3cff13b25 --- /dev/null +++ b/access/mock/block_stream_api.go @@ -0,0 +1,211 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" + + subscription "github.com/onflow/flow-go/engine/access/subscription" +) + +// BlockStreamAPI is an autogenerated mock type for the BlockStreamAPI type +type BlockStreamAPI struct { + mock.Mock +} + +// SubscribeBlockDigestsFromLatest provides a mock function with given fields: ctx, blockStatus +func (_m *BlockStreamAPI) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription[*flow.BlockDigest] { + ret := _m.Called(ctx, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlockDigestsFromLatest") + } + + var r0 subscription.Subscription[*flow.BlockDigest] + if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription[*flow.BlockDigest]); ok { + r0 = rf(ctx, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.BlockDigest]) + } + } + + return r0 +} + +// SubscribeBlockDigestsFromStartBlockID provides a mock function with given fields: ctx, startBlockID, blockStatus +func (_m *BlockStreamAPI) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription[*flow.BlockDigest] { + ret := _m.Called(ctx, startBlockID, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlockDigestsFromStartBlockID") + } + + var r0 subscription.Subscription[*flow.BlockDigest] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription[*flow.BlockDigest]); ok { + r0 = rf(ctx, startBlockID, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.BlockDigest]) + } + } + + return r0 +} + +// SubscribeBlockDigestsFromStartHeight provides a mock function with given fields: ctx, startHeight, blockStatus +func (_m *BlockStreamAPI) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription[*flow.BlockDigest] { + ret := _m.Called(ctx, startHeight, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlockDigestsFromStartHeight") + } + + var r0 subscription.Subscription[*flow.BlockDigest] + if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription[*flow.BlockDigest]); ok { + r0 = rf(ctx, startHeight, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.BlockDigest]) + } + } + + return r0 +} + +// SubscribeBlockHeadersFromLatest provides a mock function with given fields: ctx, blockStatus +func (_m *BlockStreamAPI) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Header] { + ret := _m.Called(ctx, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlockHeadersFromLatest") + } + + var r0 subscription.Subscription[*flow.Header] + if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription[*flow.Header]); ok { + r0 = rf(ctx, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.Header]) + } + } + + return r0 +} + +// SubscribeBlockHeadersFromStartBlockID provides a mock function with given fields: ctx, startBlockID, blockStatus +func (_m *BlockStreamAPI) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Header] { + ret := _m.Called(ctx, startBlockID, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlockHeadersFromStartBlockID") + } + + var r0 subscription.Subscription[*flow.Header] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription[*flow.Header]); ok { + r0 = rf(ctx, startBlockID, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.Header]) + } + } + + return r0 +} + +// SubscribeBlockHeadersFromStartHeight provides a mock function with given fields: ctx, startHeight, blockStatus +func (_m *BlockStreamAPI) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Header] { + ret := _m.Called(ctx, startHeight, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlockHeadersFromStartHeight") + } + + var r0 subscription.Subscription[*flow.Header] + if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription[*flow.Header]); ok { + r0 = rf(ctx, startHeight, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.Header]) + } + } + + return r0 +} + +// SubscribeBlocksFromLatest provides a mock function with given fields: ctx, blockStatus +func (_m *BlockStreamAPI) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { + ret := _m.Called(ctx, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlocksFromLatest") + } + + var r0 subscription.Subscription[*flow.Block] + if rf, ok := ret.Get(0).(func(context.Context, flow.BlockStatus) subscription.Subscription[*flow.Block]); ok { + r0 = rf(ctx, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.Block]) + } + } + + return r0 +} + +// SubscribeBlocksFromStartBlockID provides a mock function with given fields: ctx, startBlockID, blockStatus +func (_m *BlockStreamAPI) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { + ret := _m.Called(ctx, startBlockID, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlocksFromStartBlockID") + } + + var r0 subscription.Subscription[*flow.Block] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, flow.BlockStatus) subscription.Subscription[*flow.Block]); ok { + r0 = rf(ctx, startBlockID, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.Block]) + } + } + + return r0 +} + +// SubscribeBlocksFromStartHeight provides a mock function with given fields: ctx, startHeight, blockStatus +func (_m *BlockStreamAPI) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { + ret := _m.Called(ctx, startHeight, blockStatus) + + if len(ret) == 0 { + panic("no return value specified for SubscribeBlocksFromStartHeight") + } + + var r0 subscription.Subscription[*flow.Block] + if rf, ok := ret.Get(0).(func(context.Context, uint64, flow.BlockStatus) subscription.Subscription[*flow.Block]); ok { + r0 = rf(ctx, startHeight, blockStatus) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*flow.Block]) + } + } + + return r0 +} + +// NewBlockStreamAPI creates a new instance of BlockStreamAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBlockStreamAPI(t interface { + mock.TestingT + Cleanup(func()) +}) *BlockStreamAPI { + mock := &BlockStreamAPI{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/access/mock/transaction_stream_api.go b/access/mock/transaction_stream_api.go index 9968f9b71f8..fcd90175561 100644 --- a/access/mock/transaction_stream_api.go +++ b/access/mock/transaction_stream_api.go @@ -5,9 +5,12 @@ package mock import ( context "context" - flow "github.com/onflow/flow-go/model/flow" + access "github.com/onflow/flow-go/model/access" + entities "github.com/onflow/flow/protobuf/go/flow/entities" + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" subscription "github.com/onflow/flow-go/engine/access/subscription" @@ -19,19 +22,19 @@ type TransactionStreamAPI struct { } // SendAndSubscribeTransactionStatuses provides a mock function with given fields: ctx, tx, requiredEventEncodingVersion -func (_m *TransactionStreamAPI) SendAndSubscribeTransactionStatuses(ctx context.Context, tx *flow.TransactionBody, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription { +func (_m *TransactionStreamAPI) SendAndSubscribeTransactionStatuses(ctx context.Context, tx *flow.TransactionBody, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription[[]*access.TransactionResult] { ret := _m.Called(ctx, tx, requiredEventEncodingVersion) if len(ret) == 0 { panic("no return value specified for SendAndSubscribeTransactionStatuses") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, *flow.TransactionBody, entities.EventEncodingVersion) subscription.Subscription); ok { + var r0 subscription.Subscription[[]*access.TransactionResult] + if rf, ok := ret.Get(0).(func(context.Context, *flow.TransactionBody, entities.EventEncodingVersion) subscription.Subscription[[]*access.TransactionResult]); ok { r0 = rf(ctx, tx, requiredEventEncodingVersion) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[[]*access.TransactionResult]) } } @@ -39,19 +42,19 @@ func (_m *TransactionStreamAPI) SendAndSubscribeTransactionStatuses(ctx context. } // SubscribeTransactionStatuses provides a mock function with given fields: ctx, txID, requiredEventEncodingVersion -func (_m *TransactionStreamAPI) SubscribeTransactionStatuses(ctx context.Context, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription { +func (_m *TransactionStreamAPI) SubscribeTransactionStatuses(ctx context.Context, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion) subscription.Subscription[[]*access.TransactionResult] { ret := _m.Called(ctx, txID, requiredEventEncodingVersion) if len(ret) == 0 { panic("no return value specified for SubscribeTransactionStatuses") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, entities.EventEncodingVersion) subscription.Subscription); ok { + var r0 subscription.Subscription[[]*access.TransactionResult] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, entities.EventEncodingVersion) subscription.Subscription[[]*access.TransactionResult]); ok { r0 = rf(ctx, txID, requiredEventEncodingVersion) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[[]*access.TransactionResult]) } } diff --git a/cmd/access/node_builder/access_node_builder.go b/cmd/access/node_builder/access_node_builder.go index 68850f4cd11..5fcfea9748a 100644 --- a/cmd/access/node_builder/access_node_builder.go +++ b/cmd/access/node_builder/access_node_builder.go @@ -56,6 +56,7 @@ import ( "github.com/onflow/flow-go/engine/access/state_stream" statestreambackend "github.com/onflow/flow-go/engine/access/state_stream/backend" "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription/streamer" subscriptiontracker "github.com/onflow/flow-go/engine/access/subscription/tracker" followereng "github.com/onflow/flow-go/engine/common/follower" "github.com/onflow/flow-go/engine/common/requester" @@ -1099,6 +1100,8 @@ func (builder *FlowAccessNodeBuilder) BuildExecutionSyncComponents() *FlowAccess useIndex, ) + streamOptions := streamer.NewDefaultStreamOptions() + builder.stateStreamBackend, err = statestreambackend.New( node.Logger, node.State, @@ -1111,16 +1114,11 @@ func (builder *FlowAccessNodeBuilder) BuildExecutionSyncComponents() *FlowAccess builder.EventsIndex, useIndex, int(builder.stateStreamConf.RegisterIDsRequestLimit), - subscription.NewSubscriptionHandler( - builder.Logger, - broadcaster, - builder.stateStreamConf.ClientSendTimeout, - builder.stateStreamConf.ResponseLimit, - builder.stateStreamConf.ClientSendBufferSize, - ), executionDataTracker, notNil(builder.executionResultInfoProvider), builder.executionStateCache, // might be nil + broadcaster, + streamOptions, ) if err != nil { return nil, fmt.Errorf("could not create state stream backend: %w", err) @@ -2217,37 +2215,30 @@ func (builder *FlowAccessNodeBuilder) Build() (cmd.Node, error) { ) builder.nodeBackend, err = backend.New(backend.Params{ - State: node.State, - CollectionRPC: builder.CollectionRPC, // might be nil - HistoricalAccessNodes: notNil(builder.HistoricalAccessRPCs), - Blocks: node.Storage.Blocks, - Headers: node.Storage.Headers, - Collections: notNil(builder.collections), - Transactions: notNil(builder.transactions), - ExecutionReceipts: node.Storage.Receipts, - ExecutionResults: node.Storage.Results, - TxResultErrorMessages: builder.transactionResultErrorMessages, // might be nil - ChainID: node.RootChainID, - AccessMetrics: notNil(builder.AccessMetrics), - ConnFactory: connFactory, - RetryEnabled: builder.retryEnabled, - MaxHeightRange: backendConfig.MaxHeightRange, - Log: node.Logger, - SnapshotHistoryLimit: backend.DefaultSnapshotHistoryLimit, - Communicator: nodeCommunicator, - TxResultCacheSize: builder.TxResultCacheSize, - ScriptExecutor: notNil(builder.ScriptExecutor), - ScriptExecutionMode: scriptExecMode, - CheckPayerBalanceMode: checkPayerBalanceMode, - EventQueryMode: eventQueryMode, - BlockTracker: blockTracker, - SubscriptionHandler: subscription.NewSubscriptionHandler( - builder.Logger, - broadcaster, - builder.stateStreamConf.ClientSendTimeout, - builder.stateStreamConf.ResponseLimit, - builder.stateStreamConf.ClientSendBufferSize, - ), + State: node.State, + CollectionRPC: builder.CollectionRPC, // might be nil + HistoricalAccessNodes: notNil(builder.HistoricalAccessRPCs), + Blocks: node.Storage.Blocks, + Headers: node.Storage.Headers, + Collections: notNil(builder.collections), + Transactions: notNil(builder.transactions), + ExecutionReceipts: node.Storage.Receipts, + ExecutionResults: node.Storage.Results, + TxResultErrorMessages: builder.transactionResultErrorMessages, // might be nil + ChainID: node.RootChainID, + AccessMetrics: notNil(builder.AccessMetrics), + ConnFactory: connFactory, + RetryEnabled: builder.retryEnabled, + MaxHeightRange: backendConfig.MaxHeightRange, + Log: node.Logger, + SnapshotHistoryLimit: backend.DefaultSnapshotHistoryLimit, + Communicator: nodeCommunicator, + TxResultCacheSize: builder.TxResultCacheSize, + ScriptExecutor: notNil(builder.ScriptExecutor), + ScriptExecutionMode: scriptExecMode, + CheckPayerBalanceMode: checkPayerBalanceMode, + EventQueryMode: eventQueryMode, + BlockTracker: blockTracker, EventsIndex: notNil(builder.EventsIndex), TxResultQueryMode: txResultQueryMode, TxResultsIndex: notNil(builder.TxResultsIndex), diff --git a/cmd/observer/node_builder/observer_builder.go b/cmd/observer/node_builder/observer_builder.go index 80eb929df78..8dc57780325 100644 --- a/cmd/observer/node_builder/observer_builder.go +++ b/cmd/observer/node_builder/observer_builder.go @@ -50,8 +50,9 @@ import ( rpcConnection "github.com/onflow/flow-go/engine/access/rpc/connection" "github.com/onflow/flow-go/engine/access/state_stream" statestreambackend "github.com/onflow/flow-go/engine/access/state_stream/backend" - "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription/streamer" subscriptiontracker "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription_old" "github.com/onflow/flow-go/engine/common/follower" commonrpc "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/stop" @@ -215,13 +216,13 @@ func DefaultObserverServiceConfig() *ObserverServiceConfig { }, stateStreamConf: statestreambackend.Config{ MaxExecutionDataMsgSize: commonrpc.DefaultAccessMaxResponseSize, - ExecutionDataCacheSize: subscription.DefaultCacheSize, - ClientSendTimeout: subscription.DefaultSendTimeout, - ClientSendBufferSize: subscription.DefaultSendBufferSize, - MaxGlobalStreams: subscription.DefaultMaxGlobalStreams, + ExecutionDataCacheSize: subscription_old.DefaultCacheSize, + ClientSendTimeout: subscription_old.DefaultSendTimeout, + ClientSendBufferSize: subscription_old.DefaultSendBufferSize, + MaxGlobalStreams: subscription_old.DefaultMaxGlobalStreams, EventFilterConfig: state_stream.DefaultEventFilterConfig, - ResponseLimit: subscription.DefaultResponseLimit, - HeartbeatInterval: subscription.DefaultHeartbeatInterval, + ResponseLimit: subscription_old.DefaultResponseLimit, + HeartbeatInterval: subscription_old.DefaultHeartbeatInterval, RegisterIDsRequestLimit: state_stream.DefaultRegisterIDsRequestLimit, }, stateStreamFilterConf: nil, @@ -1653,6 +1654,8 @@ func (builder *ObserverServiceBuilder) BuildExecutionSyncComponents() *ObserverS useIndex, ) + streamOptions := streamer.NewDefaultStreamOptions() + builder.stateStreamBackend, err = statestreambackend.New( node.Logger, node.State, @@ -1665,16 +1668,11 @@ func (builder *ObserverServiceBuilder) BuildExecutionSyncComponents() *ObserverS builder.EventsIndex, useIndex, int(builder.stateStreamConf.RegisterIDsRequestLimit), - subscription.NewSubscriptionHandler( - builder.Logger, - broadcaster, - builder.stateStreamConf.ClientSendTimeout, - builder.stateStreamConf.ResponseLimit, - builder.stateStreamConf.ClientSendBufferSize, - ), executionDataTracker, builder.executionResultInfoProvider, builder.executionStateCache, + broadcaster, + streamOptions, ) if err != nil { return nil, fmt.Errorf("could not create state stream backend: %w", err) @@ -2051,32 +2049,25 @@ func (builder *ObserverServiceBuilder) enqueueRPCServer() { ) backendParams := backend.Params{ - State: node.State, - Blocks: node.Storage.Blocks, - Headers: node.Storage.Headers, - Collections: node.Storage.Collections, - Transactions: node.Storage.Transactions, - ExecutionReceipts: node.Storage.Receipts, - ExecutionResults: node.Storage.Results, - ChainID: node.RootChainID, - AccessMetrics: accessMetrics, - ConnFactory: connFactory, - RetryEnabled: false, - MaxHeightRange: backendConfig.MaxHeightRange, - Log: node.Logger, - SnapshotHistoryLimit: backend.DefaultSnapshotHistoryLimit, - Communicator: node_communicator.NewNodeCommunicator(backendConfig.CircuitBreakerConfig.Enabled), - BlockTracker: blockTracker, - ScriptExecutionMode: scriptExecMode, - EventQueryMode: eventQueryMode, - TxResultQueryMode: txResultQueryMode, - SubscriptionHandler: subscription.NewSubscriptionHandler( - builder.Logger, - broadcaster, - builder.stateStreamConf.ClientSendTimeout, - builder.stateStreamConf.ResponseLimit, - builder.stateStreamConf.ClientSendBufferSize, - ), + State: node.State, + Blocks: node.Storage.Blocks, + Headers: node.Storage.Headers, + Collections: node.Storage.Collections, + Transactions: node.Storage.Transactions, + ExecutionReceipts: node.Storage.Receipts, + ExecutionResults: node.Storage.Results, + ChainID: node.RootChainID, + AccessMetrics: accessMetrics, + ConnFactory: connFactory, + RetryEnabled: false, + MaxHeightRange: backendConfig.MaxHeightRange, + Log: node.Logger, + SnapshotHistoryLimit: backend.DefaultSnapshotHistoryLimit, + Communicator: node_communicator.NewNodeCommunicator(backendConfig.CircuitBreakerConfig.Enabled), + BlockTracker: blockTracker, + ScriptExecutionMode: scriptExecMode, + EventQueryMode: eventQueryMode, + TxResultQueryMode: txResultQueryMode, IndexReporter: indexReporter, VersionControl: builder.VersionControl, ExecNodeIdentitiesProvider: execNodeIdentitiesProvider, diff --git a/cmd/util/cmd/run-script/cmd.go b/cmd/util/cmd/run-script/cmd.go index ba81e5a98eb..9ba55501116 100644 --- a/cmd/util/cmd/run-script/cmd.go +++ b/cmd/util/cmd/run-script/cmd.go @@ -19,6 +19,7 @@ import ( "github.com/onflow/flow-go/engine/access/rest/websockets" "github.com/onflow/flow-go/engine/access/state_stream/backend" "github.com/onflow/flow-go/engine/access/subscription" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" "github.com/onflow/flow-go/engine/execution/computation" "github.com/onflow/flow-go/fvm" "github.com/onflow/flow-go/fvm/storage/snapshot" @@ -489,83 +490,83 @@ func (*api) SubscribeBlocksFromStartBlockID( _ context.Context, _ flow.Identifier, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.Block] { + return subimpl.NewFailedSubscription[*flow.Block](ErrNotImplemented, "failed to call SubscribeBlocksFromStartBlockID") } func (*api) SubscribeBlocksFromStartHeight( _ context.Context, _ uint64, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.Block] { + return subimpl.NewFailedSubscription[*flow.Block](ErrNotImplemented, "failed to call SubscribeBlocksFromStartHeight") } func (*api) SubscribeBlocksFromLatest( _ context.Context, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.Block] { + return subimpl.NewFailedSubscription[*flow.Block](ErrNotImplemented, "failed to call SubscribeBlocksFromLatest") } func (*api) SubscribeBlockHeadersFromStartBlockID( _ context.Context, _ flow.Identifier, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.Header] { + return subimpl.NewFailedSubscription[*flow.Header](ErrNotImplemented, "failed to call SubscribeBlockHeadersFromStartBlockID") } func (*api) SubscribeBlockHeadersFromStartHeight( _ context.Context, _ uint64, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.Header] { + return subimpl.NewFailedSubscription[*flow.Header](ErrNotImplemented, "failed to call SubscribeBlockHeadersFromStartHeight") } func (*api) SubscribeBlockHeadersFromLatest( _ context.Context, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.Header] { + return subimpl.NewFailedSubscription[*flow.Header](ErrNotImplemented, "failed to call SubscribeBlockHeadersFromLatest") } func (*api) SubscribeBlockDigestsFromStartBlockID( _ context.Context, _ flow.Identifier, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.BlockDigest] { + return subimpl.NewFailedSubscription[*flow.BlockDigest](ErrNotImplemented, "failed to call SubscribeBlockDigestsFromStartBlockID") } func (*api) SubscribeBlockDigestsFromStartHeight( _ context.Context, _ uint64, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.BlockDigest] { + return subimpl.NewFailedSubscription[*flow.BlockDigest](ErrNotImplemented, "failed to call SubscribeBlockDigestsFromStartHeight") } func (*api) SubscribeBlockDigestsFromLatest( _ context.Context, _ flow.BlockStatus, -) subscription.Subscription { - return nil +) subscription.Subscription[*flow.BlockDigest] { + return subimpl.NewFailedSubscription[*flow.BlockDigest](ErrNotImplemented, "failed to call SubscribeBlockDigestsFromLatest") } func (a *api) SubscribeTransactionStatuses( _ context.Context, _ flow.Identifier, _ entities.EventEncodingVersion, -) subscription.Subscription { - return subscription.NewFailedSubscription(ErrNotImplemented, "failed to call SubscribeTransactionStatuses") +) subscription.Subscription[[]*accessmodel.TransactionResult] { + return subimpl.NewFailedSubscription[[]*accessmodel.TransactionResult](ErrNotImplemented, "failed to call SubscribeTransactionStatuses") } func (a *api) SendAndSubscribeTransactionStatuses( _ context.Context, _ *flow.TransactionBody, _ entities.EventEncodingVersion, -) subscription.Subscription { - return subscription.NewFailedSubscription(ErrNotImplemented, "failed to call SendAndSubscribeTransactionStatuses") +) subscription.Subscription[[]*accessmodel.TransactionResult] { + return subimpl.NewFailedSubscription[[]*accessmodel.TransactionResult](ErrNotImplemented, "failed to call SendAndSubscribeTransactionStatuses") } diff --git a/engine/access/integration_unsecure_grpc_server_test.go b/engine/access/integration_unsecure_grpc_server_test.go index f9e96c6f46f..fec5599702f 100644 --- a/engine/access/integration_unsecure_grpc_server_test.go +++ b/engine/access/integration_unsecure_grpc_server_test.go @@ -29,6 +29,7 @@ import ( "github.com/onflow/flow-go/engine/access/state_stream" statestreambackend "github.com/onflow/flow-go/engine/access/state_stream/backend" "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription/streamer" "github.com/onflow/flow-go/engine/access/subscription/tracker" commonrpc "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/model/flow" @@ -67,7 +68,7 @@ type SameGRPCPortTestSuite struct { metrics *metrics.NoopCollector rpcEng *rpc.Engine stateStreamEng *statestreambackend.Engine - executionDataTracker tracker.ExecutionDataTracker + executionDataTracker subscription.ExecutionDataTracker // storage blocks *storagemock.Blocks @@ -92,6 +93,7 @@ type SameGRPCPortTestSuite struct { broadcaster *engine.Broadcaster execDataCache *cache.ExecutionDataCache execDataHeroCache *herocache.BlockExecutionData + streamOptions *streamer.StreamOptions blockMap map[uint64]*flow.Block @@ -131,6 +133,7 @@ func (suite *SameGRPCPortTestSuite) SetupTest() { suite.eds = execution_data.NewExecutionDataStore(suite.bs, execution_data.DefaultSerializer) suite.broadcaster = engine.NewBroadcaster() + suite.streamOptions = streamer.NewDefaultStreamOptions() suite.execDataHeroCache = herocache.NewBlockExecutionData(subscription.DefaultCacheSize, suite.log, metrics.NewNoopCollector()) suite.execDataCache = cache.NewExecutionDataCache(suite.eds, suite.headers, suite.seals, suite.results, suite.execDataHeroCache) @@ -260,14 +263,6 @@ func (suite *SameGRPCPortTestSuite) SetupTest() { ClientSendBufferSize: subscription.DefaultSendBufferSize, } - subscriptionHandler := subscription.NewSubscriptionHandler( - suite.log, - suite.broadcaster, - subscription.DefaultSendTimeout, - subscription.DefaultResponseLimit, - subscription.DefaultSendBufferSize, - ) - eventIndexer := index.NewEventsIndex(index.NewReporter(), suite.events) suite.executionDataTracker = tracker.NewExecutionDataTracker( @@ -293,10 +288,11 @@ func (suite *SameGRPCPortTestSuite) SetupTest() { eventIndexer, false, state_stream.DefaultRegisterIDsRequestLimit, - subscriptionHandler, suite.executionDataTracker, suite.executionResultInfoProvider, suite.executionStateCache, + suite.broadcaster, + suite.streamOptions, ) assert.NoError(suite.T(), err) diff --git a/engine/access/rest/websockets/controller.go b/engine/access/rest/websockets/controller.go index adba5e4566b..665e3d65f2a 100644 --- a/engine/access/rest/websockets/controller.go +++ b/engine/access/rest/websockets/controller.go @@ -130,7 +130,7 @@ type Controller struct { // // This design ensures that the channel is only closed when it is safe to do so, avoiding // issues such as sending on a closed channel while maintaining proper cleanup. - multiplexedStream chan interface{} + multiplexedStream chan any dataProviders *concurrentmap.Map[SubscriptionID, dp.DataProvider] dataProviderFactory dp.DataProviderFactory @@ -153,7 +153,7 @@ func NewWebSocketController( logger: logger.With().Str("component", "websocket-controller").Logger(), config: config, conn: conn, - multiplexedStream: make(chan interface{}), + multiplexedStream: make(chan any), dataProviders: concurrentmap.New[SubscriptionID, dp.DataProvider](), dataProviderFactory: dataProviderFactory, dataProvidersGroup: &sync.WaitGroup{}, @@ -451,7 +451,13 @@ func (c *Controller) handleSubscribe(ctx context.Context, msg models.SubscribeMe } // register new provider - provider, err := c.dataProviderFactory.NewDataProvider(ctx, subscriptionID.String(), msg.Topic, msg.Arguments, c.multiplexedStream) + provider, err := c.dataProviderFactory.NewDataProvider( + ctx, + subscriptionID.String(), + msg.Topic, + msg.Arguments, + c.multiplexedStream, + ) if err != nil { err = fmt.Errorf("error creating data provider: %w", err) c.writeErrorResponse( @@ -565,7 +571,7 @@ func (c *Controller) writeErrorResponse(ctx context.Context, err error, msg mode c.writeResponse(ctx, msg) } -func (c *Controller) writeResponse(ctx context.Context, response interface{}) { +func (c *Controller) writeResponse(ctx context.Context, response any) { select { case <-ctx.Done(): return diff --git a/engine/access/rest/websockets/data_providers/account_statuses_provider.go b/engine/access/rest/websockets/data_providers/account_statuses_provider.go index 9ad67d9ad52..c4a60ba145b 100644 --- a/engine/access/rest/websockets/data_providers/account_statuses_provider.go +++ b/engine/access/rest/websockets/data_providers/account_statuses_provider.go @@ -10,7 +10,6 @@ import ( "github.com/onflow/flow-go/engine/access/rest/websockets/data_providers/models" wsmodels "github.com/onflow/flow-go/engine/access/rest/websockets/models" "github.com/onflow/flow-go/engine/access/state_stream" - "github.com/onflow/flow-go/engine/access/state_stream/backend" "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/counters" @@ -43,7 +42,7 @@ func NewAccountStatusesDataProvider( subscriptionID string, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, chain flow.Chain, eventFilterConfig state_stream.EventFilterConfig, defaultHeartbeatInterval uint64, @@ -92,7 +91,7 @@ func (p *AccountStatusesDataProvider) Run() error { // This function is not expected to be called concurrently. // // No errors expected during normal operations. -func (p *AccountStatusesDataProvider) handleResponse(response *backend.AccountStatusesResponse) error { +func (p *AccountStatusesDataProvider) handleResponse(response *state_stream.AccountStatusesResponse) error { // convert events to JSON-CDC format convertedResponse, err := convertAccountStatusesResponse(response) if err != nil { @@ -106,7 +105,7 @@ func (p *AccountStatusesDataProvider) handleResponse(response *backend.AccountSt // This function is not safe to call concurrently. // // No errors are expected during normal operations -func (p *AccountStatusesDataProvider) sendResponse(response *backend.AccountStatusesResponse) error { +func (p *AccountStatusesDataProvider) sendResponse(response *state_stream.AccountStatusesResponse) error { // Only send a response if there's meaningful data to send // or the heartbeat interval limit is reached p.blocksSinceLastMessage += 1 @@ -134,7 +133,7 @@ func (p *AccountStatusesDataProvider) sendResponse(response *backend.AccountStat func (p *AccountStatusesDataProvider) createAndStartSubscription( ctx context.Context, args accountStatusesArguments, -) subscription.Subscription { +) subscription.Subscription[*state_stream.AccountStatusesResponse] { if args.StartBlockID != flow.ZeroID { return p.stateStreamApi.SubscribeAccountStatusesFromStartBlockID(ctx, args.StartBlockID, args.Filter) } @@ -150,7 +149,7 @@ func (p *AccountStatusesDataProvider) createAndStartSubscription( // to JSON-CDC format. // // No errors expected during normal operations. -func convertAccountStatusesResponse(resp *backend.AccountStatusesResponse) (*backend.AccountStatusesResponse, error) { +func convertAccountStatusesResponse(resp *state_stream.AccountStatusesResponse) (*state_stream.AccountStatusesResponse, error) { jsoncdcEvents := make(map[string]flow.EventsList, len(resp.AccountEvents)) for eventType, events := range resp.AccountEvents { convertedEvents, err := convertEvents(events) @@ -160,7 +159,7 @@ func convertAccountStatusesResponse(resp *backend.AccountStatusesResponse) (*bac jsoncdcEvents[eventType] = convertedEvents } - return &backend.AccountStatusesResponse{ + return &state_stream.AccountStatusesResponse{ BlockID: resp.BlockID, Height: resp.Height, AccountEvents: jsoncdcEvents, diff --git a/engine/access/rest/websockets/data_providers/account_statuses_provider_test.go b/engine/access/rest/websockets/data_providers/account_statuses_provider_test.go index aef1c3e14e1..c044c172cdd 100644 --- a/engine/access/rest/websockets/data_providers/account_statuses_provider_test.go +++ b/engine/access/rest/websockets/data_providers/account_statuses_provider_test.go @@ -16,7 +16,6 @@ import ( "github.com/onflow/flow-go/engine/access/rest/websockets/data_providers/models" wsmodels "github.com/onflow/flow-go/engine/access/rest/websockets/models" "github.com/onflow/flow-go/engine/access/state_stream" - "github.com/onflow/flow-go/engine/access/state_stream/backend" ssmock "github.com/onflow/flow-go/engine/access/state_stream/mock" "github.com/onflow/flow-go/engine/access/subscription" submock "github.com/onflow/flow-go/engine/access/subscription/mock" @@ -77,12 +76,12 @@ func (s *AccountStatusesProviderSuite) TestAccountStatusesDataProvider_HappyPath backendResponses := s.backendAccountStatusesResponses(events) - testHappyPath( + testHappyPath[*state_stream.AccountStatusesResponse, any]( s.T(), AccountStatusesTopic, s.factory, s.subscribeAccountStatusesDataProviderTestCases(backendResponses), - func(dataChan chan interface{}) { + func(dataChan chan *state_stream.AccountStatusesResponse) { for i := 0; i < len(backendResponses); i++ { dataChan <- backendResponses[i] } @@ -92,7 +91,7 @@ func (s *AccountStatusesProviderSuite) TestAccountStatusesDataProvider_HappyPath } func (s *AccountStatusesProviderSuite) TestAccountStatusesDataProvider_StateStreamNotConfigured() { - send := make(chan interface{}) + send := make(chan any) topic := AccountStatusesTopic provider, err := NewAccountStatusesDataProvider( @@ -113,11 +112,11 @@ func (s *AccountStatusesProviderSuite) TestAccountStatusesDataProvider_StateStre } func (s *AccountStatusesProviderSuite) subscribeAccountStatusesDataProviderTestCases( - backendResponses []*backend.AccountStatusesResponse, -) []testType { + backendResponses []*state_stream.AccountStatusesResponse, +) []testType[*state_stream.AccountStatusesResponse, any] { expectedResponses := s.expectedAccountStatusesResponses(backendResponses) - return []testType{ + return []testType[*state_stream.AccountStatusesResponse, any]{ { name: "SubscribeAccountStatusesFromStartBlockID happy path", arguments: wsmodels.Arguments{ @@ -125,7 +124,7 @@ func (s *AccountStatusesProviderSuite) subscribeAccountStatusesDataProviderTestC "event_types": []string{string(flow.EventAccountCreated)}, "account_addresses": []string{unittest.AddressFixture().String()}, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*state_stream.AccountStatusesResponse]) { s.api.On( "SubscribeAccountStatusesFromStartBlockID", mock.Anything, @@ -142,7 +141,7 @@ func (s *AccountStatusesProviderSuite) subscribeAccountStatusesDataProviderTestC "event_types": []string{string(flow.EventAccountCreated)}, "account_addresses": []string{unittest.AddressFixture().String()}, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*state_stream.AccountStatusesResponse]) { s.api.On( "SubscribeAccountStatusesFromStartHeight", mock.Anything, @@ -158,7 +157,7 @@ func (s *AccountStatusesProviderSuite) subscribeAccountStatusesDataProviderTestC "event_types": []string{string(flow.EventAccountCreated)}, "account_addresses": []string{unittest.AddressFixture().String()}, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*state_stream.AccountStatusesResponse]) { s.api.On( "SubscribeAccountStatusesFromLatestBlock", mock.Anything, @@ -171,7 +170,7 @@ func (s *AccountStatusesProviderSuite) subscribeAccountStatusesDataProviderTestC } // requireAccountStatuses ensures that the received account statuses information matches the expected data. -func (s *AccountStatusesProviderSuite) requireAccountStatuses(actual interface{}, expected interface{}) { +func (s *AccountStatusesProviderSuite) requireAccountStatuses(actual any, expected any) { expectedResponse, expectedResponsePayload := extractPayload[*models.AccountStatusesResponse](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*models.AccountStatusesResponse](s.T(), actual) @@ -190,12 +189,12 @@ func (s *AccountStatusesProviderSuite) requireAccountStatuses(actual interface{} } // expectedAccountStatusesResponses creates the expected responses for the provided events and backend responses. -func (s *AccountStatusesProviderSuite) expectedAccountStatusesResponses(backendResponses []*backend.AccountStatusesResponse) []interface{} { - expectedResponses := make([]interface{}, len(backendResponses)) +func (s *AccountStatusesProviderSuite) expectedAccountStatusesResponses(backendResponses []*state_stream.AccountStatusesResponse) []any { + expectedResponses := make([]any, len(backendResponses)) for i, resp := range backendResponses { // avoid updating the original response - expected := &backend.AccountStatusesResponse{ + expected := &state_stream.AccountStatusesResponse{ Height: resp.Height, BlockID: resp.BlockID, AccountEvents: make(map[string]flow.EventsList, len(resp.AccountEvents)), @@ -226,7 +225,7 @@ func (s *AccountStatusesProviderSuite) expectedAccountStatusesResponses(backendR // when invalid arguments are provided. It verifies that appropriate errors are returned // for missing or conflicting arguments. func (s *AccountStatusesProviderSuite) TestAccountStatusesDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) topic := AccountStatusesTopic for _, test := range invalidAccountStatusesArgumentsTestCases() { @@ -252,22 +251,22 @@ func (s *AccountStatusesProviderSuite) TestAccountStatusesDataProvider_InvalidAr // TestMessageIndexAccountStatusesProviderResponse_HappyPath tests that MessageIndex values in response are strictly increasing. func (s *AccountStatusesProviderSuite) TestMessageIndexAccountStatusesProviderResponse_HappyPath() { - send := make(chan interface{}, 10) + send := make(chan any, 10) topic := AccountStatusesTopic accountStatusesCount := 4 // Create a channel to simulate the subscription's account statuses channel - accountStatusesChan := make(chan interface{}) + accountStatusesChan := make(chan *state_stream.AccountStatusesResponse) // Create a mock subscription and mock the channel - sub := submock.NewSubscription(s.T()) - sub.On("Channel").Return((<-chan interface{})(accountStatusesChan)) + sub := submock.NewSubscription[*state_stream.AccountStatusesResponse](s.T()) + sub.On("Channel").Return((<-chan *state_stream.AccountStatusesResponse)(accountStatusesChan)) sub.On("Err").Return(nil).Once() s.api.On("SubscribeAccountStatusesFromStartBlockID", mock.Anything, mock.Anything, mock.Anything).Return(sub) arguments := - map[string]interface{}{ + map[string]any{ "start_block_id": s.rootBlock.ID().String(), "event_types": []string{string(flow.EventAccountCreated)}, "account_addresses": []string{unittest.AddressFixture().String()}, @@ -305,7 +304,7 @@ func (s *AccountStatusesProviderSuite) TestMessageIndexAccountStatusesProviderRe defer close(accountStatusesChan) // Close the channel when done for i := 0; i < accountStatusesCount; i++ { - accountStatusesChan <- &backend.AccountStatusesResponse{} + accountStatusesChan <- &state_stream.AccountStatusesResponse{} } }() @@ -334,11 +333,11 @@ func (s *AccountStatusesProviderSuite) TestMessageIndexAccountStatusesProviderRe } // backendAccountStatusesResponses creates backend account statuses responses based on the provided events. -func (s *AccountStatusesProviderSuite) backendAccountStatusesResponses(events []flow.Event) []*backend.AccountStatusesResponse { - responses := make([]*backend.AccountStatusesResponse, len(events)) +func (s *AccountStatusesProviderSuite) backendAccountStatusesResponses(events []flow.Event) []*state_stream.AccountStatusesResponse { + responses := make([]*state_stream.AccountStatusesResponse, len(events)) for i := range events { - responses[i] = &backend.AccountStatusesResponse{ + responses[i] = &state_stream.AccountStatusesResponse{ Height: s.rootBlock.Height, BlockID: s.rootBlock.ID(), AccountEvents: map[string]flow.EventsList{ @@ -364,7 +363,7 @@ func invalidAccountStatusesArgumentsTestCases() []testErrType { }, { name: "invalid 'start_block_id' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_id": "invalid_block_id", "event_types": []string{state_stream.CoreEventAccountCreated}, "account_addresses": []string{unittest.AddressFixture().String()}, @@ -373,7 +372,7 @@ func invalidAccountStatusesArgumentsTestCases() []testErrType { }, { name: "invalid 'start_block_height' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_height": "-1", "event_types": []string{state_stream.CoreEventAccountCreated}, "account_addresses": []string{unittest.AddressFixture().String()}, @@ -382,7 +381,7 @@ func invalidAccountStatusesArgumentsTestCases() []testErrType { }, { name: "invalid 'heartbeat_interval' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_id": unittest.BlockFixture().ID().String(), "event_types": []string{state_stream.CoreEventAccountCreated}, "account_addresses": []string{unittest.AddressFixture().String()}, @@ -392,7 +391,7 @@ func invalidAccountStatusesArgumentsTestCases() []testErrType { }, { name: "unexpected argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_id": unittest.BlockFixture().ID().String(), "event_types": []string{state_stream.CoreEventAccountCreated}, "account_addresses": []string{unittest.AddressFixture().String()}, diff --git a/engine/access/rest/websockets/data_providers/args_validation.go b/engine/access/rest/websockets/data_providers/args_validation.go index 831801cdff4..7c316c5406c 100644 --- a/engine/access/rest/websockets/data_providers/args_validation.go +++ b/engine/access/rest/websockets/data_providers/args_validation.go @@ -8,7 +8,7 @@ import ( "github.com/onflow/flow-go/engine/access/rest/websockets/models" ) -func ensureAllowedFields(fields map[string]interface{}, allowedFields map[string]struct{}) error { +func ensureAllowedFields(fields map[string]any, allowedFields map[string]struct{}) error { // Ensure only allowed fields are present for key := range fields { if _, exists := allowedFields[key]; !exists { diff --git a/engine/access/rest/websockets/data_providers/base_provider.go b/engine/access/rest/websockets/data_providers/base_provider.go index 6d6e55bb49d..45b1ccbf3b7 100644 --- a/engine/access/rest/websockets/data_providers/base_provider.go +++ b/engine/access/rest/websockets/data_providers/base_provider.go @@ -20,7 +20,7 @@ type baseDataProvider struct { subscriptionID string topic string rawArguments wsmodels.Arguments - send chan<- interface{} + send chan<- any cancelSubscriptionContext context.CancelFunc } @@ -32,7 +32,7 @@ func newBaseDataProvider( subscriptionID string, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, ) *baseDataProvider { ctx, cancel := context.WithCancel(ctx) return &baseDataProvider{ @@ -90,11 +90,11 @@ type sendResponseCallback[T any] func(T) error // // No other errors are expected during normal operation func run[T any]( - subscription subscription.Subscription, + subscription subscription.Subscription[T], sendResponse sendResponseCallback[T], ) error { for { - value, ok := <-subscription.Channel() + response, ok := <-subscription.Channel() if !ok { err := subscription.Err() if err != nil && !errors.Is(err, context.Canceled) { @@ -104,11 +104,6 @@ func run[T any]( return nil } - response, ok := value.(T) - if !ok { - return fmt.Errorf("unexpected response type: %T", value) - } - err := sendResponse(response) if err != nil { return fmt.Errorf("error sending response: %w", err) diff --git a/engine/access/rest/websockets/data_providers/block_digests_provider.go b/engine/access/rest/websockets/data_providers/block_digests_provider.go index 123ad6ac452..af2ae6f2e86 100644 --- a/engine/access/rest/websockets/data_providers/block_digests_provider.go +++ b/engine/access/rest/websockets/data_providers/block_digests_provider.go @@ -31,7 +31,7 @@ func NewBlockDigestsDataProvider( subscriptionID string, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, ) (*BlockDigestsDataProvider, error) { args, err := parseBlocksArguments(rawArguments) if err != nil { @@ -69,7 +69,7 @@ func (p *BlockDigestsDataProvider) Run() error { func (p *BlockDigestsDataProvider) createAndStartSubscription( ctx context.Context, args blocksArguments, -) subscription.Subscription { +) subscription.Subscription[*flow.BlockDigest] { if args.StartBlockID != flow.ZeroID { return p.api.SubscribeBlockDigestsFromStartBlockID(ctx, args.StartBlockID, args.BlockStatus) } diff --git a/engine/access/rest/websockets/data_providers/block_digests_provider_test.go b/engine/access/rest/websockets/data_providers/block_digests_provider_test.go index 7f75d7244f9..6ba879722de 100644 --- a/engine/access/rest/websockets/data_providers/block_digests_provider_test.go +++ b/engine/access/rest/websockets/data_providers/block_digests_provider_test.go @@ -34,14 +34,15 @@ func (s *BlockDigestsProviderSuite) SetupTest() { // validates that block digests are correctly streamed to the channel and ensures // no unexpected errors occur. func (s *BlockDigestsProviderSuite) TestBlockDigestsDataProvider_HappyPath() { - testHappyPath( + testHappyPath[*flow.BlockDigest, any]( s.T(), BlockDigestsTopic, s.factory, s.validBlockDigestsArgumentsTestCases(), - func(dataChan chan interface{}) { + func(dataChan chan *flow.BlockDigest) { for _, block := range s.blocks { - dataChan <- flow.NewBlockDigest(block.ID(), block.Height, time.UnixMilli(int64(block.Timestamp)).UTC()) + bd := flow.NewBlockDigest(block.ID(), block.Height, time.UnixMilli(int64(block.Timestamp)).UTC()) + dataChan <- bd } }, s.requireBlockDigest, @@ -50,8 +51,8 @@ func (s *BlockDigestsProviderSuite) TestBlockDigestsDataProvider_HappyPath() { // validBlockDigestsArgumentsTestCases defines test happy cases for block digests data providers. // Each test case specifies input arguments, and setup functions for the mock API used in the test. -func (s *BlockDigestsProviderSuite) validBlockDigestsArgumentsTestCases() []testType { - expectedResponses := make([]interface{}, len(s.blocks)) +func (s *BlockDigestsProviderSuite) validBlockDigestsArgumentsTestCases() []testType[*flow.BlockDigest, any] { + expectedResponses := make([]any, len(s.blocks)) for i, b := range s.blocks { blockDigest := flow.NewBlockDigest(b.ID(), b.Height, time.UnixMilli(int64(b.Timestamp)).UTC()) blockDigestPayload := models.NewBlockDigest(blockDigest) @@ -61,14 +62,14 @@ func (s *BlockDigestsProviderSuite) validBlockDigestsArgumentsTestCases() []test } } - return []testType{ + return []testType[*flow.BlockDigest, any]{ { name: "happy path with start_block_id argument", arguments: wsmodels.Arguments{ "start_block_id": s.rootBlock.ID().String(), "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.BlockDigest]) { s.api.On( "SubscribeBlockDigestsFromStartBlockID", mock.Anything, @@ -84,7 +85,7 @@ func (s *BlockDigestsProviderSuite) validBlockDigestsArgumentsTestCases() []test "start_block_height": strconv.FormatUint(s.rootBlock.Height, 10), "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.BlockDigest]) { s.api.On( "SubscribeBlockDigestsFromStartHeight", mock.Anything, @@ -99,7 +100,7 @@ func (s *BlockDigestsProviderSuite) validBlockDigestsArgumentsTestCases() []test arguments: wsmodels.Arguments{ "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.BlockDigest]) { s.api.On( "SubscribeBlockDigestsFromLatest", mock.Anything, @@ -112,7 +113,7 @@ func (s *BlockDigestsProviderSuite) validBlockDigestsArgumentsTestCases() []test } // requireBlockDigest ensures that the received block header information matches the expected data. -func (s *BlocksProviderSuite) requireBlockDigest(actual interface{}, expected interface{}) { +func (s *BlocksProviderSuite) requireBlockDigest(actual any, expected any) { expectedResponse, expectedResponsePayload := extractPayload[*models.BlockDigest](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*models.BlockDigest](s.T(), actual) @@ -124,7 +125,7 @@ func (s *BlocksProviderSuite) requireBlockDigest(actual interface{}, expected in // when invalid arguments are provided. It verifies that appropriate errors are returned // for missing or conflicting arguments. func (s *BlockDigestsProviderSuite) TestBlockDigestsDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) topic := BlockDigestsTopic diff --git a/engine/access/rest/websockets/data_providers/block_headers_provider.go b/engine/access/rest/websockets/data_providers/block_headers_provider.go index 8e5eb4159fb..2c8c411dec4 100644 --- a/engine/access/rest/websockets/data_providers/block_headers_provider.go +++ b/engine/access/rest/websockets/data_providers/block_headers_provider.go @@ -32,7 +32,7 @@ func NewBlockHeadersDataProvider( subscriptionID string, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, ) (*BlockHeadersDataProvider, error) { args, err := parseBlocksArguments(rawArguments) if err != nil { @@ -70,7 +70,7 @@ func (p *BlockHeadersDataProvider) Run() error { func (p *BlockHeadersDataProvider) createAndStartSubscription( ctx context.Context, args blocksArguments, -) subscription.Subscription { +) subscription.Subscription[*flow.Header] { if args.StartBlockID != flow.ZeroID { return p.api.SubscribeBlockHeadersFromStartBlockID(ctx, args.StartBlockID, args.BlockStatus) } diff --git a/engine/access/rest/websockets/data_providers/block_headers_provider_test.go b/engine/access/rest/websockets/data_providers/block_headers_provider_test.go index b834ebaf609..2e13b8c7d44 100644 --- a/engine/access/rest/websockets/data_providers/block_headers_provider_test.go +++ b/engine/access/rest/websockets/data_providers/block_headers_provider_test.go @@ -34,12 +34,12 @@ func (s *BlockHeadersProviderSuite) SetupTest() { // validates that block headers are correctly streamed to the channel and ensures // no unexpected errors occur. func (s *BlockHeadersProviderSuite) TestBlockHeadersDataProvider_HappyPath() { - testHappyPath( + testHappyPath[*flow.Header, any]( s.T(), BlockHeadersTopic, s.factory, s.validBlockHeadersArgumentsTestCases(), - func(dataChan chan interface{}) { + func(dataChan chan *flow.Header) { for _, block := range s.blocks { dataChan <- block.ToHeader() } @@ -50,8 +50,8 @@ func (s *BlockHeadersProviderSuite) TestBlockHeadersDataProvider_HappyPath() { // validBlockHeadersArgumentsTestCases defines test happy cases for block headers data providers. // Each test case specifies input arguments, and setup functions for the mock API used in the test. -func (s *BlockHeadersProviderSuite) validBlockHeadersArgumentsTestCases() []testType { - expectedResponses := make([]interface{}, len(s.blocks)) +func (s *BlockHeadersProviderSuite) validBlockHeadersArgumentsTestCases() []testType[*flow.Header, any] { + expectedResponses := make([]any, len(s.blocks)) for i, b := range s.blocks { var header commonmodels.BlockHeader header.Build(b.ToHeader()) @@ -62,14 +62,14 @@ func (s *BlockHeadersProviderSuite) validBlockHeadersArgumentsTestCases() []test } } - return []testType{ + return []testType[*flow.Header, any]{ { name: "happy path with start_block_id argument", arguments: wsmodels.Arguments{ "start_block_id": s.rootBlock.ID().String(), "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Header]) { s.api.On( "SubscribeBlockHeadersFromStartBlockID", mock.Anything, @@ -85,7 +85,7 @@ func (s *BlockHeadersProviderSuite) validBlockHeadersArgumentsTestCases() []test "start_block_height": strconv.FormatUint(s.rootBlock.Height, 10), "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Header]) { s.api.On( "SubscribeBlockHeadersFromStartHeight", mock.Anything, @@ -100,7 +100,7 @@ func (s *BlockHeadersProviderSuite) validBlockHeadersArgumentsTestCases() []test arguments: wsmodels.Arguments{ "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Header]) { s.api.On( "SubscribeBlockHeadersFromLatest", mock.Anything, @@ -113,7 +113,7 @@ func (s *BlockHeadersProviderSuite) validBlockHeadersArgumentsTestCases() []test } // requireBlockHeaders ensures that the received block header information matches the expected data. -func (s *BlockHeadersProviderSuite) requireBlockHeader(actual interface{}, expected interface{}) { +func (s *BlockHeadersProviderSuite) requireBlockHeader(actual any, expected any) { expectedResponse, expectedResponsePayload := extractPayload[*commonmodels.BlockHeader](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*commonmodels.BlockHeader](s.T(), actual) @@ -125,7 +125,7 @@ func (s *BlockHeadersProviderSuite) requireBlockHeader(actual interface{}, expec // when invalid arguments are provided. It verifies that appropriate errors are returned // for missing or conflicting arguments. func (s *BlockHeadersProviderSuite) TestBlockHeadersDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) topic := BlockHeadersTopic for _, test := range s.invalidArgumentsTestCases() { diff --git a/engine/access/rest/websockets/data_providers/blocks_provider.go b/engine/access/rest/websockets/data_providers/blocks_provider.go index 4ae85022899..83e748ec279 100644 --- a/engine/access/rest/websockets/data_providers/blocks_provider.go +++ b/engine/access/rest/websockets/data_providers/blocks_provider.go @@ -43,7 +43,7 @@ func NewBlocksDataProvider( linkGenerator commonmodels.LinkGenerator, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, ) (*BlocksDataProvider, error) { args, err := parseBlocksArguments(rawArguments) if err != nil { @@ -82,7 +82,7 @@ func (p *BlocksDataProvider) Run() error { func (p *BlocksDataProvider) createAndStartSubscription( ctx context.Context, args blocksArguments, -) subscription.Subscription { +) subscription.Subscription[*flow.Block] { if args.StartBlockID != flow.ZeroID { return p.api.SubscribeBlocksFromStartBlockID(ctx, args.StartBlockID, args.BlockStatus) } diff --git a/engine/access/rest/websockets/data_providers/blocks_provider_test.go b/engine/access/rest/websockets/data_providers/blocks_provider_test.go index 5b51613677c..5d805dd685f 100644 --- a/engine/access/rest/websockets/data_providers/blocks_provider_test.go +++ b/engine/access/rest/websockets/data_providers/blocks_provider_test.go @@ -98,12 +98,12 @@ func (s *BlocksProviderSuite) TestBlocksDataProvider_HappyPath() { }, ) - testHappyPath( + testHappyPath[*flow.Block, any]( s.T(), BlocksTopic, s.factory, s.validBlockArgumentsTestCases(), - func(dataChan chan interface{}) { + func(dataChan chan *flow.Block) { for _, block := range s.blocks { dataChan <- block } @@ -114,17 +114,17 @@ func (s *BlocksProviderSuite) TestBlocksDataProvider_HappyPath() { // validBlockArgumentsTestCases defines test happy cases for block data providers. // Each test case specifies input arguments, and setup functions for the mock API used in the test. -func (s *BlocksProviderSuite) validBlockArgumentsTestCases() []testType { +func (s *BlocksProviderSuite) validBlockArgumentsTestCases() []testType[*flow.Block, any] { expectedResponses := s.expectedBlockResponses(s.blocks, map[string]bool{commonmodels.ExpandableFieldPayload: true}, flow.BlockStatusFinalized) - return []testType{ + return []testType[*flow.Block, any]{ { name: "happy path with start_block_id argument", arguments: wsmodels.Arguments{ "start_block_id": s.rootBlock.ID().String(), "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Block]) { s.api.On( "SubscribeBlocksFromStartBlockID", mock.Anything, @@ -140,7 +140,7 @@ func (s *BlocksProviderSuite) validBlockArgumentsTestCases() []testType { "start_block_height": strconv.FormatUint(s.rootBlock.Height, 10), "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Block]) { s.api.On( "SubscribeBlocksFromStartHeight", mock.Anything, @@ -155,7 +155,7 @@ func (s *BlocksProviderSuite) validBlockArgumentsTestCases() []testType { arguments: wsmodels.Arguments{ "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Block]) { s.api.On( "SubscribeBlocksFromLatest", mock.Anything, @@ -169,7 +169,7 @@ func (s *BlocksProviderSuite) validBlockArgumentsTestCases() []testType { arguments: wsmodels.Arguments{ "block_status": parser.Finalized, }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*flow.Block]) { s.api.On( "SubscribeBlocksFromLatest", mock.Anything, @@ -182,7 +182,7 @@ func (s *BlocksProviderSuite) validBlockArgumentsTestCases() []testType { } // requireBlock ensures that the received block information matches the expected data. -func (s *BlocksProviderSuite) requireBlock(actual interface{}, expected interface{}) { +func (s *BlocksProviderSuite) requireBlock(actual any, expected any) { expectedResponse, expectedResponsePayload := extractPayload[*commonmodels.Block](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*commonmodels.Block](s.T(), actual) @@ -195,8 +195,8 @@ func (s *BlocksProviderSuite) expectedBlockResponses( blocks []*flow.Block, expand map[string]bool, status flow.BlockStatus, -) []interface{} { - responses := make([]interface{}, len(blocks)) +) []any { + responses := make([]any, len(blocks)) for i, b := range blocks { var block commonmodels.Block err := block.Build(b, nil, s.linkGenerator, status, expand) @@ -215,7 +215,7 @@ func (s *BlocksProviderSuite) expectedBlockResponses( // when invalid arguments are provided. It verifies that appropriate errors are returned // for missing or conflicting arguments. func (s *BlocksProviderSuite) TestBlocksDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) for _, test := range s.invalidArgumentsTestCases() { s.Run(test.name, func() { @@ -257,7 +257,7 @@ func (s *BlocksProviderSuite) invalidArgumentsTestCases() []testErrType { }, { name: "unexpected argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "block_status": parser.Finalized, "start_block_id": unittest.BlockFixture().ID().String(), "unexpected_argument": "dummy", diff --git a/engine/access/rest/websockets/data_providers/events_provider.go b/engine/access/rest/websockets/data_providers/events_provider.go index 7b2f933c285..9e08f5f1120 100644 --- a/engine/access/rest/websockets/data_providers/events_provider.go +++ b/engine/access/rest/websockets/data_providers/events_provider.go @@ -10,7 +10,6 @@ import ( "github.com/onflow/flow-go/engine/access/rest/websockets/data_providers/models" wsmodels "github.com/onflow/flow-go/engine/access/rest/websockets/models" "github.com/onflow/flow-go/engine/access/state_stream" - "github.com/onflow/flow-go/engine/access/state_stream/backend" "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/model/flow" @@ -45,7 +44,7 @@ func NewEventsDataProvider( subscriptionID string, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, chain flow.Chain, eventFilterConfig state_stream.EventFilterConfig, defaultHeartbeatInterval uint64, @@ -94,7 +93,7 @@ func (p *EventsDataProvider) Run() error { // This function is not expected to be called concurrently. // // No errors expected during normal operations. -func (p *EventsDataProvider) handleResponse(response *backend.EventsResponse) error { +func (p *EventsDataProvider) handleResponse(response *state_stream.EventsResponse) error { // convert events to JSON-CDC format convertedResponse, err := convertEventsResponse(response) if err != nil { @@ -108,7 +107,7 @@ func (p *EventsDataProvider) handleResponse(response *backend.EventsResponse) er // This function is not expected to be called concurrently. // // No errors are expected during normal operations. -func (p *EventsDataProvider) sendResponse(eventsResponse *backend.EventsResponse) error { +func (p *EventsDataProvider) sendResponse(eventsResponse *state_stream.EventsResponse) error { // Only send a response if there's meaningful data to send // or the heartbeat interval limit is reached p.blocksSinceLastMessage += 1 @@ -133,7 +132,10 @@ func (p *EventsDataProvider) sendResponse(eventsResponse *backend.EventsResponse } // createAndStartSubscription creates a new subscription using the specified input arguments. -func (p *EventsDataProvider) createAndStartSubscription(ctx context.Context, args eventsArguments) subscription.Subscription { +func (p *EventsDataProvider) createAndStartSubscription( + ctx context.Context, + args eventsArguments, +) subscription.Subscription[*state_stream.EventsResponse] { if args.StartBlockID != flow.ZeroID { return p.stateStreamApi.SubscribeEventsFromStartBlockID(ctx, args.StartBlockID, args.Filter) } @@ -148,13 +150,13 @@ func (p *EventsDataProvider) createAndStartSubscription(ctx context.Context, arg // convertEventsResponse converts events in the provided EventsResponse from CCF to JSON-CDC format. // // No errors expected during normal operations. -func convertEventsResponse(resp *backend.EventsResponse) (*backend.EventsResponse, error) { +func convertEventsResponse(resp *state_stream.EventsResponse) (*state_stream.EventsResponse, error) { jsoncdcEvents, err := convertEvents(resp.Events) if err != nil { return nil, fmt.Errorf("failed to convert events to JSON-CDC: %w", err) } - return &backend.EventsResponse{ + return &state_stream.EventsResponse{ BlockID: resp.BlockID, Height: resp.Height, BlockTimestamp: resp.BlockTimestamp, diff --git a/engine/access/rest/websockets/data_providers/events_provider_test.go b/engine/access/rest/websockets/data_providers/events_provider_test.go index 88ef6a67c13..19415cecd1c 100644 --- a/engine/access/rest/websockets/data_providers/events_provider_test.go +++ b/engine/access/rest/websockets/data_providers/events_provider_test.go @@ -15,7 +15,6 @@ import ( "github.com/onflow/flow-go/engine/access/rest/websockets/data_providers/models" wsmodels "github.com/onflow/flow-go/engine/access/rest/websockets/models" "github.com/onflow/flow-go/engine/access/state_stream" - "github.com/onflow/flow-go/engine/access/state_stream/backend" ssmock "github.com/onflow/flow-go/engine/access/state_stream/mock" "github.com/onflow/flow-go/engine/access/subscription" submock "github.com/onflow/flow-go/engine/access/subscription/mock" @@ -72,12 +71,12 @@ func (s *EventsProviderSuite) TestEventsDataProvider_HappyPath() { backendResponses := s.backendEventsResponses(events) - testHappyPath( + testHappyPath[*state_stream.EventsResponse, any]( s.T(), EventsTopic, s.factory, s.subscribeEventsDataProviderTestCases(backendResponses), - func(dataChan chan interface{}) { + func(dataChan chan *state_stream.EventsResponse) { for i := 0; i < len(backendResponses); i++ { dataChan <- backendResponses[i] } @@ -87,10 +86,10 @@ func (s *EventsProviderSuite) TestEventsDataProvider_HappyPath() { } // subscribeEventsDataProviderTestCases generates test cases for events data providers. -func (s *EventsProviderSuite) subscribeEventsDataProviderTestCases(backendResponses []*backend.EventsResponse) []testType { +func (s *EventsProviderSuite) subscribeEventsDataProviderTestCases(backendResponses []*state_stream.EventsResponse) []testType[*state_stream.EventsResponse, any] { expectedResponses := s.expectedEventsResponses(backendResponses) - return []testType{ + return []testType[*state_stream.EventsResponse, any]{ { name: "SubscribeBlocksFromStartBlockID happy path", arguments: wsmodels.Arguments{ @@ -100,7 +99,7 @@ func (s *EventsProviderSuite) subscribeEventsDataProviderTestCases(backendRespon "contracts": []string{"A.0000000000000001.Contract1", "A.0000000000000001.Contract2"}, "heartbeat_interval": "3", }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*state_stream.EventsResponse]) { s.api.On( "SubscribeEventsFromStartBlockID", mock.Anything, @@ -119,7 +118,7 @@ func (s *EventsProviderSuite) subscribeEventsDataProviderTestCases(backendRespon "contracts": []string{"A.0000000000000001.Contract1", "A.0000000000000001.Contract2"}, "heartbeat_interval": "3", }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*state_stream.EventsResponse]) { s.api.On( "SubscribeEventsFromStartHeight", mock.Anything, @@ -137,7 +136,7 @@ func (s *EventsProviderSuite) subscribeEventsDataProviderTestCases(backendRespon "contracts": []string{"A.0000000000000001.Contract1", "A.0000000000000001.Contract2"}, "heartbeat_interval": "3", }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[*state_stream.EventsResponse]) { s.api.On( "SubscribeEventsFromLatest", mock.Anything, @@ -150,7 +149,7 @@ func (s *EventsProviderSuite) subscribeEventsDataProviderTestCases(backendRespon } // requireEvents ensures that the received event information matches the expected data. -func (s *EventsProviderSuite) requireEvents(actual interface{}, expected interface{}) { +func (s *EventsProviderSuite) requireEvents(actual any, expected any) { expectedResponse, expectedResponsePayload := extractPayload[*models.EventResponse](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*models.EventResponse](s.T(), actual) @@ -160,11 +159,11 @@ func (s *EventsProviderSuite) requireEvents(actual interface{}, expected interfa } // backendEventsResponses creates backend events responses based on the provided events. -func (s *EventsProviderSuite) backendEventsResponses(events []flow.Event) []*backend.EventsResponse { - responses := make([]*backend.EventsResponse, len(events)) +func (s *EventsProviderSuite) backendEventsResponses(events []flow.Event) []*state_stream.EventsResponse { + responses := make([]*state_stream.EventsResponse, len(events)) for i := range events { - responses[i] = &backend.EventsResponse{ + responses[i] = &state_stream.EventsResponse{ Height: s.rootBlock.Height, BlockID: s.rootBlock.ID(), Events: events, @@ -177,13 +176,13 @@ func (s *EventsProviderSuite) backendEventsResponses(events []flow.Event) []*bac // expectedEventsResponses creates the expected responses for the provided backend responses. func (s *EventsProviderSuite) expectedEventsResponses( - backendResponses []*backend.EventsResponse, -) []interface{} { - expectedResponses := make([]interface{}, len(backendResponses)) + backendResponses []*state_stream.EventsResponse, +) []any { + expectedResponses := make([]any, len(backendResponses)) for i, resp := range backendResponses { // avoid updating the original response - expected := &backend.EventsResponse{ + expected := &state_stream.EventsResponse{ Height: resp.Height, BlockID: resp.BlockID, BlockTimestamp: resp.BlockTimestamp, @@ -209,22 +208,22 @@ func (s *EventsProviderSuite) expectedEventsResponses( // TestMessageIndexEventProviderResponse_HappyPath tests that MessageIndex values in response are strictly increasing. func (s *EventsProviderSuite) TestMessageIndexEventProviderResponse_HappyPath() { - send := make(chan interface{}, 10) + send := make(chan any, 10) topic := EventsTopic eventsCount := 4 // Create a channel to simulate the subscription's event channel - eventChan := make(chan interface{}) + eventChan := make(chan *state_stream.EventsResponse) // Create a mock subscription and mock the channel - sub := submock.NewSubscription(s.T()) - sub.On("Channel").Return((<-chan interface{})(eventChan)) + sub := submock.NewSubscription[*state_stream.EventsResponse](s.T()) + sub.On("Channel").Return((<-chan *state_stream.EventsResponse)(eventChan)) sub.On("Err").Return(nil).Once() s.api.On("SubscribeEventsFromStartBlockID", mock.Anything, mock.Anything, mock.Anything).Return(sub) arguments := - map[string]interface{}{ + map[string]any{ "start_block_id": s.rootBlock.ID().String(), "event_types": []string{state_stream.CoreEventAccountCreated}, "addresses": []string{unittest.AddressFixture().String()}, @@ -264,7 +263,7 @@ func (s *EventsProviderSuite) TestMessageIndexEventProviderResponse_HappyPath() defer close(eventChan) // Close the channel when done for i := 0; i < eventsCount; i++ { - eventChan <- &backend.EventsResponse{ + eventChan <- &state_stream.EventsResponse{ Height: s.rootBlock.Height, } } @@ -302,7 +301,7 @@ func (s *EventsProviderSuite) TestMessageIndexEventProviderResponse_HappyPath() // 2. Invalid 'start_block_id' argument. // 3. Invalid 'start_block_height' argument. func (s *EventsProviderSuite) TestEventsDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) topic := EventsTopic for _, test := range invalidEventsArgumentsTestCases() { @@ -327,7 +326,7 @@ func (s *EventsProviderSuite) TestEventsDataProvider_InvalidArguments() { } func (s *EventsProviderSuite) TestEventsDataProvider_StateStreamNotConfigured() { - send := make(chan interface{}) + send := make(chan any) topic := EventsTopic provider, err := NewEventsDataProvider( @@ -365,7 +364,7 @@ func invalidEventsArgumentsTestCases() []testErrType { }, { name: "invalid 'start_block_id' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_id": "invalid_block_id", "event_types": []string{state_stream.CoreEventAccountCreated}, "addresses": []string{unittest.AddressFixture().String()}, @@ -375,7 +374,7 @@ func invalidEventsArgumentsTestCases() []testErrType { }, { name: "invalid 'start_block_height' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_height": "-1", "event_types": []string{state_stream.CoreEventAccountCreated}, "addresses": []string{unittest.AddressFixture().String()}, @@ -385,7 +384,7 @@ func invalidEventsArgumentsTestCases() []testErrType { }, { name: "invalid 'heartbeat_interval' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_id": unittest.BlockFixture().ID().String(), "event_types": []string{state_stream.CoreEventAccountCreated}, "addresses": []string{unittest.AddressFixture().String()}, @@ -396,7 +395,7 @@ func invalidEventsArgumentsTestCases() []testErrType { }, { name: "unexpected argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "start_block_id": unittest.BlockFixture().ID().String(), "event_types": []string{state_stream.CoreEventAccountCreated}, "addresses": []string{unittest.AddressFixture().String()}, diff --git a/engine/access/rest/websockets/data_providers/factory.go b/engine/access/rest/websockets/data_providers/factory.go index e612549611a..d654706ec55 100644 --- a/engine/access/rest/websockets/data_providers/factory.go +++ b/engine/access/rest/websockets/data_providers/factory.go @@ -33,7 +33,7 @@ type DataProviderFactory interface { // and configuration parameters. // // No errors are expected during normal operations. - NewDataProvider(ctx context.Context, subID string, topic string, args wsmodels.Arguments, stream chan<- interface{}) (DataProvider, error) + NewDataProvider(ctx context.Context, subID string, topic string, args wsmodels.Arguments, stream chan<- any) (DataProvider, error) } var _ DataProviderFactory = (*DataProviderFactoryImpl)(nil) @@ -91,7 +91,7 @@ func NewDataProviderFactory( // - ch: Channel to which the data provider sends data. // // No errors are expected during normal operations. -func (s *DataProviderFactoryImpl) NewDataProvider(ctx context.Context, subscriptionID string, topic string, arguments wsmodels.Arguments, ch chan<- interface{}) (DataProvider, error) { +func (s *DataProviderFactoryImpl) NewDataProvider(ctx context.Context, subscriptionID string, topic string, arguments wsmodels.Arguments, ch chan<- any) (DataProvider, error) { switch topic { case BlocksTopic: return NewBlocksDataProvider(ctx, s.logger, s.accessApi, subscriptionID, s.linkGenerator, topic, arguments, ch) diff --git a/engine/access/rest/websockets/data_providers/factory_test.go b/engine/access/rest/websockets/data_providers/factory_test.go index dd5ed485179..4e609569d75 100644 --- a/engine/access/rest/websockets/data_providers/factory_test.go +++ b/engine/access/rest/websockets/data_providers/factory_test.go @@ -5,7 +5,6 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" accessmock "github.com/onflow/flow-go/access/mock" @@ -14,7 +13,6 @@ import ( "github.com/onflow/flow-go/engine/access/state_stream" ssmock "github.com/onflow/flow-go/engine/access/state_stream/mock" "github.com/onflow/flow-go/engine/access/subscription" - submock "github.com/onflow/flow-go/engine/access/subscription/mock" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/utils/unittest" ) @@ -24,7 +22,7 @@ type DataProviderFactorySuite struct { suite.Suite ctx context.Context - ch chan interface{} + ch chan any accessApi *accessmock.API stateStreamApi *ssmock.API @@ -44,7 +42,7 @@ func (s *DataProviderFactorySuite) SetupTest() { s.accessApi = accessmock.NewAPI(s.T()) s.ctx = context.Background() - s.ch = make(chan interface{}) + s.ch = make(chan any) s.factory = NewDataProviderFactory( log, @@ -58,13 +56,6 @@ func (s *DataProviderFactorySuite) SetupTest() { s.Require().NotNil(s.factory) } -// setupSubscription creates a mock subscription instance for testing purposes. -// It configures the return value of the specified API call to the mock subscription. -func (s *DataProviderFactorySuite) setupSubscription(apiCall *mock.Call) { - sub := submock.NewSubscription(s.T()) - apiCall.Return(sub).Once() -} - // TestSupportedTopics verifies that supported topics return a valid provider and no errors. // Each test case includes a topic and arguments for which a data provider should be created. func (s *DataProviderFactorySuite) TestSupportedTopics() { diff --git a/engine/access/rest/websockets/data_providers/mock/data_provider_factory.go b/engine/access/rest/websockets/data_providers/mock/data_provider_factory.go index 61be02fc1b0..0710fbc2d93 100644 --- a/engine/access/rest/websockets/data_providers/mock/data_provider_factory.go +++ b/engine/access/rest/websockets/data_providers/mock/data_provider_factory.go @@ -17,7 +17,7 @@ type DataProviderFactory struct { } // NewDataProvider provides a mock function with given fields: ctx, subID, topic, args, stream -func (_m *DataProviderFactory) NewDataProvider(ctx context.Context, subID string, topic string, args models.Arguments, stream chan<- interface{}) (data_providers.DataProvider, error) { +func (_m *DataProviderFactory) NewDataProvider(ctx context.Context, subID string, topic string, args models.Arguments, stream chan<- any) (data_providers.DataProvider, error) { ret := _m.Called(ctx, subID, topic, args, stream) if len(ret) == 0 { @@ -26,10 +26,10 @@ func (_m *DataProviderFactory) NewDataProvider(ctx context.Context, subID string var r0 data_providers.DataProvider var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, models.Arguments, chan<- interface{}) (data_providers.DataProvider, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string, models.Arguments, chan<- any) (data_providers.DataProvider, error)); ok { return rf(ctx, subID, topic, args, stream) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, models.Arguments, chan<- interface{}) data_providers.DataProvider); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, string, models.Arguments, chan<- any) data_providers.DataProvider); ok { r0 = rf(ctx, subID, topic, args, stream) } else { if ret.Get(0) != nil { @@ -37,7 +37,7 @@ func (_m *DataProviderFactory) NewDataProvider(ctx context.Context, subID string } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, models.Arguments, chan<- interface{}) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, string, string, models.Arguments, chan<- any) error); ok { r1 = rf(ctx, subID, topic, args, stream) } else { r1 = ret.Error(1) diff --git a/engine/access/rest/websockets/data_providers/models/account_statuses.go b/engine/access/rest/websockets/data_providers/models/account_statuses.go index b3373e8fc5f..0674d9f855e 100644 --- a/engine/access/rest/websockets/data_providers/models/account_statuses.go +++ b/engine/access/rest/websockets/data_providers/models/account_statuses.go @@ -3,7 +3,7 @@ package models import ( "strconv" - "github.com/onflow/flow-go/engine/access/state_stream/backend" + "github.com/onflow/flow-go/engine/access/state_stream" ) // AccountStatusesResponse is the response message for 'events' topic. @@ -14,7 +14,7 @@ type AccountStatusesResponse struct { MessageIndex uint64 `json:"message_index"` } -func NewAccountStatusesResponse(accountStatusesResponse *backend.AccountStatusesResponse, index uint64) *AccountStatusesResponse { +func NewAccountStatusesResponse(accountStatusesResponse *state_stream.AccountStatusesResponse, index uint64) *AccountStatusesResponse { accountEvents := NewAccountEvents(accountStatusesResponse.AccountEvents) return &AccountStatusesResponse{ diff --git a/engine/access/rest/websockets/data_providers/models/base_data_provider.go b/engine/access/rest/websockets/data_providers/models/base_data_provider.go index 31fd72a7380..8b7d3c507ae 100644 --- a/engine/access/rest/websockets/data_providers/models/base_data_provider.go +++ b/engine/access/rest/websockets/data_providers/models/base_data_provider.go @@ -2,7 +2,7 @@ package models // BaseDataProvidersResponse represents a base structure for responses from subscriptions. type BaseDataProvidersResponse struct { - SubscriptionID string `json:"subscription_id"` // Unique subscriptionID - Topic string `json:"topic"` // Topic of the subscription - Payload interface{} `json:"payload"` // Payload that's being returned within a subscription. + SubscriptionID string `json:"subscription_id"` // Unique subscriptionID + Topic string `json:"topic"` // Topic of the subscription + Payload any `json:"payload"` // Payload that's being returned within a subscription. } diff --git a/engine/access/rest/websockets/data_providers/models/event.go b/engine/access/rest/websockets/data_providers/models/event.go index 99c605972d5..1454649f393 100644 --- a/engine/access/rest/websockets/data_providers/models/event.go +++ b/engine/access/rest/websockets/data_providers/models/event.go @@ -5,7 +5,7 @@ import ( "github.com/onflow/flow-go/engine/access/rest/common/models" commonmodels "github.com/onflow/flow-go/engine/access/rest/common/models" - "github.com/onflow/flow-go/engine/access/state_stream/backend" + "github.com/onflow/flow-go/engine/access/state_stream" ) // EventResponse is the response message for 'events' topic. @@ -15,7 +15,7 @@ type EventResponse struct { } // NewEventResponse creates EventResponse instance. -func NewEventResponse(eventsResponse *backend.EventsResponse, index uint64) *EventResponse { +func NewEventResponse(eventsResponse *state_stream.EventsResponse, index uint64) *EventResponse { return &EventResponse{ BlockEvents: commonmodels.BlockEvents{ BlockId: eventsResponse.BlockID.String(), diff --git a/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider.go b/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider.go index 9ae1839c01c..e1548c5fdc2 100644 --- a/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider.go +++ b/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider.go @@ -44,7 +44,7 @@ func NewSendAndGetTransactionStatusesDataProvider( linkGenerator commonmodels.LinkGenerator, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, chain flow.Chain, ) (*SendAndGetTransactionStatusesDataProvider, error) { args, err := parseSendAndGetTransactionStatusesArguments(rawArguments, chain) @@ -105,7 +105,7 @@ func (p *SendAndGetTransactionStatusesDataProvider) sendResponse(txResults []*ac func (p *SendAndGetTransactionStatusesDataProvider) createAndStartSubscription( ctx context.Context, args sendAndGetTransactionStatusesArguments, -) subscription.Subscription { +) subscription.Subscription[[]*accessmodel.TransactionResult] { return p.api.SendAndSubscribeTransactionStatuses(ctx, &args.Transaction, entities.EventEncodingVersion_JSON_CDC_V0) } diff --git a/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider_test.go b/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider_test.go index 3e17f49bed3..b8b493f16dd 100644 --- a/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider_test.go +++ b/engine/access/rest/websockets/data_providers/send_and_get_transaction_statuses_provider_test.go @@ -15,6 +15,7 @@ import ( "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" submock "github.com/onflow/flow-go/engine/access/subscription/mock" + accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/utils/unittest" @@ -80,11 +81,11 @@ func (s *TransactionStatusesProviderSuite) TestSendTransactionStatusesDataProvid backendResponse := backendTransactionStatusesResponse(s.rootBlock) expectedResponse := s.expectedTransactionStatusesResponses(backendResponse, SendAndGetTransactionStatusesTopic) - sendTxStatutesTestCases := []testType{ + sendTxStatutesTestCases := []testType[[]*accessmodel.TransactionResult, any]{ { name: "SubscribeTransactionStatusesFromStartBlockID happy path", arguments: unittest.CreateSendTxHttpPayload(tx), - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[[]*accessmodel.TransactionResult]) { s.api.On( "SendAndSubscribeTransactionStatuses", mock.Anything, @@ -96,12 +97,12 @@ func (s *TransactionStatusesProviderSuite) TestSendTransactionStatusesDataProvid }, } - testHappyPath( + testHappyPath[[]*accessmodel.TransactionResult, any]( s.T(), SendAndGetTransactionStatusesTopic, s.factory, sendTxStatutesTestCases, - func(dataChan chan interface{}) { + func(dataChan chan []*accessmodel.TransactionResult) { dataChan <- backendResponse }, s.requireTransactionStatuses, @@ -110,8 +111,8 @@ func (s *TransactionStatusesProviderSuite) TestSendTransactionStatusesDataProvid // requireTransactionStatuses ensures that the received transaction statuses information matches the expected data. func (s *SendTransactionStatusesProviderSuite) requireTransactionStatuses( - actual interface{}, - expected interface{}, + actual any, + expected any, ) { expectedResponse, expectedResponsePayload := extractPayload[*models.TransactionStatusesResponse](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*models.TransactionStatusesResponse](s.T(), actual) @@ -124,7 +125,7 @@ func (s *SendTransactionStatusesProviderSuite) requireTransactionStatuses( // when invalid arguments are provided. It verifies that appropriate errors are returned // for missing or conflicting arguments. func (s *SendTransactionStatusesProviderSuite) TestSendTransactionStatusesDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) topic := SendAndGetTransactionStatusesTopic for _, test := range invalidSendTransactionStatusesArgumentsTestCases() { @@ -154,84 +155,84 @@ func invalidSendTransactionStatusesArgumentsTestCases() []testErrType { return []testErrType{ { name: "invalid 'script' argument type", - arguments: map[string]interface{}{ + arguments: map[string]any{ "script": 0, }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'script' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "script": "invalid_script", }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'arguments' type", - arguments: map[string]interface{}{ + arguments: map[string]any{ "arguments": 0, }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'arguments' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "arguments": []string{"invalid_base64_1", "invalid_base64_2"}, }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'reference_block_id' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "reference_block_id": "invalid_reference_block_id", }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'gas_limit' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "gas_limit": "-1", }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'payer' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "payer": "invalid_payer", }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'proposal_key' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "proposal_key": "invalid ProposalKey object", }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'authorizers' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "authorizers": []string{"invalid_base64_1", "invalid_base64_2"}, }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'payload_signatures' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "payload_signatures": "invalid TransactionSignature array", }, expectedErrorMsg: "failed to parse transaction", }, { name: "invalid 'envelope_signatures' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "envelope_signatures": "invalid TransactionSignature array", }, expectedErrorMsg: "failed to parse transaction", }, { name: "unexpected argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "unexpected_argument": "dummy", }, expectedErrorMsg: "request body contains unknown field", diff --git a/engine/access/rest/websockets/data_providers/transaction_statuses_provider.go b/engine/access/rest/websockets/data_providers/transaction_statuses_provider.go index dbfe91c87dd..cf966058403 100644 --- a/engine/access/rest/websockets/data_providers/transaction_statuses_provider.go +++ b/engine/access/rest/websockets/data_providers/transaction_statuses_provider.go @@ -43,7 +43,7 @@ func NewTransactionStatusesDataProvider( linkGenerator commonmodels.LinkGenerator, topic string, rawArguments wsmodels.Arguments, - send chan<- interface{}, + send chan<- any, ) (*TransactionStatusesDataProvider, error) { args, err := parseTransactionStatusesArguments(rawArguments) if err != nil { @@ -102,7 +102,7 @@ func (p *TransactionStatusesDataProvider) sendResponse(txResults []*accessmodel. func (p *TransactionStatusesDataProvider) createAndStartSubscription( ctx context.Context, args transactionStatusesArguments, -) subscription.Subscription { +) subscription.Subscription[[]*accessmodel.TransactionResult] { return p.api.SubscribeTransactionStatuses(ctx, args.TxID, entities.EventEncodingVersion_JSON_CDC_V0) } diff --git a/engine/access/rest/websockets/data_providers/transaction_statuses_provider_test.go b/engine/access/rest/websockets/data_providers/transaction_statuses_provider_test.go index 8421a28eafa..4ffea782999 100644 --- a/engine/access/rest/websockets/data_providers/transaction_statuses_provider_test.go +++ b/engine/access/rest/websockets/data_providers/transaction_statuses_provider_test.go @@ -73,28 +73,28 @@ func (s *TransactionStatusesProviderSuite) TestTransactionStatusesDataProvider_H }, ) - testHappyPath( + testHappyPath[[]*accessmodel.TransactionResult, any]( s.T(), TransactionStatusesTopic, s.factory, s.subscribeTransactionStatusesDataProviderTestCases(backendResponse), - func(dataChan chan interface{}) { + func(dataChan chan []*accessmodel.TransactionResult) { dataChan <- backendResponse }, s.requireTransactionStatuses, ) } -func (s *TransactionStatusesProviderSuite) subscribeTransactionStatusesDataProviderTestCases(backendResponses []*accessmodel.TransactionResult) []testType { +func (s *TransactionStatusesProviderSuite) subscribeTransactionStatusesDataProviderTestCases(backendResponses []*accessmodel.TransactionResult) []testType[[]*accessmodel.TransactionResult, any] { expectedResponses := s.expectedTransactionStatusesResponses(backendResponses, TransactionStatusesTopic) - return []testType{ + return []testType[[]*accessmodel.TransactionResult, any]{ { name: "SubscribeTransactionStatuses happy path", arguments: wsmodels.Arguments{ "tx_id": unittest.IdentifierFixture().String(), }, - setupBackend: func(sub *submock.Subscription) { + setupBackend: func(sub *submock.Subscription[[]*accessmodel.TransactionResult]) { s.api.On( "SubscribeTransactionStatuses", mock.Anything, @@ -109,8 +109,8 @@ func (s *TransactionStatusesProviderSuite) subscribeTransactionStatusesDataProvi // requireTransactionStatuses ensures that the received transaction statuses information matches the expected data. func (s *TransactionStatusesProviderSuite) requireTransactionStatuses( - actual interface{}, - expected interface{}, + actual any, + expected any, ) { expectedResponse, expectedResponsePayload := extractPayload[*models.TransactionStatusesResponse](s.T(), expected) actualResponse, actualResponsePayload := extractPayload[*models.TransactionStatusesResponse](s.T(), actual) @@ -144,8 +144,8 @@ func backendTransactionStatusesResponse(block *flow.Block) []*accessmodel.Transa func (s *TransactionStatusesProviderSuite) expectedTransactionStatusesResponses( backendResponses []*accessmodel.TransactionResult, topic string, -) []interface{} { - expectedResponses := make([]interface{}, len(backendResponses)) +) []any { + expectedResponses := make([]any, len(backendResponses)) for i, resp := range backendResponses { expectedResponsePayload := models.NewTransactionStatusesResponse(s.linkGenerator, resp, uint64(i)) @@ -160,16 +160,16 @@ func (s *TransactionStatusesProviderSuite) expectedTransactionStatusesResponses( // TestMessageIndexTransactionStatusesProviderResponse_HappyPath tests that MessageIndex values in response are strictly increasing. func (s *TransactionStatusesProviderSuite) TestMessageIndexTransactionStatusesProviderResponse_HappyPath() { - send := make(chan interface{}, 10) + send := make(chan any, 10) topic := TransactionStatusesTopic txStatusesCount := 4 // Create a channel to simulate the subscription's account statuses channel - txStatusesChan := make(chan interface{}) + txStatusesChan := make(chan []*accessmodel.TransactionResult) // Create a mock subscription and mock the channel - sub := submock.NewSubscription(s.T()) - sub.On("Channel").Return((<-chan interface{})(txStatusesChan)) + sub := submock.NewSubscription[[]*accessmodel.TransactionResult](s.T()) + sub.On("Channel").Return((<-chan []*accessmodel.TransactionResult)(txStatusesChan)) sub.On("Err").Return(nil).Once() s.api.On( @@ -186,7 +186,7 @@ func (s *TransactionStatusesProviderSuite) TestMessageIndexTransactionStatusesPr ) arguments := - map[string]interface{}{ + map[string]any{ "tx_id": unittest.TransactionFixture().ID().String(), } @@ -255,7 +255,7 @@ func (s *TransactionStatusesProviderSuite) TestMessageIndexTransactionStatusesPr // when invalid arguments are provided. It verifies that appropriate errors are returned // for missing or conflicting arguments. func (s *TransactionStatusesProviderSuite) TestTransactionStatusesDataProvider_InvalidArguments() { - send := make(chan interface{}) + send := make(chan any) topic := TransactionStatusesTopic @@ -285,26 +285,26 @@ func invalidTransactionStatusesArgumentsTestCases() []testErrType { return []testErrType{ { name: "invalid 'tx_id' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "tx_id": "invalid_tx_id", }, expectedErrorMsg: "invalid ID format", }, { name: "empty 'tx_id' argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "tx_id": "", }, expectedErrorMsg: "'tx_id' must not be empty", }, { name: "missing 'tx_id' argument", - arguments: map[string]interface{}{}, + arguments: map[string]any{}, expectedErrorMsg: "missing 'tx_id' field", }, { name: "unexpected argument", - arguments: map[string]interface{}{ + arguments: map[string]any{ "unexpected_argument": "dummy", "tx_id": unittest.TransactionFixture().ID().String(), }, diff --git a/engine/access/rest/websockets/data_providers/unit_test.go b/engine/access/rest/websockets/data_providers/unit_test.go index de9abde3cba..7340f5a0efd 100644 --- a/engine/access/rest/websockets/data_providers/unit_test.go +++ b/engine/access/rest/websockets/data_providers/unit_test.go @@ -16,11 +16,13 @@ import ( ) // testType represents a valid test scenario for subscribing -type testType struct { +// In: type flowing through the subscription channel +// Out: type of the expected response produced by the provider (often any wrapping BaseDataProvidersResponse) +type testType[In any, Out any] struct { name string arguments wsmodels.Arguments - setupBackend func(sub *submock.Subscription) - expectedResponses []interface{} + setupBackend func(sub *submock.Subscription[In]) + expectedResponses []Out } // testErrType represents an error cases for subscribing @@ -42,29 +44,35 @@ type testErrType struct { // - tests: A slice of test cases to run, each specifying setup and validation logic. // - sendData: A function to simulate emitting data into the subscription's data channel. // - requireFn: A function to validate the output received in the send channel. -func testHappyPath( +func testHappyPath[In any, Out any]( t *testing.T, topic string, factory *DataProviderFactoryImpl, - tests []testType, - sendData func(chan interface{}), - requireFn func(interface{}, interface{}), + tests []testType[In, Out], + sendData func(chan In), + requireFn func(actual any, expected Out), ) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - send := make(chan interface{}, 10) + send := make(chan any, 10) // Create a channel to simulate the subscription's data channel - dataChan := make(chan interface{}) + dataChan := make(chan In) // Create a mock subscription and mock the channel - sub := submock.NewSubscription(t) - sub.On("Channel").Return((<-chan interface{})(dataChan)) + sub := submock.NewSubscription[In](t) + sub.On("Channel").Return((<-chan In)(dataChan)) sub.On("Err").Return(nil) test.setupBackend(sub) // Create the data provider instance - provider, err := factory.NewDataProvider(context.Background(), "dummy-id", topic, test.arguments, send) + provider, err := factory.NewDataProvider( + context.Background(), + "dummy-id", + topic, + test.arguments, + send, + ) require.NoError(t, err) require.NotNil(t, provider) @@ -102,7 +110,7 @@ func testHappyPath( } // extractPayload extracts the BaseDataProvidersResponse and its typed Payload. -func extractPayload[T any](t *testing.T, v interface{}) (*models.BaseDataProvidersResponse, T) { +func extractPayload[T any](t *testing.T, v any) (*models.BaseDataProvidersResponse, T) { response, ok := v.(*models.BaseDataProvidersResponse) require.True(t, ok, "Expected *models.BaseDataProvidersResponse, got %T", v) @@ -125,7 +133,7 @@ func TestEnsureAllowedFields(t *testing.T) { } t.Run("Valid fields with all required", func(t *testing.T) { - fields := map[string]interface{}{ + fields := map[string]any{ "start_block_id": "abc", "start_block_height": 123, "event_types": []string{"flow.Event"}, @@ -138,7 +146,7 @@ func TestEnsureAllowedFields(t *testing.T) { }) t.Run("Unexpected field present", func(t *testing.T) { - fields := map[string]interface{}{ + fields := map[string]any{ "start_block_id": "abc", "start_block_height": 123, "unknown_field": "unexpected", @@ -184,7 +192,7 @@ func TestExtractArrayOfStrings(t *testing.T) { }, { name: "Invalid type in array", - args: wsmodels.Arguments{"tags": []interface{}{"a", 123}}, + args: wsmodels.Arguments{"tags": []any{"a", 123}}, key: "tags", required: true, expect: nil, diff --git a/engine/access/rest/websockets/legacy/routes/subscribe_events.go b/engine/access/rest/websockets/legacy/routes/subscribe_events.go index 9d159e7bdd3..2ae130f6140 100644 --- a/engine/access/rest/websockets/legacy/routes/subscribe_events.go +++ b/engine/access/rest/websockets/legacy/routes/subscribe_events.go @@ -15,7 +15,7 @@ func SubscribeEvents( ctx context.Context, r *common.Request, wsController *legacy.WebsocketController, -) (subscription.Subscription, error) { +) (subscription.Subscription[*state_stream.EventsResponse], error) { req, err := request.SubscribeEventsRequest(r) if err != nil { return nil, common.NewBadRequestError(err) diff --git a/engine/access/rest/websockets/legacy/routes/subscribe_events_test.go b/engine/access/rest/websockets/legacy/routes/subscribe_events_test.go index aa59ffc8d99..c377e6f4f11 100644 --- a/engine/access/rest/websockets/legacy/routes/subscribe_events_test.go +++ b/engine/access/rest/websockets/legacy/routes/subscribe_events_test.go @@ -22,7 +22,6 @@ import ( "github.com/onflow/flow-go/engine/access/rest/http/request" "github.com/onflow/flow-go/engine/access/rest/router" "github.com/onflow/flow-go/engine/access/state_stream" - "github.com/onflow/flow-go/engine/access/state_stream/backend" ssmock "github.com/onflow/flow-go/engine/access/state_stream/mock" submock "github.com/onflow/flow-go/engine/access/subscription/mock" "github.com/onflow/flow-go/model/flow" @@ -165,7 +164,7 @@ func (s *SubscribeEventsSuite) TestSubscribeEvents() { for _, test := range tests { s.Run(test.name, func() { stateStreamBackend := ssmock.NewAPI(s.T()) - subscription := submock.NewSubscription(s.T()) + subscription := submock.NewSubscription[*state_stream.EventsResponse](s.T()) filter, err := state_stream.NewEventFilter( state_stream.DefaultEventFilterConfig, @@ -175,8 +174,8 @@ func (s *SubscribeEventsSuite) TestSubscribeEvents() { test.contracts) require.NoError(s.T(), err) - var expectedEventsResponses []*backend.EventsResponse - var subscriptionEventsResponses []*backend.EventsResponse + var expectedEventsResponses []*state_stream.EventsResponse + var subscriptionEventsResponses []*state_stream.EventsResponse startBlockFound := test.startBlockID == flow.ZeroID // construct expected event responses based on the provided test configuration @@ -199,14 +198,14 @@ func (s *SubscribeEventsSuite) TestSubscribeEvents() { } } if len(expectedEvents) > 0 || (i+1)%int(test.heartbeatInterval) == 0 { - expectedEventsResponses = append(expectedEventsResponses, &backend.EventsResponse{ + expectedEventsResponses = append(expectedEventsResponses, &state_stream.EventsResponse{ Height: block.Height, BlockID: blockID, Events: expectedEvents, BlockTimestamp: time.UnixMilli(int64(block.Timestamp)).UTC(), }) } - subscriptionEventsResponses = append(subscriptionEventsResponses, &backend.EventsResponse{ + subscriptionEventsResponses = append(subscriptionEventsResponses, &state_stream.EventsResponse{ Height: block.Height, BlockID: blockID, Events: subscriptionEvents, @@ -217,8 +216,8 @@ func (s *SubscribeEventsSuite) TestSubscribeEvents() { } // Create a channel to receive mock EventsResponse objects - ch := make(chan interface{}) - var chReadOnly <-chan interface{} + ch := make(chan *state_stream.EventsResponse) + var chReadOnly <-chan *state_stream.EventsResponse // Simulate sending a mock EventsResponse go func() { for _, eventResponse := range subscriptionEventsResponses { @@ -267,10 +266,10 @@ func (s *SubscribeEventsSuite) TestSubscribeEventsHandlesErrors() { s.Run("returns error for invalid block id", func() { stateStreamBackend := ssmock.NewAPI(s.T()) invalidBlock := unittest.BlockFixture() - subscription := submock.NewSubscription(s.T()) + subscription := submock.NewSubscription[*state_stream.EventsResponse](s.T()) - ch := make(chan interface{}) - var chReadOnly <-chan interface{} + ch := make(chan *state_stream.EventsResponse) + var chReadOnly <-chan *state_stream.EventsResponse go func() { close(ch) }() @@ -300,10 +299,10 @@ func (s *SubscribeEventsSuite) TestSubscribeEventsHandlesErrors() { s.Run("returns error when channel closed", func() { stateStreamBackend := ssmock.NewAPI(s.T()) - subscription := submock.NewSubscription(s.T()) + subscription := submock.NewSubscription[*state_stream.EventsResponse](s.T()) - ch := make(chan interface{}) - var chReadOnly <-chan interface{} + ch := make(chan *state_stream.EventsResponse) + var chReadOnly <-chan *state_stream.EventsResponse go func() { close(ch) @@ -397,7 +396,7 @@ func requireError(t *testing.T, recorder *router.TestHijackResponseRecorder, exp // requireResponse validates that the response received from WebSocket communication matches the expected EventsResponse. // This function compares the BlockID, Events count, and individual event properties for each expected and actual // EventsResponse. It ensures that the response received from WebSocket matches the expected structure and content. -func requireResponse(t *testing.T, recorder *router.TestHijackResponseRecorder, expected []*backend.EventsResponse) { +func requireResponse(t *testing.T, recorder *router.TestHijackResponseRecorder, expected []*state_stream.EventsResponse) { <-recorder.Closed // Convert the actual response from respRecorder to JSON bytes actualJSON := recorder.ResponseBuff.Bytes() @@ -406,9 +405,9 @@ func requireResponse(t *testing.T, recorder *router.TestHijackResponseRecorder, matches := regexp.MustCompile(pattern).FindAll(actualJSON, -1) // Unmarshal each matched JSON into []state_stream.EventsResponse - var actual []backend.EventsResponse + var actual []state_stream.EventsResponse for _, match := range matches { - var response backend.EventsResponse + var response state_stream.EventsResponse if err := json.Unmarshal(match, &response); err == nil { actual = append(actual, response) } diff --git a/engine/access/rest/websockets/legacy/websocket_handler.go b/engine/access/rest/websockets/legacy/websocket_handler.go index 37e30d53d49..5de73810a7e 100644 --- a/engine/access/rest/websockets/legacy/websocket_handler.go +++ b/engine/access/rest/websockets/legacy/websocket_handler.go @@ -102,7 +102,7 @@ func (wsController *WebsocketController) wsErrorHandler(err error) { // It listens to the subscription's channel for events and writes them to the WebSocket connection. // If an error occurs or the subscription channel is closed, it handles the error or termination accordingly. // The function uses a ticker to periodically send ping messages to the client to maintain the connection. -func (wsController *WebsocketController) writeEvents(sub subscription.Subscription) { +func (wsController *WebsocketController) writeEvents(sub subscription.Subscription[*state_stream.EventsResponse]) { ticker := time.NewTicker(websockets.PingPeriod) defer ticker.Stop() @@ -118,7 +118,7 @@ func (wsController *WebsocketController) writeEvents(sub subscription.Subscripti wsController.wsErrorHandler(err) } return - case event, ok := <-sub.Channel(): + case resp, ok := <-sub.Channel(): if !ok { if sub.Err() != nil { err := fmt.Errorf("stream encountered an error: %v", sub.Err()) @@ -135,12 +135,6 @@ func (wsController *WebsocketController) writeEvents(sub subscription.Subscripti return } - resp, ok := event.(*backend.EventsResponse) - if !ok { - err = fmt.Errorf("unexpected response type: %s", event) - wsController.wsErrorHandler(err) - return - } // responses with empty events increase heartbeat interval counter, when threshold is met a heartbeat // message will be emitted. if len(resp.Events) == 0 { @@ -177,7 +171,7 @@ func (wsController *WebsocketController) writeEvents(sub subscription.Subscripti } // Write the response to the WebSocket connection - err = wsController.conn.WriteJSON(event) + err = wsController.conn.WriteJSON(resp) if err != nil { wsController.wsErrorHandler(err) return @@ -234,7 +228,7 @@ type SubscribeHandlerFunc func( ctx context.Context, request *common.Request, wsController *WebsocketController, -) (subscription.Subscription, error) +) (subscription.Subscription[*state_stream.EventsResponse], error) // WSHandler is websocket handler implementing custom websocket handler function and allows easier handling of errors and // responses as it wraps functionality for handling error and responses outside of endpoint handling. diff --git a/engine/access/rest/websockets/models/subscribe_message.go b/engine/access/rest/websockets/models/subscribe_message.go index 532e4c6a987..898565dd51f 100644 --- a/engine/access/rest/websockets/models/subscribe_message.go +++ b/engine/access/rest/websockets/models/subscribe_message.go @@ -1,6 +1,6 @@ package models -type Arguments map[string]interface{} +type Arguments = map[string]any // SubscribeMessageRequest represents a request to subscribe to a topic. type SubscribeMessageRequest struct { diff --git a/engine/access/rpc/backend/backend.go b/engine/access/rpc/backend/backend.go index f654d9fdc8e..6c3a435607b 100644 --- a/engine/access/rpc/backend/backend.go +++ b/engine/access/rpc/backend/backend.go @@ -13,6 +13,7 @@ import ( "github.com/onflow/flow-go/access" "github.com/onflow/flow-go/access/validator" "github.com/onflow/flow-go/cmd/build" + "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/engine/access/index" "github.com/onflow/flow-go/engine/access/rpc/backend/accounts" "github.com/onflow/flow-go/engine/access/rpc/backend/common" @@ -27,7 +28,7 @@ import ( txstream "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/stream" "github.com/onflow/flow-go/engine/access/rpc/connection" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/streamer" "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/version" "github.com/onflow/flow-go/fvm/blueprints" @@ -80,7 +81,7 @@ type Backend struct { stateParams protocol.Params versionControl *version.VersionControl - BlockTracker tracker.BlockTracker + BlockTracker subscription.BlockTracker } type Params struct { @@ -107,8 +108,7 @@ type Params struct { ScriptExecutionMode query_mode.IndexQueryMode CheckPayerBalanceMode validator.PayerBalanceMode EventQueryMode query_mode.IndexQueryMode - BlockTracker tracker.BlockTracker - SubscriptionHandler *subscription.SubscriptionHandler + BlockTracker subscription.BlockTracker MaxScriptAndArgumentSize uint EventsIndex *index.EventsIndex @@ -123,6 +123,9 @@ type Params struct { ExecutionResultInfoProvider optimistic_sync.ExecutionResultInfoProvider ExecutionStateCache optimistic_sync.ExecutionStateCache ScheduledCallbacksEnabled bool + + FinalizedBlockBroadcaster *engine.Broadcaster + StreamOptions *streamer.StreamOptions } var _ access.API = (*Backend)(nil) @@ -289,7 +292,6 @@ func New(params Params) (*Backend, error) { txStreamBackend := txstream.NewTransactionStreamBackend( params.Log, params.State, - params.SubscriptionHandler, params.BlockTracker, txBackend.SendTransaction, params.Blocks, @@ -297,6 +299,9 @@ func New(params Params) (*Backend, error) { params.Transactions, failoverTxProvider, txStatusDeriver, + params.FinalizedBlockBroadcaster, + params.StreamOptions, + 0, // all streams in backend are unbounded (not true for txs though TODO) ) b := &Backend{ @@ -329,12 +334,14 @@ func New(params Params) (*Backend, error) { snapshotHistoryLimit: params.SnapshotHistoryLimit, }, backendSubscribeBlocks: backendSubscribeBlocks{ - log: params.Log, - state: params.State, - headers: params.Headers, - blocks: params.Blocks, - subscriptionHandler: params.SubscriptionHandler, - blockTracker: params.BlockTracker, + log: params.Log, + state: params.State, + headers: params.Headers, + blocks: params.Blocks, + blockTracker: params.BlockTracker, + finalizedBlockBroadcaster: params.FinalizedBlockBroadcaster, + streamOptions: params.StreamOptions, + endHeight: 0, }, state: params.State, diff --git a/engine/access/rpc/backend/backend_stream_block_digests_test.go b/engine/access/rpc/backend/backend_stream_block_digests_test.go index 76e4c508207..15180deafd2 100644 --- a/engine/access/rpc/backend/backend_stream_block_digests_test.go +++ b/engine/access/rpc/backend/backend_stream_block_digests_test.go @@ -3,9 +3,9 @@ package backend import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -31,36 +31,45 @@ func (s *BackendBlockDigestSuite) SetupTest() { // TestSubscribeBlockDigestsFromStartBlockID tests the SubscribeBlockDigestsFromStartBlockID method. func (s *BackendBlockDigestSuite) TestSubscribeBlockDigestsFromStartBlockID() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + subscribeFunc := func( + ctx context.Context, + startValue interface{}, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.BlockDigest] { return s.backend.SubscribeBlockDigestsFromStartBlockID(ctx, startValue.(flow.Identifier), blockStatus) } - s.subscribe(call, s.requireBlockDigests, s.subscribeFromStartBlockIdTestCases()) + subscribe(&s.BackendBlocksSuite, subscribeFunc, s.requireBlockDigests, s.subscribeFromStartBlockIdTestCases()) } // TestSubscribeBlockDigestsFromStartHeight tests the SubscribeBlockDigestsFromStartHeight method. func (s *BackendBlockDigestSuite) TestSubscribeBlockDigestsFromStartHeight() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func( + ctx context.Context, + startValue interface{}, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.BlockDigest] { return s.backend.SubscribeBlockDigestsFromStartHeight(ctx, startValue.(uint64), blockStatus) } - s.subscribe(call, s.requireBlockDigests, s.subscribeFromStartHeightTestCases()) + subscribe(&s.BackendBlocksSuite, call, s.requireBlockDigests, s.subscribeFromStartHeightTestCases()) } // TestSubscribeBlockDigestsFromLatest tests the SubscribeBlockDigestsFromLatest method. func (s *BackendBlockDigestSuite) TestSubscribeBlockDigestsFromLatest() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func( + ctx context.Context, + startValue interface{}, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.BlockDigest] { return s.backend.SubscribeBlockDigestsFromLatest(ctx, blockStatus) } - s.subscribe(call, s.requireBlockDigests, s.subscribeFromLatestTestCases()) + subscribe(&s.BackendBlocksSuite, call, s.requireBlockDigests, s.subscribeFromLatestTestCases()) } // requireBlockDigests ensures that the received block digest information matches the expected data. -func (s *BackendBlockDigestSuite) requireBlockDigests(v interface{}, expectedBlock *flow.Block) { - actualBlock, ok := v.(*flow.BlockDigest) - require.True(s.T(), ok, "unexpected response type: %T", v) - +func (s *BackendBlockDigestSuite) requireBlockDigests(actualBlock *flow.BlockDigest, expectedBlock *flow.Block) { s.Require().Equal(expectedBlock.ID(), actualBlock.BlockID) s.Require().Equal(expectedBlock.Height, actualBlock.Height) s.Require().Equal(expectedBlock.Timestamp, uint64(actualBlock.Timestamp.UnixMilli())) @@ -82,14 +91,14 @@ func (s *BackendBlockDigestSuite) requireBlockDigests(v interface{}, expectedBlo // // Each test case checks for specific error conditions and ensures that the methods responds appropriately. func (s *BackendBlockDigestSuite) TestSubscribeBlockDigestsHandlesErrors() { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() backend, err := New(s.backendParams(engine.NewBroadcaster())) s.Require().NoError(err) s.Run("returns error if unknown start block id is provided", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlockDigestsFromStartBlockID(subCtx, unittest.IdentifierFixture(), flow.BlockStatusFinalized) @@ -97,7 +106,7 @@ func (s *BackendBlockDigestSuite) TestSubscribeBlockDigestsHandlesErrors() { }) s.Run("returns error for start height before root height", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlockDigestsFromStartHeight(subCtx, s.rootBlock.Height-1, flow.BlockStatusFinalized) @@ -105,7 +114,7 @@ func (s *BackendBlockDigestSuite) TestSubscribeBlockDigestsHandlesErrors() { }) s.Run("returns error if unknown start height is provided", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlockDigestsFromStartHeight(subCtx, s.blocksArray[len(s.blocksArray)-1].Height+10, flow.BlockStatusFinalized) diff --git a/engine/access/rpc/backend/backend_stream_block_headers_test.go b/engine/access/rpc/backend/backend_stream_block_headers_test.go index 5994b01cfa3..057a1aac403 100644 --- a/engine/access/rpc/backend/backend_stream_block_headers_test.go +++ b/engine/access/rpc/backend/backend_stream_block_headers_test.go @@ -3,9 +3,9 @@ package backend import ( "context" "testing" + "time" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -31,36 +31,45 @@ func (s *BackendBlockHeadersSuite) SetupTest() { // TestSubscribeBlockHeadersFromStartBlockID tests the SubscribeBlockHeadersFromStartBlockID method. func (s *BackendBlockHeadersSuite) TestSubscribeBlockHeadersFromStartBlockID() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func( + ctx context.Context, + startValue interface{}, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Header] { return s.backend.SubscribeBlockHeadersFromStartBlockID(ctx, startValue.(flow.Identifier), blockStatus) } - s.subscribe(call, s.requireBlockHeaders, s.subscribeFromStartBlockIdTestCases()) + subscribe(&s.BackendBlocksSuite, call, s.requireBlockHeaders, s.subscribeFromStartBlockIdTestCases()) } // TestSubscribeBlockHeadersFromStartHeight tests the SubscribeBlockHeadersFromStartHeight method. func (s *BackendBlockHeadersSuite) TestSubscribeBlockHeadersFromStartHeight() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func( + ctx context.Context, + startValue interface{}, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Header] { return s.backend.SubscribeBlockHeadersFromStartHeight(ctx, startValue.(uint64), blockStatus) } - s.subscribe(call, s.requireBlockHeaders, s.subscribeFromStartHeightTestCases()) + subscribe(&s.BackendBlocksSuite, call, s.requireBlockHeaders, s.subscribeFromStartHeightTestCases()) } // TestSubscribeBlockHeadersFromLatest tests the SubscribeBlockHeadersFromLatest method. func (s *BackendBlockHeadersSuite) TestSubscribeBlockHeadersFromLatest() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func( + ctx context.Context, + startValue interface{}, + blockStatus flow.BlockStatus, + ) subscription.Subscription[*flow.Header] { return s.backend.SubscribeBlockHeadersFromLatest(ctx, blockStatus) } - s.subscribe(call, s.requireBlockHeaders, s.subscribeFromLatestTestCases()) + subscribe(&s.BackendBlocksSuite, call, s.requireBlockHeaders, s.subscribeFromLatestTestCases()) } // requireBlockHeaders ensures that the received block header information matches the expected data. -func (s *BackendBlockHeadersSuite) requireBlockHeaders(v interface{}, expectedBlock *flow.Block) { - actualHeader, ok := v.(*flow.Header) - require.True(s.T(), ok, "unexpected response type: %T", v) - +func (s *BackendBlockHeadersSuite) requireBlockHeaders(actualHeader *flow.Header, expectedBlock *flow.Block) { s.Require().Equal(expectedBlock.Height, actualHeader.Height) s.Require().Equal(expectedBlock.ToHeader().ID(), actualHeader.ID()) s.Require().Equal(*expectedBlock.ToHeader(), *actualHeader) @@ -82,14 +91,14 @@ func (s *BackendBlockHeadersSuite) requireBlockHeaders(v interface{}, expectedBl // // Each test case checks for specific error conditions and ensures that the methods responds appropriately. func (s *BackendBlockHeadersSuite) TestSubscribeBlockHeadersHandlesErrors() { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() backend, err := New(s.backendParams(engine.NewBroadcaster())) s.Require().NoError(err) s.Run("returns error for unknown start block id is provided", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlockHeadersFromStartBlockID(subCtx, unittest.IdentifierFixture(), flow.BlockStatusFinalized) @@ -97,7 +106,7 @@ func (s *BackendBlockHeadersSuite) TestSubscribeBlockHeadersHandlesErrors() { }) s.Run("returns error if start height before root height", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlockHeadersFromStartHeight(subCtx, s.rootBlock.Height-1, flow.BlockStatusFinalized) @@ -105,7 +114,7 @@ func (s *BackendBlockHeadersSuite) TestSubscribeBlockHeadersHandlesErrors() { }) s.Run("returns error for unknown start height is provided", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlockHeadersFromStartHeight(subCtx, s.blocksArray[len(s.blocksArray)-1].Height+10, flow.BlockStatusFinalized) diff --git a/engine/access/rpc/backend/backend_stream_blocks.go b/engine/access/rpc/backend/backend_stream_blocks.go index 4c7b032fbc7..3d85ea59b12 100644 --- a/engine/access/rpc/backend/backend_stream_blocks.go +++ b/engine/access/rpc/backend/backend_stream_blocks.go @@ -8,8 +8,11 @@ import ( "github.com/rs/zerolog" + "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/height_source" + "github.com/onflow/flow-go/engine/access/subscription/streamer" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/state/protocol" "github.com/onflow/flow-go/storage" @@ -23,8 +26,10 @@ type backendSubscribeBlocks struct { blocks storage.Blocks headers storage.Headers - subscriptionHandler *subscription.SubscriptionHandler - blockTracker tracker.BlockTracker + blockTracker subscription.BlockTracker + finalizedBlockBroadcaster *engine.Broadcaster + streamOptions *streamer.StreamOptions + endHeight uint64 } // SubscribeBlocksFromStartBlockID subscribes to the finalized or sealed blocks starting at the requested @@ -41,8 +46,34 @@ type backendSubscribeBlocks struct { // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlocksFromStartBlockID will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlocksFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromStartBlockID(ctx, startBlockID, b.getBlockResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlocksFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.Block] { + startHeight, err := b.blockTracker.GetStartHeightFromBlockID(startBlockID) + if err != nil { + return subimpl.NewFailedSubscription[*flow.Block](err, "could not get start height from latest") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.blockAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.Block](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlocksFromStartHeight subscribes to the finalized or sealed blocks starting at the requested @@ -59,8 +90,35 @@ func (b *backendSubscribeBlocks) SubscribeBlocksFromStartBlockID(ctx context.Con // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlocksFromStartHeight will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlocksFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromStartHeight(ctx, startHeight, b.getBlockResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlocksFromStartHeight( + ctx context.Context, + startHeight uint64, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.Block] { + // validate and normalize start height via tracker to surface errors early (e.g., before-root, unknown height) + startHeight, err := b.blockTracker.GetStartHeightFromHeight(startHeight) + if err != nil { + return subimpl.NewFailedSubscription[*flow.Block](err, "could not get start height from provided height") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.blockAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.Block](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlocksFromLatest subscribes to the finalized or sealed blocks starting at the latest sealed block, @@ -76,8 +134,33 @@ func (b *backendSubscribeBlocks) SubscribeBlocksFromStartHeight(ctx context.Cont // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlocksFromLatest will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlocksFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromLatest(ctx, b.getBlockResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlocksFromLatest( + ctx context.Context, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.Block] { + startHeight, err := b.blockTracker.GetStartHeightFromLatest(ctx) + if err != nil { + return subimpl.NewFailedSubscription[*flow.Block](err, "could not get start height from latest") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.blockAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.Block](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlockHeadersFromStartBlockID streams finalized or sealed block headers starting at the requested @@ -94,8 +177,34 @@ func (b *backendSubscribeBlocks) SubscribeBlocksFromLatest(ctx context.Context, // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlockHeadersFromStartBlockID will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromStartBlockID(ctx, startBlockID, b.getBlockHeaderResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.Header] { + startHeight, err := b.blockTracker.GetStartHeightFromBlockID(startBlockID) + if err != nil { + return subimpl.NewFailedSubscription[*flow.Header](err, "could not get start height from latest") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.headerAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.Header](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlockHeadersFromStartHeight streams finalized or sealed block headers starting at the requested @@ -112,8 +221,35 @@ func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartBlockID(ctx conte // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlockHeadersFromStartHeight will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromStartHeight(ctx, startHeight, b.getBlockHeaderResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartHeight( + ctx context.Context, + startHeight uint64, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.Header] { + // validate and normalize start height via tracker to surface errors early (e.g., before-root, unknown height) + startHeight, err := b.blockTracker.GetStartHeightFromHeight(startHeight) + if err != nil { + return subimpl.NewFailedSubscription[*flow.Header](err, "could not get start height from provided height") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.headerAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.Header](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlockHeadersFromLatest streams finalized or sealed block headers starting at the latest sealed block, @@ -129,8 +265,33 @@ func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromStartHeight(ctx contex // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlockHeadersFromLatest will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromLatest(ctx, b.getBlockHeaderResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromLatest( + ctx context.Context, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.Header] { + startHeight, err := b.blockTracker.GetStartHeightFromLatest(ctx) + if err != nil { + return subimpl.NewFailedSubscription[*flow.Header](err, "could not get start height from latest") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.headerAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.Header](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlockDigestsFromStartBlockID streams finalized or sealed lightweight block starting at the requested @@ -147,8 +308,34 @@ func (b *backendSubscribeBlocks) SubscribeBlockHeadersFromLatest(ctx context.Con // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlockDigestsFromStartBlockID will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromStartBlockID(ctx, startBlockID, b.getBlockDigestResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.BlockDigest] { + startHeight, err := b.blockTracker.GetStartHeightFromBlockID(startBlockID) + if err != nil { + return subimpl.NewFailedSubscription[*flow.BlockDigest](err, "could not get start height from latest") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.blockDigestAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.BlockDigest](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeBlockDigestsFromStartHeight streams finalized or sealed lightweight block starting at the requested @@ -165,134 +352,98 @@ func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartBlockID(ctx conte // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // // If invalid parameters will be supplied SubscribeBlockDigestsFromStartHeight will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartHeight(ctx context.Context, startHeight uint64, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromStartHeight(ctx, startHeight, b.getBlockDigestResponse(blockStatus)) +func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromStartHeight( + ctx context.Context, + startHeight uint64, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.BlockDigest] { + // validate and normalize start height via tracker to surface errors early (e.g., before-root, unknown height) + startHeight, err := b.blockTracker.GetStartHeightFromHeight(startHeight) + if err != nil { + return subimpl.NewFailedSubscription[*flow.BlockDigest](err, "could not get start height from provided height") + } + + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.blockDigestAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.BlockDigest](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } -// SubscribeBlockDigestsFromLatest streams finalized or sealed lightweight block starting at the latest sealed block, -// up until the latest available block. Once the latest is +// SubscribeBlockDigestsFromLatest streams finalized or sealed block digests starting at the latest sealed block, +// up until the latest available block digest. Once the latest is // reached, the stream will remain open and responses are sent for each new -// block as it becomes available. +// block header as it becomes available. // -// Each lightweight block are filtered by the provided block status, and only -// those blocks that match the status are returned. +// Each block digest are filtered by the provided block status, and only +// those block digests that match the status are returned. // // Parameters: // - ctx: Context for the operation. // - blockStatus: The status of the block, which could be only BlockStatusSealed or BlockStatusFinalized. // -// If invalid parameters will be supplied SubscribeBlockDigestsFromLatest will return a failed subscription. -func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromLatest(ctx context.Context, blockStatus flow.BlockStatus) subscription.Subscription { - return b.subscribeFromLatest(ctx, b.getBlockDigestResponse(blockStatus)) -} - -// subscribeFromStartBlockID is common method that allows clients to subscribe starting at the requested start block id. -// -// Parameters: -// - ctx: Context for the operation. -// - startBlockID: The identifier of the starting block. -// - getData: The callback used by subscriptions to retrieve data information for the specified height and block status. -// -// If invalid parameters are supplied, subscribeFromStartBlockID will return a failed subscription. -func (b *backendSubscribeBlocks) subscribeFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, getData subscription.GetDataByHeightFunc) subscription.Subscription { - nextHeight, err := b.blockTracker.GetStartHeightFromBlockID(startBlockID) +// If invalid parameters are provided, SubscribeBlockDigestsFromLatest will return a failed subscription. +func (b *backendSubscribeBlocks) SubscribeBlockDigestsFromLatest( + ctx context.Context, + blockStatus flow.BlockStatus, +) subscription.Subscription[*flow.BlockDigest] { + startHeight, err := b.blockTracker.GetStartHeightFromLatest(ctx) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block id") + return subimpl.NewFailedSubscription[*flow.BlockDigest](err, "could not get start height from latest") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, getData) -} -// subscribeFromStartHeight is common method that allows clients to subscribe starting at the requested start block height. -// -// Parameters: -// - ctx: Context for the operation. -// - startHeight: The height of the starting block. -// - getData: The callback used by subscriptions to retrieve data information for the specified height and block status. -// -// If invalid parameters are supplied, subscribeFromStartHeight will return a failed subscription. -func (b *backendSubscribeBlocks) subscribeFromStartHeight(ctx context.Context, startHeight uint64, getData subscription.GetDataByHeightFunc) subscription.Subscription { - nextHeight, err := b.blockTracker.GetStartHeightFromHeight(startHeight) - if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block height") - } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, getData) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.buildReadyUpToHeight(blockStatus), + b.blockDigestAtHeight, + ) + + sub := subimpl.NewSubscription[*flow.BlockDigest](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.finalizedBlockBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } -// subscribeFromLatest is common method that allows clients to subscribe starting at the latest sealed block. -// -// Parameters: -// - ctx: Context for the operation. -// - getData: The callback used by subscriptions to retrieve data information for the specified height and block status. -// -// No errors are expected during normal operation. -func (b *backendSubscribeBlocks) subscribeFromLatest(ctx context.Context, getData subscription.GetDataByHeightFunc) subscription.Subscription { - nextHeight, err := b.blockTracker.GetStartHeightFromLatest(ctx) +func (b *backendSubscribeBlocks) blockAtHeight(_ context.Context, height uint64) (*flow.Block, error) { + // since we are querying a finalized or sealed block, we can use the height index and save an ID computation + block, err := b.blocks.ByHeight(height) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from latest") - } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, getData) -} - -// getBlockResponse returns a GetDataByHeightFunc that retrieves block information for the specified height. -func (b *backendSubscribeBlocks) getBlockResponse(blockStatus flow.BlockStatus) subscription.GetDataByHeightFunc { - return func(_ context.Context, height uint64) (interface{}, error) { - block, err := b.getBlock(height, blockStatus) - if err != nil { - return nil, err - } - - b.log.Trace(). - Hex("block_id", logging.ID(block.ID())). - Uint64("height", height). - Msgf("sending block info") - - return block, nil - } -} - -// getBlockHeaderResponse returns a GetDataByHeightFunc that retrieves block header information for the specified height. -func (b *backendSubscribeBlocks) getBlockHeaderResponse(blockStatus flow.BlockStatus) subscription.GetDataByHeightFunc { - return func(_ context.Context, height uint64) (interface{}, error) { - header, err := b.getBlockHeader(height, blockStatus) - if err != nil { - return nil, err + if errors.Is(err, storage.ErrNotFound) { + return nil, fmt.Errorf("failed to retrieve block for height %d: %w", height, subscription.ErrBlockNotReady) } - - b.log.Trace(). - Hex("block_id", logging.ID(header.ID())). - Uint64("height", height). - Msgf("sending block header info") - - return header, nil + return nil, err } -} -// getBlockDigestResponse returns a GetDataByHeightFunc that retrieves lightweight block information for the specified height. -func (b *backendSubscribeBlocks) getBlockDigestResponse(blockStatus flow.BlockStatus) subscription.GetDataByHeightFunc { - return func(_ context.Context, height uint64) (interface{}, error) { - header, err := b.getBlockHeader(height, blockStatus) - if err != nil { - return nil, err - } + b.log.Trace(). + Hex("block_id", logging.ID(block.ID())). + Uint64("height", height). + Msgf("sending block info") - b.log.Trace(). - Hex("block_id", logging.ID(header.ID())). - Uint64("height", height). - Msgf("sending lightweight block info") - - return flow.NewBlockDigest(header.ID(), header.Height, time.UnixMilli(int64(header.Timestamp)).UTC()), nil - } + return block, nil } -// getBlockHeader returns the block header for the given block height. -// Expected errors during normal operation: -// - subscription.ErrBlockNotReady: block for the given block height is not available. -func (b *backendSubscribeBlocks) getBlockHeader(height uint64, expectedBlockStatus flow.BlockStatus) (*flow.Header, error) { - err := b.validateHeight(height, expectedBlockStatus) - if err != nil { - return nil, err - } - +func (b *backendSubscribeBlocks) headerAtHeight(_ context.Context, height uint64) (*flow.Header, error) { // since we are querying a finalized or sealed block header, we can use the height index and save an ID computation header, err := b.headers.ByHeight(height) if err != nil { @@ -302,45 +453,30 @@ func (b *backendSubscribeBlocks) getBlockHeader(height uint64, expectedBlockStat return nil, err } + b.log.Trace(). + Hex("block_id", logging.ID(header.ID())). + Uint64("height", height). + Msgf("sending block header info") + return header, nil } -// getBlock returns the block for the given block height. -// Expected errors during normal operation: -// - subscription.ErrBlockNotReady: block for the given block height is not available. -func (b *backendSubscribeBlocks) getBlock(height uint64, expectedBlockStatus flow.BlockStatus) (*flow.Block, error) { - err := b.validateHeight(height, expectedBlockStatus) +func (b *backendSubscribeBlocks) blockDigestAtHeight(ctx context.Context, height uint64) (*flow.BlockDigest, error) { + header, err := b.headerAtHeight(ctx, height) if err != nil { return nil, err } - // since we are querying a finalized or sealed block, we can use the height index and save an ID computation - block, err := b.blocks.ByHeight(height) - if err != nil { - if errors.Is(err, storage.ErrNotFound) { - return nil, fmt.Errorf("failed to retrieve block for height %d: %w", height, subscription.ErrBlockNotReady) - } - return nil, err - } + b.log.Trace(). + Hex("block_id", logging.ID(header.ID())). + Uint64("height", height). + Msgf("sending lightweight block info") - return block, nil + return flow.NewBlockDigest(header.ID(), header.Height, time.UnixMilli(int64(header.Timestamp)).UTC()), nil } -// validateHeight checks if the given block height is valid and available based on the expected block status. -// Expected errors during normal operation: -// - subscription.ErrBlockNotReady when unable to retrieve the block by height. -func (b *backendSubscribeBlocks) validateHeight(height uint64, expectedBlockStatus flow.BlockStatus) error { - highestHeight, err := b.blockTracker.GetHighestHeight(expectedBlockStatus) - if err != nil { - return fmt.Errorf("could not get highest available height: %w", err) +func (b *backendSubscribeBlocks) buildReadyUpToHeight(blockStatus flow.BlockStatus) func() (uint64, error) { + return func() (uint64, error) { + return b.blockTracker.GetHighestHeight(blockStatus) } - - // fail early if no notification has been received for the given block height. - // note: it's possible for the data to exist in the data store before the notification is - // received. this ensures a consistent view is available to all streams. - if height > highestHeight { - return fmt.Errorf("block %d is not available yet: %w", height, subscription.ErrBlockNotReady) - } - - return nil } diff --git a/engine/access/rpc/backend/backend_stream_blocks_test.go b/engine/access/rpc/backend/backend_stream_blocks_test.go index b1a565b1f06..dcb94678d01 100644 --- a/engine/access/rpc/backend/backend_stream_blocks_test.go +++ b/engine/access/rpc/backend/backend_stream_blocks_test.go @@ -5,10 +5,10 @@ import ( "fmt" "testing" "testing/synctest" + "time" "github.com/rs/zerolog" "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -18,6 +18,7 @@ import ( "github.com/onflow/flow-go/engine/access/rpc/backend/query_mode" connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription/streamer" "github.com/onflow/flow-go/engine/access/subscription/tracker" "github.com/onflow/flow-go/model/flow" osyncmock "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync/mock" @@ -40,7 +41,7 @@ type BackendBlocksSuite struct { blocks *storagemock.Blocks headers *storagemock.Headers - blockTracker tracker.BlockTracker + blockTracker subscription.BlockTracker connectionFactory *connectionmock.ConnectionFactory @@ -153,43 +154,39 @@ func (s *BackendBlocksSuite) backendParams(broadcaster *engine.Broadcaster) Para s.Require().NoError(err) return Params{ - State: s.state, - Blocks: s.blocks, - Headers: s.headers, - ChainID: s.chainID, - MaxHeightRange: events.DefaultMaxHeightRange, - SnapshotHistoryLimit: DefaultSnapshotHistoryLimit, - AccessMetrics: metrics.NewNoopCollector(), - Log: s.log, - SubscriptionHandler: subscription.NewSubscriptionHandler( - s.log, - broadcaster, - subscription.DefaultSendTimeout, - subscription.DefaultResponseLimit, - subscription.DefaultSendBufferSize, - ), + State: s.state, + Blocks: s.blocks, + Headers: s.headers, + ChainID: s.chainID, + MaxHeightRange: events.DefaultMaxHeightRange, + SnapshotHistoryLimit: DefaultSnapshotHistoryLimit, + AccessMetrics: metrics.NewNoopCollector(), + Log: s.log, BlockTracker: s.blockTracker, EventQueryMode: query_mode.IndexQueryModeExecutionNodesOnly, ScriptExecutionMode: query_mode.IndexQueryModeExecutionNodesOnly, TxResultQueryMode: query_mode.IndexQueryModeExecutionNodesOnly, ExecutionResultInfoProvider: s.executionResultInfoProvider, ExecutionStateCache: s.executionStateCache, + FinalizedBlockBroadcaster: broadcaster, + StreamOptions: streamer.NewDefaultStreamOptions(), } } // subscribeFromStartBlockIdTestCases generates variations of testType scenarios for subscriptions // starting from a specified block ID. It is designed to test the subscription functionality when the subscription // starts from a custom block ID, either sealed or finalized. +// Note: HeightTracker semantics skip the root block. When starting at root (or latest which equals root in tests), +// expectations should begin from root+1. func (s *BackendBlocksSuite) subscribeFromStartBlockIdTestCases() []testType { - expectedFromRoot := []*flow.Block{s.rootBlock} - expectedFromRoot = append(expectedFromRoot, s.blocksArray...) + expectedFromAfterRoot := s.blocksArray // start from the block after root baseTests := []testType{ { name: "happy path - all new blocks", highestBackfill: -1, // no backfill startValue: s.rootBlock.ID(), - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, { name: "happy path - partial backfill", @@ -207,7 +204,7 @@ func (s *BackendBlocksSuite) subscribeFromStartBlockIdTestCases() []testType { name: "happy path - start from root block by id", highestBackfill: len(s.blocksArray) - 1, // backfill all blocks startValue: s.rootBlock.ID(), // start from root block - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, } @@ -217,16 +214,16 @@ func (s *BackendBlocksSuite) subscribeFromStartBlockIdTestCases() []testType { // subscribeFromStartHeightTestCases generates variations of testType scenarios for subscriptions // starting from a specified block height. It is designed to test the subscription functionality when the subscription // starts from a custom height, either sealed or finalized. +// Note: HeightTracker semantics skip the root block. When starting at root height, expectations begin from root+1. func (s *BackendBlocksSuite) subscribeFromStartHeightTestCases() []testType { - expectedFromRoot := []*flow.Block{s.rootBlock} - expectedFromRoot = append(expectedFromRoot, s.blocksArray...) + expectedFromAfterRoot := s.blocksArray // start from the block after root baseTests := []testType{ { name: "happy path - all new blocks", highestBackfill: -1, // no backfill startValue: s.rootBlock.Height, - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, { name: "happy path - partial backfill", @@ -244,7 +241,7 @@ func (s *BackendBlocksSuite) subscribeFromStartHeightTestCases() []testType { name: "happy path - start from root block by id", highestBackfill: len(s.blocksArray) - 1, // backfill all blocks startValue: s.rootBlock.Height, // start from root block - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, } @@ -254,25 +251,25 @@ func (s *BackendBlocksSuite) subscribeFromStartHeightTestCases() []testType { // subscribeFromLatestTestCases generates variations of testType scenarios for subscriptions // starting from the latest sealed block. It is designed to test the subscription functionality when the subscription // starts from the latest available block, either sealed or finalized. +// Note: In these tests, latest equals root at start; expectations should begin from root+1. func (s *BackendBlocksSuite) subscribeFromLatestTestCases() []testType { - expectedFromRoot := []*flow.Block{s.rootBlock} - expectedFromRoot = append(expectedFromRoot, s.blocksArray...) + expectedFromAfterRoot := s.blocksArray // start from the block after root baseTests := []testType{ { name: "happy path - all new blocks", highestBackfill: -1, // no backfill - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, { name: "happy path - partial backfill", highestBackfill: 2, // backfill the first 3 blocks - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, { name: "happy path - complete backfill", highestBackfill: len(s.blocksArray) - 1, // backfill all blocks - expectedBlocks: expectedFromRoot, + expectedBlocks: expectedFromAfterRoot, }, } @@ -318,31 +315,30 @@ func (s *BackendBlocksSuite) setupBlockTrackerMock(blockStatus flow.BlockStatus, s.Require().NoError(err) } -// TestSubscribeBlocksFromStartBlockID tests the SubscribeBlocksFromStartBlockID method. func (s *BackendBlocksSuite) TestSubscribeBlocksFromStartBlockID() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { return s.backend.SubscribeBlocksFromStartBlockID(ctx, startValue.(flow.Identifier), blockStatus) } - s.subscribe(call, s.requireBlocks, s.subscribeFromStartBlockIdTestCases()) + subscribe(s, call, s.requireBlocks, s.subscribeFromStartBlockIdTestCases()) } // TestSubscribeBlocksFromStartHeight tests the SubscribeBlocksFromStartHeight method. func (s *BackendBlocksSuite) TestSubscribeBlocksFromStartHeight() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { return s.backend.SubscribeBlocksFromStartHeight(ctx, startValue.(uint64), blockStatus) } - s.subscribe(call, s.requireBlocks, s.subscribeFromStartHeightTestCases()) + subscribe(s, call, s.requireBlocks, s.subscribeFromStartHeightTestCases()) } // TestSubscribeBlocksFromLatest tests the SubscribeBlocksFromLatest method. func (s *BackendBlocksSuite) TestSubscribeBlocksFromLatest() { - call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription { + call := func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription[*flow.Block] { return s.backend.SubscribeBlocksFromLatest(ctx, blockStatus) } - s.subscribe(call, s.requireBlocks, s.subscribeFromLatestTestCases()) + subscribe(s, call, s.requireBlocks, s.subscribeFromLatestTestCases()) } // subscribe is the common method with tests the functionality of the subscribe methods in the Backend. @@ -370,9 +366,10 @@ func (s *BackendBlocksSuite) TestSubscribeBlocksFromLatest() { // 6. Simulates the reception of new blocks and consumes them from the subscription channel. // 7. Ensures that there are no new messages waiting after all blocks have been processed. // 8. Cancels the subscription and ensures it shuts down gracefully. -func (s *BackendBlocksSuite) subscribe( - subscribeFn func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription, - requireFn func(interface{}, *flow.Block), +func subscribe[T any]( + s *BackendBlocksSuite, + subscribeFn func(ctx context.Context, startValue interface{}, blockStatus flow.BlockStatus) subscription.Subscription[T], + requireFn func(T, *flow.Block), tests []testType, ) { for _, test := range tests { @@ -387,11 +384,11 @@ func (s *BackendBlocksSuite) subscribe( // add "backfill" block - blocks that are already in the database before the test starts // this simulates a subscription on a past block - if test.highestBackfill > 0 { + if test.highestBackfill >= 0 { s.setupBlockTrackerMock(test.blockStatus, s.blocksArray[test.highestBackfill].ToHeader()) } - subCtx, subCancel := context.WithCancel(context.Background()) + subCtx, subCancel := context.WithTimeout(context.Background(), 5*time.Second) // mock latest sealed if no start value provided if test.startValue == nil { @@ -414,9 +411,9 @@ func (s *BackendBlocksSuite) subscribe( // block until there is data waiting in the subscription channel synctest.Wait() - // consume block from subscription + // consume value from subscription v, ok := <-sub.Channel() - s.Require().True(ok, "channel closed while waiting for exec data for block %x %v: err: %v", b.Height, b.ID(), sub.Err()) + s.Require().True(ok, "channel closed while waiting for data for block %x %v: err: %v", b.Height, b.ID(), sub.Err()) requireFn(v, b) } @@ -439,7 +436,7 @@ func (s *BackendBlocksSuite) subscribe( // ensure subscription shuts down gracefully v, ok := <-sub.Channel() - s.Nil(v) + s.Nil(any(v)) s.False(ok) s.ErrorIs(sub.Err(), context.Canceled) }) @@ -448,10 +445,7 @@ func (s *BackendBlocksSuite) subscribe( } // requireBlocks ensures that the received block information matches the expected data. -func (s *BackendBlocksSuite) requireBlocks(v interface{}, expectedBlock *flow.Block) { - actualBlock, ok := v.(*flow.Block) - require.True(s.T(), ok, "unexpected response type: %T", v) - +func (s *BackendBlocksSuite) requireBlocks(actualBlock *flow.Block, expectedBlock *flow.Block) { s.Require().Equalf(expectedBlock.Height, actualBlock.Height, "expected block height %d, got %d", expectedBlock.Height, actualBlock.Height) s.Require().Equal(expectedBlock.ID(), actualBlock.ID()) s.Require().Equal(*expectedBlock, *actualBlock) @@ -473,14 +467,14 @@ func (s *BackendBlocksSuite) requireBlocks(v interface{}, expectedBlock *flow.Bl // // Each test case checks for specific error conditions and ensures that the methods responds appropriately. func (s *BackendBlocksSuite) TestSubscribeBlocksHandlesErrors() { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() backend, err := New(s.backendParams(engine.NewBroadcaster())) s.Require().NoError(err) s.Run("returns error if unknown start block id is provided", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlocksFromStartBlockID(subCtx, unittest.IdentifierFixture(), flow.BlockStatusFinalized) @@ -488,7 +482,7 @@ func (s *BackendBlocksSuite) TestSubscribeBlocksHandlesErrors() { }) s.Run("returns error for start height before root height", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) defer subCancel() sub := backend.SubscribeBlocksFromStartHeight(subCtx, s.rootBlock.Height-1, flow.BlockStatusFinalized) @@ -496,7 +490,8 @@ func (s *BackendBlocksSuite) TestSubscribeBlocksHandlesErrors() { }) s.Run("returns error if unknown start height is provided", func() { - subCtx, subCancel := context.WithCancel(ctx) + subCtx, subCancel := context.WithTimeout(ctx, 2*time.Second) + defer subCancel() sub := backend.SubscribeBlocksFromStartHeight(subCtx, s.blocksArray[len(s.blocksArray)-1].Height+10, flow.BlockStatusFinalized) diff --git a/engine/access/rpc/backend/transactions/stream/stream_backend.go b/engine/access/rpc/backend/transactions/stream/stream_backend.go index 65f029a8d4d..f42da24dd50 100644 --- a/engine/access/rpc/backend/transactions/stream/stream_backend.go +++ b/engine/access/rpc/backend/transactions/stream/stream_backend.go @@ -13,10 +13,13 @@ import ( "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/onflow/flow-go/access" + "github.com/onflow/flow-go/engine" txprovider "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/provider" txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/height_source" + "github.com/onflow/flow-go/engine/access/subscription/streamer" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/irrecoverable" @@ -35,18 +38,20 @@ type sendTransaction func(ctx context.Context, tx *flow.TransactionBody) error // It provides functionalities to send transactions, subscribe to transaction status updates, // and handle subscription lifecycles. type TransactionStream struct { - log zerolog.Logger - state protocol.State - subscriptionHandler *subscription.SubscriptionHandler - blockTracker tracker.BlockTracker - sendTransaction sendTransaction + log zerolog.Logger + state protocol.State + blockTracker subscription.BlockTracker + sendTransaction sendTransaction blocks storage.Blocks collections storage.Collections transactions storage.Transactions - txProvider *txprovider.FailoverTransactionProvider - txStatusDeriver *txstatus.TxStatusDeriver + txProvider *txprovider.FailoverTransactionProvider + txStatusDeriver *txstatus.TxStatusDeriver + execDataBroadcaster *engine.Broadcaster + streamOptions *streamer.StreamOptions + endHeight uint64 } var _ access.TransactionStreamAPI = (*TransactionStream)(nil) @@ -54,19 +59,20 @@ var _ access.TransactionStreamAPI = (*TransactionStream)(nil) func NewTransactionStreamBackend( log zerolog.Logger, state protocol.State, - subscriptionHandler *subscription.SubscriptionHandler, - blockTracker tracker.BlockTracker, + blockTracker subscription.BlockTracker, sendTransaction sendTransaction, blocks storage.Blocks, collections storage.Collections, transactions storage.Transactions, txProvider *txprovider.FailoverTransactionProvider, txStatusDeriver *txstatus.TxStatusDeriver, + execDataBroadcaster *engine.Broadcaster, + streamOptions *streamer.StreamOptions, + endHeight uint64, ) *TransactionStream { return &TransactionStream{ log: log, state: state, - subscriptionHandler: subscriptionHandler, blockTracker: blockTracker, sendTransaction: sendTransaction, blocks: blocks, @@ -74,6 +80,9 @@ func NewTransactionStreamBackend( transactions: transactions, txProvider: txProvider, txStatusDeriver: txStatusDeriver, + execDataBroadcaster: execDataBroadcaster, + streamOptions: streamOptions, + endHeight: endHeight, } } @@ -93,10 +102,10 @@ func (t *TransactionStream) SendAndSubscribeTransactionStatuses( ctx context.Context, tx *flow.TransactionBody, requiredEventEncodingVersion entities.EventEncodingVersion, -) subscription.Subscription { +) subscription.Subscription[[]*accessmodel.TransactionResult] { if err := t.sendTransaction(ctx, tx); err != nil { t.log.Debug().Err(err).Str("tx_id", tx.ID().String()).Msg("failed to send transaction") - return subscription.NewFailedSubscription(err, "failed to send transaction") + return subimpl.NewFailedSubscription[[]*accessmodel.TransactionResult](err, "failed to send transaction") } return t.createSubscription(ctx, tx.ID(), tx.ReferenceBlockID, tx.ReferenceBlockID, requiredEventEncodingVersion) @@ -116,12 +125,12 @@ func (t *TransactionStream) SubscribeTransactionStatuses( ctx context.Context, txID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion, -) subscription.Subscription { +) subscription.Subscription[[]*accessmodel.TransactionResult] { header, err := t.state.Sealed().Head() if err != nil { // throw the exception as the node must have the current sealed block in storage irrecoverable.Throw(ctx, fmt.Errorf("failed to lookup sealed block: %w", err)) - return subscription.NewFailedSubscription(err, "failed to lookup sealed block") + return subimpl.NewFailedSubscription[[]*accessmodel.TransactionResult](err, "failed to lookup sealed block") } return t.createSubscription(ctx, txID, header.ID(), flow.ZeroID, requiredEventEncodingVersion) @@ -149,12 +158,12 @@ func (t *TransactionStream) createSubscription( startBlockID flow.Identifier, referenceBlockID flow.Identifier, requiredEventEncodingVersion entities.EventEncodingVersion, -) subscription.Subscription { +) subscription.Subscription[[]*accessmodel.TransactionResult] { // Determine the height of the block to start the subscription from. startHeight, err := t.blockTracker.GetStartHeightFromBlockID(startBlockID) if err != nil { t.log.Debug().Err(err).Str("block_id", startBlockID.String()).Msg("failed to get start height") - return subscription.NewFailedSubscription(err, "failed to get start height") + return subimpl.NewFailedSubscription[[]*accessmodel.TransactionResult](err, "failed to get start height") } txInfo := NewTransactionMetadata( @@ -168,22 +177,34 @@ func (t *TransactionStream) createSubscription( t.txStatusDeriver, ) - return t.subscriptionHandler.Subscribe(ctx, startHeight, t.getTransactionStatusResponse(txInfo, startHeight)) + heightSource := height_source.NewHeightSource( + startHeight, + t.endHeight, + t.buildReadyUpToHeight(startHeight), + t.buildGetTransactionStatusesAtHeight(txInfo, startHeight), + ) + + sub := subimpl.NewSubscription[[]*accessmodel.TransactionResult](t.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + t.log, + t.execDataBroadcaster, + sub, + heightSource, + t.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } -// getTransactionStatusResponse returns a callback function that produces transaction status +// buildGetTransactionStatusesAtHeight returns a callback function that produces transaction status // subscription responses based on new blocks. // The returned callback is not concurrency-safe -func (t *TransactionStream) getTransactionStatusResponse( +func (t *TransactionStream) buildGetTransactionStatusesAtHeight( txInfo *TransactionMetadata, startHeight uint64, -) func(context.Context, uint64) (interface{}, error) { - return func(ctx context.Context, height uint64) (interface{}, error) { - err := t.checkBlockReady(height) - if err != nil { - return nil, err - } - +) subscription.GetItemAtHeightFunc[[]*accessmodel.TransactionResult] { + return func(ctx context.Context, height uint64) ([]*accessmodel.TransactionResult, error) { if txInfo.txResult.IsFinal() { return nil, fmt.Errorf("transaction final status %s already reported: %w", txInfo.txResult.Status.String(), subscription.ErrEndOfData) } @@ -197,7 +218,7 @@ func (t *TransactionStream) getTransactionStatusResponse( // Get old status here, as it could be replaced by status from founded tx result prevTxStatus := txInfo.txResult.Status - if err = txInfo.Refresh(ctx); err != nil { + if err := txInfo.Refresh(ctx); err != nil { if errors.Is(err, subscription.ErrBlockNotReady) { return nil, err } @@ -221,24 +242,23 @@ func hasReachedUnknownStatusLimit(height, startHeight uint64, status flow.Transa return height-startHeight >= TransactionExpiryForUnknownStatus } -// checkBlockReady checks if the given block height is valid and available based on the expected block status. -// Expected errors during normal operation: -// - [subscription.ErrBlockNotReady]: block for the given block height is not available. -func (t *TransactionStream) checkBlockReady(height uint64) error { - // Get the highest available finalized block height - highestHeight, err := t.blockTracker.GetHighestHeight(flow.BlockStatusFinalized) - if err != nil { - return fmt.Errorf("could not get highest height for block %d: %w", height, err) - } +func (t *TransactionStream) buildReadyUpToHeight(height uint64) func() (uint64, error) { + return func() (uint64, error) { + // Get the highest available finalized block height + highestHeight, err := t.blockTracker.GetHighestHeight(flow.BlockStatusFinalized) + if err != nil { + return 0, fmt.Errorf("could not get highest height for block %d: %w", height, err) + } - // Fail early if no block finalized notification has been received for the given height. - // Note: It's possible that the block is locally finalized before the notification is - // received. This ensures a consistent view is available to all streams. - if height > highestHeight { - return fmt.Errorf("block %d is not available yet: %w", height, subscription.ErrBlockNotReady) - } + // Fail early if no block finalized notification has been received for the given height. + // Note: It's possible that the block is locally finalized before the notification is + // received. This ensures a consistent view is available to all streams. + if height > highestHeight { + return 0, fmt.Errorf("block %d is not available yet: %w", height, subscription.ErrBlockNotReady) + } - return nil + return highestHeight, nil + } } // generateResultsStatuses checks if the current result differs from the previous result by more than one step. @@ -255,7 +275,7 @@ func generateResultsStatuses( // If the old and new transaction statuses are still the same, the status change should not be reported, so // return here with no response. if prevTxStatus == txResult.Status { - return nil, nil + return nil, subscription.ErrBlockNotReady } // return immediately if the new status is expired, since it's the last status diff --git a/engine/access/rpc/backend/transactions/stream/stream_backend_test.go b/engine/access/rpc/backend/transactions/stream/stream_backend_test.go index 630a2671480..3e115a6bd95 100644 --- a/engine/access/rpc/backend/transactions/stream/stream_backend_test.go +++ b/engine/access/rpc/backend/transactions/stream/stream_backend_test.go @@ -32,7 +32,8 @@ import ( txstatus "github.com/onflow/flow-go/engine/access/rpc/backend/transactions/status" connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" "github.com/onflow/flow-go/engine/access/subscription" - trackermock "github.com/onflow/flow-go/engine/access/subscription/tracker/mock" + trackermock "github.com/onflow/flow-go/engine/access/subscription/mock" + "github.com/onflow/flow-go/engine/access/subscription/streamer" commonrpc "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/fvm/blueprints" @@ -107,6 +108,9 @@ type TransactionStreamSuite struct { fixedExecutionNodeIDs flow.IdentifierList preferredExecutionNodeIDs flow.IdentifierList + + // dynamic collection availability by txID to avoid fragile mock sequencing + collectionByTx *concurrentmap.Map[flow.Identifier, *flow.LightCollection] } func TestTransactionStatusSuite(t *testing.T) { @@ -155,6 +159,22 @@ func (s *TransactionStreamSuite) SetupTest() { s.preferredExecutionNodeIDs = nil s.initializeBackend() + + // Initialize stateful responder for Collections.LightByTransactionID: + // return ErrNotFound until the transaction's collection is registered in collectionByTx; + // once registered (during addBlockWithTransaction), return the light collection. + if s.collectionByTx == nil { + s.collectionByTx = concurrentmap.New[flow.Identifier, *flow.LightCollection]() + } + // Default responder for polling loop; tolerant to repeated calls. + s.collections.On("LightByTransactionID", mock.AnythingOfType("flow.Identifier")).Return( + func(id flow.Identifier) (*flow.LightCollection, error) { + if col, ok := s.collectionByTx.Get(id); ok { + return col, nil + } + return nil, storage.ErrNotFound + }, + ).Maybe() } // TearDownTest cleans up the db @@ -263,14 +283,6 @@ func (s *TransactionStreamSuite) initializeBackend() { txProvider := provider.NewFailoverTransactionProvider(localTxProvider, execNodeTxProvider) - subscriptionHandler := subscription.NewSubscriptionHandler( - s.log, - s.broadcaster, - subscription.DefaultSendTimeout, - subscription.DefaultResponseLimit, - subscription.DefaultSendBufferSize, - ) - validatorBlocks := validatormock.NewBlocks(s.T()) validatorBlocks. On("HeaderByID", mock.Anything). @@ -329,10 +341,11 @@ func (s *TransactionStreamSuite) initializeBackend() { txBackend, err := transactions.NewTransactionsBackend(txParams) s.Require().NoError(err) + streamOptions := streamer.NewDefaultStreamOptions() + s.txStreamBackend = NewTransactionStreamBackend( s.log, s.state, - subscriptionHandler, s.blockTracker, txBackend.SendTransaction, s.blocks, @@ -340,6 +353,9 @@ func (s *TransactionStreamSuite) initializeBackend() { s.transactions, txProvider, txStatusDeriver, + s.broadcaster, + streamOptions, + 0, ) } @@ -475,6 +491,9 @@ func (s *TransactionStreamSuite) addBlockWithTransaction(transaction *flow.Trans colID := col.ID() guarantee := flow.CollectionGuarantee{CollectionID: colID} light := col.Light() + // Make the collection discoverable by the stateful responder used in tests + s.collectionByTx.Add(transaction.ID(), light) + s.sealedBlock = s.finalizedBlock s.addNewFinalizedBlock(s.sealedBlock.ToHeader(), true, func(block *flow.Block) { var err error @@ -486,24 +505,24 @@ func (s *TransactionStreamSuite) addBlockWithTransaction(transaction *flow.Trans ) require.NoError(s.T(), err) s.collections.On("LightByID", colID).Return(light, nil).Maybe() - s.collections.On("LightByTransactionID", transaction.ID()).Return(light, nil) - s.blocks.On("ByCollectionID", colID).Return(block, nil) + s.collections.On("LightByTransactionID", transaction.ID()).Return(light, nil).Maybe() + s.blocks.On("ByCollectionID", colID).Return(block, nil).Maybe() }) } // Create a special common function to read subscription messages from the channel and check converting it to transaction info // and check results for correctness -func (s *TransactionStreamSuite) checkNewSubscriptionMessage(sub subscription.Subscription, txId flow.Identifier, expectedTxStatuses []flow.TransactionStatus) { +func (s *TransactionStreamSuite) checkNewSubscriptionMessage( + sub subscription.Subscription[[]*accessmodel.TransactionResult], + txId flow.Identifier, + expectedTxStatuses []flow.TransactionStatus, +) { unittest.RequireReturnsBefore(s.T(), func() { - v, ok := <-sub.Channel() + txResults, ok := <-sub.Channel() require.True(s.T(), ok, "channel closed while waiting for transaction info:\n\t- txID %x\n\t- blockID: %x \n\t- err: %v", txId, s.finalizedBlock.ID(), sub.Err()) - txResults, ok := v.([]*accessmodel.TransactionResult) - require.True(s.T(), ok, "unexpected response type: %T", v) - require.Len(s.T(), txResults, len(expectedTxStatuses)) - for i, expectedTxStatus := range expectedTxStatuses { result := txResults[i] assert.Equal(s.T(), txId, result.TransactionID) @@ -514,7 +533,7 @@ func (s *TransactionStreamSuite) checkNewSubscriptionMessage(sub subscription.Su } // checkGracefulShutdown ensures the provided subscription shuts down gracefully within a specified timeout duration. -func (s *TransactionStreamSuite) checkGracefulShutdown(sub subscription.Subscription) { +func (s *TransactionStreamSuite) checkGracefulShutdown(sub subscription.Subscription[[]*accessmodel.TransactionResult]) { // Ensure subscription shuts down gracefully unittest.RequireReturnsBefore(s.T(), func() { <-sub.Channel() @@ -525,7 +544,7 @@ func (s *TransactionStreamSuite) checkGracefulShutdown(sub subscription.Subscrip // TestSendAndSubscribeTransactionStatusHappyCase tests the functionality of the SubscribeTransactionStatusesFromStartBlockID method in the Backend. // It covers the emulation of transaction stages from pending to sealed, and receiving status updates. func (s *TransactionStreamSuite) TestSendAndSubscribeTransactionStatusHappyCase() { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() s.initializeHappyCaseMockInstructions() @@ -534,8 +553,6 @@ func (s *TransactionStreamSuite) TestSendAndSubscribeTransactionStatusHappyCase( transaction := s.createSendTransaction() txId := transaction.ID() - s.collections.On("LightByTransactionID", txId).Return(nil, storage.ErrNotFound).Once() - hasTransactionResultInStorage := false s.mockTransactionResult(&txId, &hasTransactionResultInStorage) @@ -586,7 +603,10 @@ func (s *TransactionStreamSuite) TestSendAndSubscribeTransactionStatusExpired() // Generate sent transaction with ref block of the current finalized block transaction := s.createSendTransaction() txId := transaction.ID() - s.collections.On("LightByTransactionID", txId).Return(nil, storage.ErrNotFound) + s.collections. + On("LightByTransactionID", txId). + Return(nil, storage.ErrNotFound). + Maybe() // Subscribe to transaction status and receive the first message with pending status sub := s.txStreamBackend.SendAndSubscribeTransactionStatuses(ctx, &transaction, entities.EventEncodingVersion_CCF_V0) @@ -621,7 +641,6 @@ func (s *TransactionStreamSuite) TestSubscribeTransactionStatusWithCurrentPendin transaction := s.createSendTransaction() txId := transaction.ID() - s.collections.On("LightByTransactionID", txId).Return(nil, storage.ErrNotFound).Once() hasTransactionResultInStorage := false s.mockTransactionResult(&txId, &hasTransactionResultInStorage) diff --git a/engine/access/rpc/handler.go b/engine/access/rpc/handler.go index eb48ef50cb6..d60912f6990 100644 --- a/engine/access/rpc/handler.go +++ b/engine/access/rpc/handler.go @@ -11,6 +11,8 @@ import ( accessproto "github.com/onflow/flow/protobuf/go/flow/access" "github.com/onflow/flow/protobuf/go/flow/entities" + "go.uber.org/atomic" + "github.com/onflow/flow-go/access" "github.com/onflow/flow-go/consensus/hotstuff" "github.com/onflow/flow-go/consensus/hotstuff/signature" @@ -26,7 +28,9 @@ import ( ) type Handler struct { - subscription.StreamingData + MaxStreams int32 + StreamCount atomic.Int32 + api access.API chain flow.Chain signerIndicesDecoder hotstuff.BlockSignerDecoder @@ -61,7 +65,7 @@ func NewHandler( options ...HandlerOption, ) *Handler { h := &Handler{ - StreamingData: subscription.NewStreamingData(maxStreams), + MaxStreams: int32(maxStreams), api: api, chain: chain, finalizedHeaderCache: finalizedHeader, @@ -1620,7 +1624,7 @@ func checkBlockStatus(blockStatus flow.BlockStatus) error { // // Expected errors during normal operation: // - codes.Internal: If the subscription encounters an error or gets an unexpected response. -func HandleRPCSubscription[T any](sub subscription.Subscription, handleResponse func(resp T) error) error { +func HandleRPCSubscription[T any](sub subscription.Subscription[T], handleResponse func(resp T) error) error { err := subscription.HandleSubscription(sub, handleResponse) if err != nil { return rpc.ConvertError(err, "handle subscription error", codes.Internal) diff --git a/engine/access/state_stream/backend/backend.go b/engine/access/state_stream/backend/backend.go index b32692af6e0..c4e740a3fb1 100644 --- a/engine/access/state_stream/backend/backend.go +++ b/engine/access/state_stream/backend/backend.go @@ -10,10 +10,11 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/engine/access/index" "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/streamer" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/execution" "github.com/onflow/flow-go/module/executiondatasync/execution_data" @@ -64,7 +65,7 @@ type Config struct { type GetExecutionDataFunc func(context.Context, uint64) (*execution_data.BlockExecutionDataEntity, error) type StateStreamBackend struct { - tracker.ExecutionDataTracker + subscription.ExecutionDataTracker ExecutionDataBackend EventsBackend @@ -94,10 +95,11 @@ func New( eventsIndex *index.EventsIndex, useEventsIndex bool, registerIDsRequestLimit int, - subscriptionHandler *subscription.SubscriptionHandler, - executionDataTracker tracker.ExecutionDataTracker, + executionDataTracker subscription.ExecutionDataTracker, executionResultProvider optimistic_sync.ExecutionResultInfoProvider, executionStateCache optimistic_sync.ExecutionStateCache, + broadcaster *engine.Broadcaster, + streamOptions *streamer.StreamOptions, ) (*StateStreamBackend, error) { logger := log.With().Str("module", "state_stream_api").Logger() @@ -116,13 +118,15 @@ func New( } b.ExecutionDataBackend = ExecutionDataBackend{ - log: logger, - headers: headers, - subscriptionHandler: subscriptionHandler, - getExecutionData: b.getExecutionData, - executionDataTracker: executionDataTracker, - executionResultProvider: executionResultProvider, - executionStateCache: executionStateCache, + log: logger, + headers: headers, + getExecutionData: b.getExecutionData, + executionDataTracker: executionDataTracker, + executionDataBroadcaster: broadcaster, + streamOptions: streamOptions, + endHeight: 0, // execution data endpoints are unbounded streams + executionResultProvider: executionResultProvider, + executionStateCache: executionStateCache, } eventsProvider := EventsProvider{ @@ -134,17 +138,21 @@ func New( } b.EventsBackend = EventsBackend{ - log: logger, - subscriptionHandler: subscriptionHandler, - executionDataTracker: executionDataTracker, - eventsProvider: eventsProvider, + log: logger, + executionDataTracker: executionDataTracker, + eventsProvider: eventsProvider, + executionDataBroadcaster: broadcaster, + streamOptions: streamOptions, + endHeight: 0, // events endpoints are unbounded streams } b.AccountStatusesBackend = AccountStatusesBackend{ log: logger, - subscriptionHandler: subscriptionHandler, executionDataTracker: b.ExecutionDataTracker, eventsProvider: eventsProvider, + execDataBroadcaster: broadcaster, + streamOptions: streamOptions, + endHeight: 0, // account statues endpoints are unbounded streams } return b, nil diff --git a/engine/access/state_stream/backend/backend_account_statuses.go b/engine/access/state_stream/backend/backend_account_statuses.go index d168e7ddd96..361c180643a 100644 --- a/engine/access/state_stream/backend/backend_account_statuses.go +++ b/engine/access/state_stream/backend/backend_account_statuses.go @@ -6,37 +6,27 @@ import ( "github.com/rs/zerolog" + "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/height_source" + "github.com/onflow/flow-go/engine/access/subscription/streamer" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" "github.com/onflow/flow-go/fvm/errors" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/storage" ) -type AccountStatusesResponse struct { - BlockID flow.Identifier - Height uint64 - AccountEvents map[string]flow.EventsList -} - // AccountStatusesBackend is a struct representing a backend implementation for subscribing to account statuses changes. type AccountStatusesBackend struct { - log zerolog.Logger - subscriptionHandler *subscription.SubscriptionHandler + log zerolog.Logger - executionDataTracker tracker.ExecutionDataTracker + executionDataTracker subscription.ExecutionDataTracker eventsProvider EventsProvider -} - -// subscribe creates and returns a subscription to receive account status updates starting from the specified height. -func (b *AccountStatusesBackend) subscribe( - ctx context.Context, - nextHeight uint64, - filter state_stream.AccountStatusFilter, -) subscription.Subscription { - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getAccountStatusResponseFactory(filter)) + execDataBroadcaster *engine.Broadcaster + streamOptions *streamer.StreamOptions + endHeight uint64 } // SubscribeAccountStatusesFromStartBlockID subscribes to the streaming of account status changes starting from @@ -48,12 +38,12 @@ func (b *AccountStatusesBackend) SubscribeAccountStatusesFromStartBlockID( ctx context.Context, startBlockID flow.Identifier, filter state_stream.AccountStatusFilter, -) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromBlockID(startBlockID) +) subscription.Subscription[*state_stream.AccountStatusesResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromBlockID(startBlockID) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block id") + return subimpl.NewFailedSubscription[*state_stream.AccountStatusesResponse](err, "could not get start height from block id") } - return b.subscribe(ctx, nextHeight, filter) + return b.subscribe(ctx, startHeight, filter) } // SubscribeAccountStatusesFromStartHeight subscribes to the streaming of account status changes starting from @@ -65,12 +55,12 @@ func (b *AccountStatusesBackend) SubscribeAccountStatusesFromStartHeight( ctx context.Context, startHeight uint64, filter state_stream.AccountStatusFilter, -) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromHeight(startHeight) +) subscription.Subscription[*state_stream.AccountStatusesResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromHeight(startHeight) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block height") + return subimpl.NewFailedSubscription[*state_stream.AccountStatusesResponse](err, "could not get start height from block height") } - return b.subscribe(ctx, nextHeight, filter) + return b.subscribe(ctx, startHeight, filter) } // SubscribeAccountStatusesFromLatestBlock subscribes to the streaming of account status changes starting from a @@ -80,23 +70,49 @@ func (b *AccountStatusesBackend) SubscribeAccountStatusesFromStartHeight( func (b *AccountStatusesBackend) SubscribeAccountStatusesFromLatestBlock( ctx context.Context, filter state_stream.AccountStatusFilter, -) subscription.Subscription { +) subscription.Subscription[*state_stream.AccountStatusesResponse] { nextHeight, err := b.executionDataTracker.GetStartHeightFromLatest(ctx) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from latest") + return subimpl.NewFailedSubscription[*state_stream.AccountStatusesResponse](err, "could not get start height from latest") } return b.subscribe(ctx, nextHeight, filter) } -// getAccountStatusResponseFactory returns a function that returns the account statuses response for a given height. +// subscribe creates and returns a subscription to receive account status updates starting from the specified height. +func (b *AccountStatusesBackend) subscribe( + ctx context.Context, + startHeight uint64, + filter state_stream.AccountStatusFilter, +) subscription.Subscription[*state_stream.AccountStatusesResponse] { + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.buildGetAccountStatutesAtHeight(filter), + ) + + sub := subimpl.NewSubscription[*state_stream.AccountStatusesResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.execDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub +} + +// buildGetAccountStatutesAtHeight returns a function that returns the account statuses response for a given height. // // Errors: // - subscription.ErrBlockNotReady: If block header for the specified block height is not found. // - error: An error, if any, encountered during getting events from storage or execution data. -func (b *AccountStatusesBackend) getAccountStatusResponseFactory( +func (b *AccountStatusesBackend) buildGetAccountStatutesAtHeight( filter state_stream.AccountStatusFilter, -) subscription.GetDataByHeightFunc { - return func(ctx context.Context, height uint64) (interface{}, error) { +) subscription.GetItemAtHeightFunc[*state_stream.AccountStatusesResponse] { + return func(ctx context.Context, height uint64) (*state_stream.AccountStatusesResponse, error) { eventsResponse, err := b.eventsProvider.GetAllEventsResponse(ctx, height) if err != nil { if errors.Is(err, storage.ErrNotFound) || @@ -108,10 +124,14 @@ func (b *AccountStatusesBackend) getAccountStatusResponseFactory( filteredProtocolEvents := filter.Filter(eventsResponse.Events) allAccountProtocolEvents := filter.GroupCoreEventsByAccountAddress(filteredProtocolEvents, b.log) - return &AccountStatusesResponse{ + return &state_stream.AccountStatusesResponse{ BlockID: eventsResponse.BlockID, Height: eventsResponse.Height, AccountEvents: allAccountProtocolEvents, }, nil } } + +func (b *AccountStatusesBackend) readyUpToHeight() (uint64, error) { + return b.executionDataTracker.GetHighestHeight(), nil +} diff --git a/engine/access/state_stream/backend/backend_account_statuses_test.go b/engine/access/state_stream/backend/backend_account_statuses_test.go index 65d3e350aa4..800b7f467d5 100644 --- a/engine/access/state_stream/backend/backend_account_statuses_test.go +++ b/engine/access/state_stream/backend/backend_account_statuses_test.go @@ -267,7 +267,7 @@ func (s *BackendAccountStatusesSuite) generateFiltersForTestCases(baseTests []te // For each test case, it simulates backfill blocks and verifies the expected account events for each block. // It also ensures that the subscription shuts down gracefully after completing the test cases. func (s *BackendAccountStatusesSuite) subscribeToAccountStatuses( - subscribeFn func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription, + subscribeFn func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse], tests []testType, ) { ctx, cancel := context.WithCancel(context.Background()) @@ -308,7 +308,7 @@ func (s *BackendAccountStatusesSuite) subscribeToAccountStatuses( v, ok := <-sub.Channel() require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v: err: %v", b.Height, b.ID(), sub.Err()) - expected := &AccountStatusesResponse{ + expected := &state_stream.AccountStatusesResponse{ BlockID: b.ID(), Height: b.Height, AccountEvents: expectedEvents, @@ -346,7 +346,7 @@ func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromStartBlock return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID) }, nil) - call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription { + call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { return s.backend.SubscribeAccountStatusesFromStartBlockID(ctx, startValue.(flow.Identifier), filter) } @@ -362,7 +362,7 @@ func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromStartHeigh return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight) }, nil) - call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription { + call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { return s.backend.SubscribeAccountStatusesFromStartHeight(ctx, startValue.(uint64), filter) } @@ -378,7 +378,7 @@ func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromLatestBloc return s.executionDataTrackerReal.GetStartHeightFromLatest(ctx) }, nil) - call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription { + call := func(ctx context.Context, startValue interface{}, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { return s.backend.SubscribeAccountStatusesFromLatestBlock(ctx, filter) } @@ -386,17 +386,17 @@ func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromLatestBloc } // requireEventsResponse ensures that the received event information matches the expected data. -func (s *BackendAccountStatusesSuite) requireEventsResponse(v interface{}, expected *AccountStatusesResponse) { - actual, ok := v.(*AccountStatusesResponse) - require.True(s.T(), ok, "unexpected response type: %T", v) - +func (s *BackendAccountStatusesSuite) requireEventsResponse( + actual *state_stream.AccountStatusesResponse, + expected *state_stream.AccountStatusesResponse, +) { assert.Equal(s.T(), expected.BlockID, actual.BlockID) assert.Equal(s.T(), expected.Height, actual.Height) assert.Equal(s.T(), expected.AccountEvents, actual.AccountEvents) } -// TestSubscribeAccountStatusesFromSporkRootBlock tests that events subscriptions starting from the spork -// root block return an empty result for the root block. +// TestSubscribeAccountStatusesFromSporkRootBlock verifies that when subscribing from the spork +// root block, the stream starts from the first non-root block with actual events (no empty root response). func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromSporkRootBlock() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -404,29 +404,27 @@ func (s *BackendAccountStatusesSuite) TestSubscribeAccountStatusesFromSporkRootB // setup the backend to have 1 available block s.highestBlockHeader = s.blocks[0].ToHeader() - rootEventResponse := &AccountStatusesResponse{ - BlockID: s.rootBlock.ID(), - Height: s.rootBlock.Height, - AccountEvents: map[string]flow.EventsList{}, - } - - filter, err := state_stream.NewAccountStatusFilter(state_stream.DefaultEventFilterConfig, chainID.Chain(), []string{}, []string{}) + filter, err := state_stream.NewAccountStatusFilter( + state_stream.DefaultEventFilterConfig, + chainID.Chain(), + []string{}, + []string{}, + ) require.NoError(s.T(), err) expectedEvents := s.expectedAccountStatuses(s.blocks[0].ID(), filter) - firstEventResponse := &AccountStatusesResponse{ + firstEventResponse := &state_stream.AccountStatusesResponse{ BlockID: s.blocks[0].ID(), Height: s.blocks[0].Height, AccountEvents: expectedEvents, } - assertSubscriptionResponses := func(sub subscription.Subscription, cancel context.CancelFunc) { - // the first response should have details from the root block and no events + assertSubscriptionResponses := func( + sub subscription.Subscription[*state_stream.AccountStatusesResponse], + cancel context.CancelFunc, + ) { + // the first response should have details from the first block and its events resp := <-sub.Channel() - s.requireEventsResponse(resp, rootEventResponse) - - // the second response should have details from the first block and its events - resp = <-sub.Channel() s.requireEventsResponse(resp, firstEventResponse) cancel() diff --git a/engine/access/state_stream/backend/backend_events.go b/engine/access/state_stream/backend/backend_events.go index e4c94ffc5dd..bb71134fe93 100644 --- a/engine/access/state_stream/backend/backend_events.go +++ b/engine/access/state_stream/backend/backend_events.go @@ -6,9 +6,12 @@ import ( "github.com/rs/zerolog" + "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/height_source" + "github.com/onflow/flow-go/engine/access/subscription/streamer" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" "github.com/onflow/flow-go/fvm/errors" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/storage" @@ -17,9 +20,11 @@ import ( type EventsBackend struct { log zerolog.Logger - subscriptionHandler *subscription.SubscriptionHandler - executionDataTracker tracker.ExecutionDataTracker - eventsProvider EventsProvider + executionDataTracker subscription.ExecutionDataTracker + eventsProvider EventsProvider + executionDataBroadcaster *engine.Broadcaster + streamOptions *streamer.StreamOptions + endHeight uint64 } // SubscribeEvents is deprecated and will be removed in a future version. @@ -44,13 +49,35 @@ type EventsBackend struct { // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEvents will return a failed subscription. -func (b *EventsBackend) SubscribeEvents(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeight(ctx, startBlockID, startHeight) +func (b *EventsBackend) SubscribeEvents( + ctx context.Context, + startBlockID flow.Identifier, + startHeight uint64, + filter state_stream.EventFilter, +) subscription.Subscription[*state_stream.EventsResponse] { + startHeight, err := b.executionDataTracker.GetStartHeight(ctx, startBlockID, startHeight) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height") + return subimpl.NewFailedSubscription[*state_stream.EventsResponse](err, "could not get start height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponseFactory(filter)) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.buildGetEventsAtHeight(filter), + ) + + sub := subimpl.NewSubscription[*state_stream.EventsResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeEventsFromStartBlockID streams events starting at the specified block ID, @@ -68,13 +95,34 @@ func (b *EventsBackend) SubscribeEvents(ctx context.Context, startBlockID flow.I // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEventsFromStartBlockID will return a failed subscription. -func (b *EventsBackend) SubscribeEventsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.EventFilter) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromBlockID(startBlockID) +func (b *EventsBackend) SubscribeEventsFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + filter state_stream.EventFilter, +) subscription.Subscription[*state_stream.EventsResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromBlockID(startBlockID) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block id") + return subimpl.NewFailedSubscription[*state_stream.EventsResponse](err, "could not get start height from block id") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponseFactory(filter)) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.buildGetEventsAtHeight(filter), + ) + + sub := subimpl.NewSubscription[*state_stream.EventsResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeEventsFromStartHeight streams events starting at the specified block height, @@ -92,13 +140,34 @@ func (b *EventsBackend) SubscribeEventsFromStartBlockID(ctx context.Context, sta // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEventsFromStartHeight will return a failed subscription. -func (b *EventsBackend) SubscribeEventsFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromHeight(startHeight) +func (b *EventsBackend) SubscribeEventsFromStartHeight( + ctx context.Context, + startHeight uint64, + filter state_stream.EventFilter, +) subscription.Subscription[*state_stream.EventsResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromHeight(startHeight) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block height") + return subimpl.NewFailedSubscription[*state_stream.EventsResponse](err, "could not get start height from block height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponseFactory(filter)) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.buildGetEventsAtHeight(filter), + ) + + sub := subimpl.NewSubscription[*state_stream.EventsResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeEventsFromLatest subscribes to events starting at the latest sealed block, @@ -115,24 +184,44 @@ func (b *EventsBackend) SubscribeEventsFromStartHeight(ctx context.Context, star // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEventsFromLatest will return a failed subscription. -func (b *EventsBackend) SubscribeEventsFromLatest(ctx context.Context, filter state_stream.EventFilter) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromLatest(ctx) +func (b *EventsBackend) SubscribeEventsFromLatest( + ctx context.Context, + filter state_stream.EventFilter, +) subscription.Subscription[*state_stream.EventsResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromLatest(ctx) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start height from block height") + return subimpl.NewFailedSubscription[*state_stream.EventsResponse](err, "could not get start height from block height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponseFactory(filter)) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.buildGetEventsAtHeight(filter), + ) + + sub := subimpl.NewSubscription[*state_stream.EventsResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } -// getResponseFactory returns a function that retrieves the event response for a given height. +// buildGetEventsAtHeight returns a function that retrieves the event response for a given height. // // Parameters: // - filter: The event filter used to filter events. // // Expected errors during normal operation: // - subscription.ErrBlockNotReady: execution data for the given block height is not available. -func (b *EventsBackend) getResponseFactory(filter state_stream.EventFilter) subscription.GetDataByHeightFunc { - return func(ctx context.Context, height uint64) (response interface{}, err error) { +func (b *EventsBackend) buildGetEventsAtHeight(filter state_stream.EventFilter) subscription.GetItemAtHeightFunc[*state_stream.EventsResponse] { + return func(ctx context.Context, height uint64) (response *state_stream.EventsResponse, err error) { eventsResponse, err := b.eventsProvider.GetAllEventsResponse(ctx, height) if err != nil { if errors.Is(err, storage.ErrNotFound) || @@ -147,3 +236,7 @@ func (b *EventsBackend) getResponseFactory(filter state_stream.EventFilter) subs return eventsResponse, nil } } + +func (b *EventsBackend) readyUpToHeight() (uint64, error) { + return b.executionDataTracker.GetHighestHeight(), nil +} diff --git a/engine/access/state_stream/backend/backend_events_test.go b/engine/access/state_stream/backend/backend_events_test.go index 36e62e96807..830ac0488b4 100644 --- a/engine/access/state_stream/backend/backend_events_test.go +++ b/engine/access/state_stream/backend/backend_events_test.go @@ -210,11 +210,16 @@ func (s *BackendEventsSuite) runTestSubscribeEvents() { }, } - call := func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription { + call := func( + ctx context.Context, + startBlockID flow.Identifier, + startHeight uint64, + filter state_stream.EventFilter, + ) subscription.Subscription[*state_stream.EventsResponse] { return s.backend.SubscribeEvents(ctx, startBlockID, startHeight, filter) } - s.subscribe(call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) + subscribe(s, call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) } // runTestSubscribeEventsFromStartBlockID runs the test suite for SubscribeEventsFromStartBlockID subscription @@ -244,11 +249,16 @@ func (s *BackendEventsSuite) runTestSubscribeEventsFromStartBlockID() { return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID) }, nil) - call := func(ctx context.Context, startBlockID flow.Identifier, _ uint64, filter state_stream.EventFilter) subscription.Subscription { + call := func( + ctx context.Context, + startBlockID flow.Identifier, + _ uint64, + filter state_stream.EventFilter, + ) subscription.Subscription[*state_stream.EventsResponse] { return s.backend.SubscribeEventsFromStartBlockID(ctx, startBlockID, filter) } - s.subscribe(call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) + subscribe(s, call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) } // runTestSubscribeEventsFromStartHeight runs the test suite for SubscribeEventsFromStartHeight subscription @@ -278,11 +288,16 @@ func (s *BackendEventsSuite) runTestSubscribeEventsFromStartHeight() { return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight) }, nil) - call := func(ctx context.Context, _ flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription { + call := func( + ctx context.Context, + _ flow.Identifier, + startHeight uint64, + filter state_stream.EventFilter, + ) subscription.Subscription[*state_stream.EventsResponse] { return s.backend.SubscribeEventsFromStartHeight(ctx, startHeight, filter) } - s.subscribe(call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) + subscribe(s, call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) } // runTestSubscribeEventsFromLatest runs the test suite for SubscribeEventsFromLatest subscription @@ -309,11 +324,16 @@ func (s *BackendEventsSuite) runTestSubscribeEventsFromLatest() { return s.executionDataTrackerReal.GetStartHeightFromLatest(ctx) }, nil) - call := func(ctx context.Context, _ flow.Identifier, _ uint64, filter state_stream.EventFilter) subscription.Subscription { + call := func( + ctx context.Context, + _ flow.Identifier, + _ uint64, + filter state_stream.EventFilter, + ) subscription.Subscription[*state_stream.EventsResponse] { return s.backend.SubscribeEventsFromLatest(ctx, filter) } - s.subscribe(call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) + subscribe(s, call, s.requireEventsResponse, s.setupFilterForTestCases(tests)) } // subscribe is a helper function to run test scenarios for event subscription in the BackendEventsSuite. @@ -341,9 +361,10 @@ func (s *BackendEventsSuite) runTestSubscribeEventsFromLatest() { // 6. Simulates the reception of new blocks and consumes them from the subscription channel. // 7. Ensures that there are no new messages waiting after all blocks have been processed. // 8. Cancels the subscription and ensures it shuts down gracefully. -func (s *BackendEventsSuite) subscribe( - subscribeFn func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription, - requireFn func(interface{}, *EventsResponse), +func subscribe[T any]( + s *BackendEventsSuite, + subscribeFn func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription[T], + requireFn func(T, *state_stream.EventsResponse), tests []eventsTestType, ) { ctx, cancel := context.WithCancel(context.Background()) @@ -391,7 +412,7 @@ func (s *BackendEventsSuite) subscribe( v, ok := <-sub.Channel() require.True(s.T(), ok, "channel closed while waiting for exec data for block %x %v: err: %v", b.Height, b.ID(), sub.Err()) - expected := &EventsResponse{ + expected := &state_stream.EventsResponse{ BlockID: b.ID(), Height: b.Height, Events: expectedEvents, @@ -422,18 +443,15 @@ func (s *BackendEventsSuite) subscribe( } // requireEventsResponse ensures that the received event information matches the expected data. -func (s *BackendEventsSuite) requireEventsResponse(v interface{}, expected *EventsResponse) { - actual, ok := v.(*EventsResponse) - require.True(s.T(), ok, "unexpected response type: %T", v) - +func (s *BackendEventsSuite) requireEventsResponse(actual *state_stream.EventsResponse, expected *state_stream.EventsResponse) { assert.Equal(s.T(), expected.BlockID, actual.BlockID) assert.Equal(s.T(), expected.Height, actual.Height) assert.Equal(s.T(), expected.Events, actual.Events) assert.Equal(s.T(), expected.BlockTimestamp, actual.BlockTimestamp) } -// TestSubscribeEventsFromSporkRootBlock tests that events subscriptions starting from the spork -// root block return an empty result for the root block. +// TestSubscribeEventsFromSporkRootBlock verifies that when subscribing from the spork +// root block, the stream starts from the first non-root block with actual events (no empty root response). func (s *BackendEventsSuite) TestSubscribeEventsFromSporkRootBlock() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -441,26 +459,19 @@ func (s *BackendEventsSuite) TestSubscribeEventsFromSporkRootBlock() { // setup the backend to have 1 available block s.highestBlockHeader = s.blocks[0].ToHeader() - rootEventResponse := &EventsResponse{ - BlockID: s.rootBlock.ID(), - Height: s.rootBlock.Height, - BlockTimestamp: time.UnixMilli(int64(s.rootBlock.Timestamp)).UTC(), - } - - firstEventResponse := &EventsResponse{ + firstEventResponse := &state_stream.EventsResponse{ BlockID: s.blocks[0].ID(), Height: s.blocks[0].Height, BlockTimestamp: time.UnixMilli(int64(s.blocks[0].Timestamp)).UTC(), Events: flow.EventsList(s.blockEvents[s.blocks[0].ID()]), } - assertSubscriptionResponses := func(sub subscription.Subscription, cancel context.CancelFunc) { - // the first response should have details from the root block and no events + assertSubscriptionResponses := func( + sub subscription.Subscription[*state_stream.EventsResponse], + cancel context.CancelFunc, + ) { + // the first response should have details from the first non-root block and its events resp := <-sub.Channel() - s.requireEventsResponse(resp, rootEventResponse) - - // the second response should have details from the first block and its events - resp = <-sub.Channel() s.requireEventsResponse(resp, firstEventResponse) cancel() diff --git a/engine/access/state_stream/backend/backend_executiondata.go b/engine/access/state_stream/backend/backend_executiondata.go index 7c9d2fd49df..4da104d6896 100644 --- a/engine/access/state_stream/backend/backend_executiondata.go +++ b/engine/access/state_stream/backend/backend_executiondata.go @@ -4,14 +4,17 @@ import ( "context" "errors" "fmt" - "time" "github.com/rs/zerolog" "github.com/onflow/flow-go/access" + "github.com/onflow/flow-go/engine" "github.com/onflow/flow-go/engine/access/rpc/backend/common" + "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" - "github.com/onflow/flow-go/engine/access/subscription/tracker" + "github.com/onflow/flow-go/engine/access/subscription/height_source" + "github.com/onflow/flow-go/engine/access/subscription/streamer" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/executiondatasync/execution_data" @@ -19,13 +22,6 @@ import ( "github.com/onflow/flow-go/storage" ) -// ExecutionDataResponse bundles the execution data returned for a single block. -type ExecutionDataResponse struct { - Height uint64 - ExecutionData *execution_data.BlockExecutionData - BlockTimestamp time.Time -} - // ExecutionDataBackend exposes read-only access to execution data. type ExecutionDataBackend struct { log zerolog.Logger @@ -33,8 +29,10 @@ type ExecutionDataBackend struct { getExecutionData GetExecutionDataFunc - subscriptionHandler *subscription.SubscriptionHandler - executionDataTracker tracker.ExecutionDataTracker + executionDataTracker subscription.ExecutionDataTracker + executionDataBroadcaster *engine.Broadcaster + streamOptions *streamer.StreamOptions + endHeight uint64 executionResultProvider optimistic_sync.ExecutionResultInfoProvider executionStateCache optimistic_sync.ExecutionStateCache @@ -111,13 +109,34 @@ func (b *ExecutionDataBackend) GetExecutionDataByBlockID( // - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. // // If invalid parameters are provided, failed subscription will be returned. -func (b *ExecutionDataBackend) SubscribeExecutionData(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeight(ctx, startBlockID, startHeight) +func (b *ExecutionDataBackend) SubscribeExecutionData( + ctx context.Context, + startBlockID flow.Identifier, + startHeight uint64, +) subscription.Subscription[*state_stream.ExecutionDataResponse] { + startHeight, err := b.executionDataTracker.GetStartHeight(ctx, startBlockID, startHeight) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start block height") + return subimpl.NewFailedSubscription[*state_stream.ExecutionDataResponse](err, "could not get start block height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponse) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.getExecutionDataAtHeight, + ) + + sub := subimpl.NewSubscription[*state_stream.ExecutionDataResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeExecutionDataFromStartBlockID streams execution data for all blocks starting at the specified block ID @@ -129,13 +148,33 @@ func (b *ExecutionDataBackend) SubscribeExecutionData(ctx context.Context, start // - startBlockID: The identifier of the starting block. // // If invalid parameters are provided, failed subscription will be returned. -func (b *ExecutionDataBackend) SubscribeExecutionDataFromStartBlockID(ctx context.Context, startBlockID flow.Identifier) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromBlockID(startBlockID) +func (b *ExecutionDataBackend) SubscribeExecutionDataFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, +) subscription.Subscription[*state_stream.ExecutionDataResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromBlockID(startBlockID) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start block height") + return subimpl.NewFailedSubscription[*state_stream.ExecutionDataResponse](err, "could not get start block height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponse) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.getExecutionDataAtHeight, + ) + + sub := subimpl.NewSubscription[*state_stream.ExecutionDataResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeExecutionDataFromStartBlockHeight streams execution data for all blocks starting at the specified block height @@ -147,13 +186,33 @@ func (b *ExecutionDataBackend) SubscribeExecutionDataFromStartBlockID(ctx contex // - startHeight: The height of the starting block. // // If invalid parameters are provided, failed subscription will be returned. -func (b *ExecutionDataBackend) SubscribeExecutionDataFromStartBlockHeight(ctx context.Context, startBlockHeight uint64) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromHeight(startBlockHeight) +func (b *ExecutionDataBackend) SubscribeExecutionDataFromStartBlockHeight( + ctx context.Context, + startBlockHeight uint64, +) subscription.Subscription[*state_stream.ExecutionDataResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromHeight(startBlockHeight) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start block height") + return subimpl.NewFailedSubscription[*state_stream.ExecutionDataResponse](err, "could not get start block height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponse) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.getExecutionDataAtHeight, + ) + + sub := subimpl.NewSubscription[*state_stream.ExecutionDataResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } // SubscribeExecutionDataFromLatest streams execution data starting at the latest block. @@ -164,23 +223,44 @@ func (b *ExecutionDataBackend) SubscribeExecutionDataFromStartBlockHeight(ctx co // - ctx: Context for the operation. // // If invalid parameters are provided, failed subscription will be returned. -func (b *ExecutionDataBackend) SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription { - nextHeight, err := b.executionDataTracker.GetStartHeightFromLatest(ctx) +func (b *ExecutionDataBackend) SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription[*state_stream.ExecutionDataResponse] { + startHeight, err := b.executionDataTracker.GetStartHeightFromLatest(ctx) if err != nil { - return subscription.NewFailedSubscription(err, "could not get start block height") + return subimpl.NewFailedSubscription[*state_stream.ExecutionDataResponse](err, "could not get start block height") } - return b.subscriptionHandler.Subscribe(ctx, nextHeight, b.getResponse) + heightSource := height_source.NewHeightSource( + startHeight, + b.endHeight, + b.readyUpToHeight, + b.getExecutionDataAtHeight, + ) + + sub := subimpl.NewSubscription[*state_stream.ExecutionDataResponse](b.streamOptions.SendBufferSize) + streamer := streamer.NewHeightBasedStreamer( + b.log, + b.executionDataBroadcaster, + sub, + heightSource, + b.streamOptions, + ) + go streamer.Stream(ctx) + + return sub } -func (b *ExecutionDataBackend) getResponse(ctx context.Context, height uint64) (interface{}, error) { +func (b *ExecutionDataBackend) getExecutionDataAtHeight(ctx context.Context, height uint64) (*state_stream.ExecutionDataResponse, error) { executionData, err := b.getExecutionData(ctx, height) if err != nil { return nil, fmt.Errorf("could not get execution data for block %d: %w", height, err) } - return &ExecutionDataResponse{ + return &state_stream.ExecutionDataResponse{ Height: height, ExecutionData: executionData.BlockExecutionData, }, nil } + +func (b *ExecutionDataBackend) readyUpToHeight() (uint64, error) { + return b.executionDataTracker.GetHighestHeight(), nil +} diff --git a/engine/access/state_stream/backend/backend_executiondata_test.go b/engine/access/state_stream/backend/backend_executiondata_test.go index d5f204ee0ee..01bc9aecd82 100644 --- a/engine/access/state_stream/backend/backend_executiondata_test.go +++ b/engine/access/state_stream/backend/backend_executiondata_test.go @@ -21,8 +21,9 @@ import ( "github.com/onflow/flow-go/engine/access/index" "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" + trackermock "github.com/onflow/flow-go/engine/access/subscription/mock" + "github.com/onflow/flow-go/engine/access/subscription/streamer" "github.com/onflow/flow-go/engine/access/subscription/tracker" - trackermock "github.com/onflow/flow-go/engine/access/subscription/tracker/mock" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/blobs" @@ -71,7 +72,7 @@ type BackendExecutionDataSuite struct { execDataHeroCache *herocache.BlockExecutionData executionDataTracker *trackermock.ExecutionDataTracker backend *StateStreamBackend - executionDataTrackerReal tracker.ExecutionDataTracker + executionDataTrackerReal subscription.ExecutionDataTracker executionResultProvider *osyncmock.ExecutionResultProvider executionStateCache *osyncmock.ExecutionStateCache @@ -268,16 +269,11 @@ func (s *BackendExecutionDataSuite) SetupBackend(useEventsIndex bool) { s.eventsIndex, useEventsIndex, state_stream.DefaultRegisterIDsRequestLimit, - subscription.NewSubscriptionHandler( - s.logger, - s.broadcaster, - subscription.DefaultSendTimeout, - subscription.DefaultResponseLimit, - subscription.DefaultSendBufferSize, - ), s.executionDataTracker, s.executionResultProvider, s.executionStateCache, + s.broadcaster, + streamer.NewDefaultStreamOptions(), ) require.NoError(s.T(), err) @@ -560,11 +556,15 @@ func (s *BackendExecutionDataSuite) TestSubscribeExecutionData() { }, } - subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription { + subFunc := func( + ctx context.Context, + blockID flow.Identifier, + startHeight uint64, + ) subscription.Subscription[*state_stream.ExecutionDataResponse] { return s.backend.SubscribeExecutionData(ctx, blockID, startHeight) } - s.subscribe(subFunc, tests) + subscribeExecData(s, subFunc, tests) } func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromStartBlockID() { @@ -593,11 +593,15 @@ func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromStartBlockID() return s.executionDataTrackerReal.GetStartHeightFromBlockID(startBlockID) }, nil) - subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription { + subFunc := func( + ctx context.Context, + blockID flow.Identifier, + startHeight uint64, + ) subscription.Subscription[*state_stream.ExecutionDataResponse] { return s.backend.SubscribeExecutionDataFromStartBlockID(ctx, blockID) } - s.subscribe(subFunc, tests) + subscribeExecData(s, subFunc, tests) } func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromStartBlockHeight() { @@ -626,11 +630,15 @@ func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromStartBlockHeig return s.executionDataTrackerReal.GetStartHeightFromHeight(startHeight) }, nil) - subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription { + subFunc := func( + ctx context.Context, + blockID flow.Identifier, + startHeight uint64, + ) subscription.Subscription[*state_stream.ExecutionDataResponse] { return s.backend.SubscribeExecutionDataFromStartBlockHeight(ctx, startHeight) } - s.subscribe(subFunc, tests) + subscribeExecData(s, subFunc, tests) } func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromLatest() { @@ -656,14 +664,22 @@ func (s *BackendExecutionDataSuite) TestSubscribeExecutionDataFromLatest() { return s.executionDataTrackerReal.GetStartHeightFromLatest(ctx) }, nil) - subFunc := func(ctx context.Context, blockID flow.Identifier, startHeight uint64) subscription.Subscription { + subFunc := func( + ctx context.Context, + blockID flow.Identifier, + startHeight uint64, + ) subscription.Subscription[*state_stream.ExecutionDataResponse] { return s.backend.SubscribeExecutionDataFromLatest(ctx) } - s.subscribe(subFunc, tests) + subscribeExecData(s, subFunc, tests) } -func (s *BackendExecutionDataSuite) subscribe(subscribeFunc func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) subscription.Subscription, tests []executionDataTestType) { +func subscribeExecData( + s *BackendExecutionDataSuite, + subscribeFunc func(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) subscription.Subscription[*state_stream.ExecutionDataResponse], + tests []executionDataTestType, +) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -698,12 +714,8 @@ func (s *BackendExecutionDataSuite) subscribe(subscribeFunc func(ctx context.Con // consume execution data from subscription unittest.RequireReturnsBefore(s.T(), func() { - v, ok := <-sub.Channel() + resp, ok := <-sub.Channel() require.True(s.T(), ok, "channel closed while waiting for exec data for block %d %v: err: %v", b.Height, b.ID(), sub.Err()) - - resp, ok := v.(*ExecutionDataResponse) - require.True(s.T(), ok, "unexpected response type: %T", v) - assert.Equal(s.T(), b.Height, resp.Height) assert.Equal(s.T(), execData.BlockExecutionData, resp.ExecutionData) }, time.Second, fmt.Sprintf("timed out waiting for exec data for block %d %v", b.Height, b.ID())) @@ -728,8 +740,8 @@ func (s *BackendExecutionDataSuite) subscribe(subscribeFunc func(ctx context.Con } } -// TestSubscribeEventsFromSporkRootBlock tests that events subscriptions starting from the spork -// root block return an empty result for the root block. +// TestSubscribeExecutionFromSporkRootBlock verifies that when subscribing from the spork +// root block, the stream starts from the first non-root block with actual data (no empty root response). func (s *BackendExecutionDataSuite) TestSubscribeExecutionFromSporkRootBlock() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -737,32 +749,21 @@ func (s *BackendExecutionDataSuite) TestSubscribeExecutionFromSporkRootBlock() { // setup the backend to have 1 available block s.highestBlockHeader = s.blocks[0].ToHeader() - rootEventResponse := &ExecutionDataResponse{ - Height: s.rootBlock.Height, - ExecutionData: &execution_data.BlockExecutionData{ - BlockID: s.rootBlock.ID(), - }, - } - - firstEventResponse := &ExecutionDataResponse{ + firstEventResponse := &state_stream.ExecutionDataResponse{ Height: s.blocks[0].Height, ExecutionData: s.execDataMap[s.blocks[0].ID()].BlockExecutionData, } - assertExecutionDataResponse := func(v interface{}, expected *ExecutionDataResponse) { - resp, ok := v.(*ExecutionDataResponse) + assertExecutionDataResponse := func(v interface{}, expected *state_stream.ExecutionDataResponse) { + resp, ok := v.(*state_stream.ExecutionDataResponse) require.True(s.T(), ok, "unexpected response type: %T", v) assert.Equal(s.T(), expected, resp) } - assertSubscriptionResponses := func(sub subscription.Subscription, cancel context.CancelFunc) { - // the first response should have details from the root block and no events + assertSubscriptionResponses := func(sub subscription.Subscription[*state_stream.ExecutionDataResponse], cancel context.CancelFunc) { + // the first response should have details from the first non-root block resp := <-sub.Channel() - assertExecutionDataResponse(resp, rootEventResponse) - - // the second response should have details from the first block and its events - resp = <-sub.Channel() assertExecutionDataResponse(resp, firstEventResponse) cancel() diff --git a/engine/access/state_stream/backend/event_retriever.go b/engine/access/state_stream/backend/event_retriever.go index 70aa5db032f..fdeeb3e63c1 100644 --- a/engine/access/state_stream/backend/event_retriever.go +++ b/engine/access/state_stream/backend/event_retriever.go @@ -8,19 +8,12 @@ import ( "github.com/rs/zerolog" "github.com/onflow/flow-go/engine/access/index" + "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/storage" "github.com/onflow/flow-go/utils/logging" ) -// EventsResponse represents the response containing events for a specific block. -type EventsResponse struct { - BlockID flow.Identifier - Height uint64 - Events flow.EventsList - BlockTimestamp time.Time -} - // EventsProvider retrieves events by block height. It can be configured to retrieve events from // the events indexer(if available) or using a dedicated callback to query it from other sources. type EventsProvider struct { @@ -35,8 +28,8 @@ type EventsProvider struct { // Expected errors: // - codes.NotFound: If block header for the specified block height is not found. // - error: An error, if any, encountered during getting events from storage or execution data. -func (b *EventsProvider) GetAllEventsResponse(ctx context.Context, height uint64) (*EventsResponse, error) { - var response *EventsResponse +func (b *EventsProvider) GetAllEventsResponse(ctx context.Context, height uint64) (*state_stream.EventsResponse, error) { + var response *state_stream.EventsResponse var err error if b.useEventsIndex { response, err = b.getEventsFromStorage(height) @@ -66,7 +59,7 @@ func (b *EventsProvider) GetAllEventsResponse(ctx context.Context, height uint64 // getEventsFromExecutionData returns the events for a given height extract from the execution data. // Expected errors: // - error: An error indicating issues with getting execution data for block -func (b *EventsProvider) getEventsFromExecutionData(ctx context.Context, height uint64) (*EventsResponse, error) { +func (b *EventsProvider) getEventsFromExecutionData(ctx context.Context, height uint64) (*state_stream.EventsResponse, error) { executionData, err := b.getExecutionData(ctx, height) if err != nil { return nil, fmt.Errorf("could not get execution data for block %d: %w", height, err) @@ -77,7 +70,7 @@ func (b *EventsProvider) getEventsFromExecutionData(ctx context.Context, height events = append(events, chunkExecutionData.Events...) } - return &EventsResponse{ + return &state_stream.EventsResponse{ BlockID: executionData.BlockID, Height: height, Events: events, @@ -88,7 +81,7 @@ func (b *EventsProvider) getEventsFromExecutionData(ctx context.Context, height // Expected errors: // - error: An error indicating any issues with the provided block height or // an error indicating issue with getting events for a block. -func (b *EventsProvider) getEventsFromStorage(height uint64) (*EventsResponse, error) { +func (b *EventsProvider) getEventsFromStorage(height uint64) (*state_stream.EventsResponse, error) { blockID, err := b.headers.BlockIDByHeight(height) if err != nil { return nil, fmt.Errorf("could not get header for height %d: %w", height, err) @@ -99,7 +92,7 @@ func (b *EventsProvider) getEventsFromStorage(height uint64) (*EventsResponse, e return nil, fmt.Errorf("could not get events for block %d: %w", height, err) } - return &EventsResponse{ + return &state_stream.EventsResponse{ BlockID: blockID, Height: height, Events: events, diff --git a/engine/access/state_stream/backend/handler.go b/engine/access/state_stream/backend/handler.go index 8cb97d696a1..235abb3e6a9 100644 --- a/engine/access/state_stream/backend/handler.go +++ b/engine/access/state_stream/backend/handler.go @@ -10,6 +10,8 @@ import ( "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/onflow/flow/protobuf/go/flow/executiondata" + "go.uber.org/atomic" + "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/engine/common/rpc" @@ -19,7 +21,8 @@ import ( ) type Handler struct { - subscription.StreamingData + MaxStreams int32 + StreamCount atomic.Int32 api state_stream.API chain flow.Chain @@ -40,7 +43,7 @@ var _ executiondata.ExecutionDataAPIServer = (*Handler)(nil) func NewHandler(api state_stream.API, chain flow.Chain, config Config) *Handler { h := &Handler{ - StreamingData: subscription.NewStreamingData(config.MaxGlobalStreams), + MaxStreams: int32(config.MaxGlobalStreams), api: api, chain: chain, eventFilterConfig: config.EventFilterConfig, @@ -337,8 +340,8 @@ func (h *Handler) SubscribeEventsFromLatest(request *executiondata.SubscribeEven // // Expected errors during normal operation: // - codes.Internal - could not convert execution data to entity or could not convert execution data event payloads to JSON. -func handleSubscribeExecutionData(send sendSubscribeExecutionDataResponseFunc, eventEncodingVersion entities.EventEncodingVersion) func(response *ExecutionDataResponse) error { - return func(resp *ExecutionDataResponse) error { +func handleSubscribeExecutionData(send sendSubscribeExecutionDataResponseFunc, eventEncodingVersion entities.EventEncodingVersion) func(response *state_stream.ExecutionDataResponse) error { + return func(resp *state_stream.ExecutionDataResponse) error { execData, err := convert.BlockExecutionDataToMessage(resp.ExecutionData) if err != nil { return status.Errorf(codes.Internal, "could not convert execution data to entity: %v", err) @@ -370,7 +373,7 @@ func handleSubscribeExecutionData(send sendSubscribeExecutionDataResponseFunc, e // // Expected errors during normal operation: // - codes.Internal - could not convert events to entity or the stream could not send a response. -func (h *Handler) handleEventsResponse(send sendSubscribeEventsResponseFunc, heartbeatInterval uint64, eventEncodingVersion entities.EventEncodingVersion) func(*EventsResponse) error { +func (h *Handler) handleEventsResponse(send sendSubscribeEventsResponseFunc, heartbeatInterval uint64, eventEncodingVersion entities.EventEncodingVersion) func(*state_stream.EventsResponse) error { if heartbeatInterval == 0 { heartbeatInterval = h.defaultHeartbeatInterval } @@ -378,7 +381,7 @@ func (h *Handler) handleEventsResponse(send sendSubscribeEventsResponseFunc, hea blocksSinceLastMessage := uint64(0) messageIndex := counters.NewMonotonicCounter(0) - return func(resp *EventsResponse) error { + return func(resp *state_stream.EventsResponse) error { // check if there are any events in the response. if not, do not send a message unless the last // response was more than HeartbeatInterval blocks ago if len(resp.Events) == 0 { @@ -465,7 +468,7 @@ func (h *Handler) GetRegisterValues(_ context.Context, request *executiondata.Ge // convertAccountsStatusesResultsToMessage converts account status responses to the message func convertAccountsStatusesResultsToMessage( eventVersion entities.EventEncodingVersion, - resp *AccountStatusesResponse, + resp *state_stream.AccountStatusesResponse, ) ([]*executiondata.SubscribeAccountStatusesResponse_Result, error) { var results []*executiondata.SubscribeAccountStatusesResponse_Result for address, events := range resp.AccountEvents { @@ -490,7 +493,7 @@ func (h *Handler) handleAccountStatusesResponse( heartbeatInterval uint64, evenVersion entities.EventEncodingVersion, send sendSubscribeAccountStatusesResponseFunc, -) func(resp *AccountStatusesResponse) error { +) func(resp *state_stream.AccountStatusesResponse) error { if heartbeatInterval == 0 { heartbeatInterval = h.defaultHeartbeatInterval } @@ -498,7 +501,7 @@ func (h *Handler) handleAccountStatusesResponse( blocksSinceLastMessage := uint64(0) messageIndex := counters.NewMonotonicCounter(0) - return func(resp *AccountStatusesResponse) error { + return func(resp *state_stream.AccountStatusesResponse) error { // check if there are any events in the response. if not, do not send a message unless the last // response was more than HeartbeatInterval blocks ago if len(resp.AccountEvents) == 0 { @@ -627,7 +630,7 @@ func (h *Handler) SubscribeAccountStatusesFromLatestBlock( // // Expected errors during normal operation: // - codes.Internal: If the subscription encounters an error or gets an unexpected response. -func HandleRPCSubscription[T any](sub subscription.Subscription, handleResponse func(resp T) error) error { +func HandleRPCSubscription[T any](sub subscription.Subscription[T], handleResponse func(resp T) error) error { err := subscription.HandleSubscription(sub, handleResponse) if err != nil { return rpc.ConvertError(err, "handle subscription error", codes.Internal) diff --git a/engine/access/state_stream/backend/handler_test.go b/engine/access/state_stream/backend/handler_test.go index 6f29a9a8aec..535856ca23b 100644 --- a/engine/access/state_stream/backend/handler_test.go +++ b/engine/access/state_stream/backend/handler_test.go @@ -25,6 +25,7 @@ import ( "github.com/onflow/flow-go/engine/access/state_stream" ssmock "github.com/onflow/flow-go/engine/access/state_stream/mock" "github.com/onflow/flow-go/engine/access/subscription" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" "github.com/onflow/flow-go/engine/common/rpc/convert" accessmodel "github.com/onflow/flow-go/model/access" "github.com/onflow/flow-go/model/flow" @@ -286,9 +287,9 @@ func TestExecutionDataStream(t *testing.T) { stream *StreamMock[executiondata.SubscribeExecutionDataRequest, executiondata.SubscribeExecutionDataResponse], api *ssmock.API, request *executiondata.SubscribeExecutionDataRequest, - response *ExecutionDataResponse, + response *state_stream.ExecutionDataResponse, ) { - sub := subscription.NewSubscription(1) + sub := subimpl.NewSubscription[*state_stream.ExecutionDataResponse](1) api.On("SubscribeExecutionData", mock.Anything, flow.ZeroID, uint64(0), mock.Anything).Return(sub) @@ -382,7 +383,7 @@ func TestExecutionDataStream(t *testing.T) { &executiondata.SubscribeExecutionDataRequest{ EventEncodingVersion: test.eventVersion, }, - &ExecutionDataResponse{ + &state_stream.ExecutionDataResponse{ Height: blockHeight, ExecutionData: unittest.BlockExecutionDataFixture( unittest.WithChunkExecutionDatas( @@ -412,9 +413,9 @@ func TestEventStream(t *testing.T) { stream *StreamMock[executiondata.SubscribeEventsRequest, executiondata.SubscribeEventsResponse], api *ssmock.API, request *executiondata.SubscribeEventsRequest, - response *EventsResponse, + response *state_stream.EventsResponse, ) { - sub := subscription.NewSubscription(1) + sub := subimpl.NewSubscription[*state_stream.EventsResponse](1) api.On("SubscribeEvents", mock.Anything, flow.ZeroID, uint64(0), mock.Anything).Return(sub) @@ -508,7 +509,7 @@ func TestEventStream(t *testing.T) { &executiondata.SubscribeEventsRequest{ EventEncodingVersion: test.eventVersion, }, - &EventsResponse{ + &state_stream.EventsResponse{ BlockID: blockID, Height: blockHeight, Events: ccfEvents, diff --git a/engine/access/state_stream/mock/account_statuses_stream_api.go b/engine/access/state_stream/mock/account_statuses_stream_api.go new file mode 100644 index 00000000000..b5a8c2fb5bc --- /dev/null +++ b/engine/access/state_stream/mock/account_statuses_stream_api.go @@ -0,0 +1,93 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" + + state_stream "github.com/onflow/flow-go/engine/access/state_stream" + + subscription "github.com/onflow/flow-go/engine/access/subscription" +) + +// AccountStatusesStreamAPI is an autogenerated mock type for the AccountStatusesStreamAPI type +type AccountStatusesStreamAPI struct { + mock.Mock +} + +// SubscribeAccountStatusesFromLatestBlock provides a mock function with given fields: ctx, filter +func (_m *AccountStatusesStreamAPI) SubscribeAccountStatusesFromLatestBlock(ctx context.Context, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { + ret := _m.Called(ctx, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeAccountStatusesFromLatestBlock") + } + + var r0 subscription.Subscription[*state_stream.AccountStatusesResponse] + if rf, ok := ret.Get(0).(func(context.Context, state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse]); ok { + r0 = rf(ctx, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.AccountStatusesResponse]) + } + } + + return r0 +} + +// SubscribeAccountStatusesFromStartBlockID provides a mock function with given fields: ctx, startBlockID, filter +func (_m *AccountStatusesStreamAPI) SubscribeAccountStatusesFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { + ret := _m.Called(ctx, startBlockID, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeAccountStatusesFromStartBlockID") + } + + var r0 subscription.Subscription[*state_stream.AccountStatusesResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse]); ok { + r0 = rf(ctx, startBlockID, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.AccountStatusesResponse]) + } + } + + return r0 +} + +// SubscribeAccountStatusesFromStartHeight provides a mock function with given fields: ctx, startHeight, filter +func (_m *AccountStatusesStreamAPI) SubscribeAccountStatusesFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { + ret := _m.Called(ctx, startHeight, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeAccountStatusesFromStartHeight") + } + + var r0 subscription.Subscription[*state_stream.AccountStatusesResponse] + if rf, ok := ret.Get(0).(func(context.Context, uint64, state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse]); ok { + r0 = rf(ctx, startHeight, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.AccountStatusesResponse]) + } + } + + return r0 +} + +// NewAccountStatusesStreamAPI creates a new instance of AccountStatusesStreamAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewAccountStatusesStreamAPI(t interface { + mock.TestingT + Cleanup(func()) +}) *AccountStatusesStreamAPI { + mock := &AccountStatusesStreamAPI{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/state_stream/mock/api.go b/engine/access/state_stream/mock/api.go index 1f07962cdca..d0f90b92689 100644 --- a/engine/access/state_stream/mock/api.go +++ b/engine/access/state_stream/mock/api.go @@ -95,19 +95,19 @@ func (_m *API) GetRegisterValues(registerIDs flow.RegisterIDs, height uint64) ([ } // SubscribeAccountStatusesFromLatestBlock provides a mock function with given fields: ctx, filter -func (_m *API) SubscribeAccountStatusesFromLatestBlock(ctx context.Context, filter state_stream.AccountStatusFilter) subscription.Subscription { +func (_m *API) SubscribeAccountStatusesFromLatestBlock(ctx context.Context, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { ret := _m.Called(ctx, filter) if len(ret) == 0 { panic("no return value specified for SubscribeAccountStatusesFromLatestBlock") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, state_stream.AccountStatusFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.AccountStatusesResponse] + if rf, ok := ret.Get(0).(func(context.Context, state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse]); ok { r0 = rf(ctx, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.AccountStatusesResponse]) } } @@ -115,19 +115,19 @@ func (_m *API) SubscribeAccountStatusesFromLatestBlock(ctx context.Context, filt } // SubscribeAccountStatusesFromStartBlockID provides a mock function with given fields: ctx, startBlockID, filter -func (_m *API) SubscribeAccountStatusesFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.AccountStatusFilter) subscription.Subscription { +func (_m *API) SubscribeAccountStatusesFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { ret := _m.Called(ctx, startBlockID, filter) if len(ret) == 0 { panic("no return value specified for SubscribeAccountStatusesFromStartBlockID") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, state_stream.AccountStatusFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.AccountStatusesResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse]); ok { r0 = rf(ctx, startBlockID, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.AccountStatusesResponse]) } } @@ -135,19 +135,19 @@ func (_m *API) SubscribeAccountStatusesFromStartBlockID(ctx context.Context, sta } // SubscribeAccountStatusesFromStartHeight provides a mock function with given fields: ctx, startHeight, filter -func (_m *API) SubscribeAccountStatusesFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.AccountStatusFilter) subscription.Subscription { +func (_m *API) SubscribeAccountStatusesFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse] { ret := _m.Called(ctx, startHeight, filter) if len(ret) == 0 { panic("no return value specified for SubscribeAccountStatusesFromStartHeight") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, uint64, state_stream.AccountStatusFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.AccountStatusesResponse] + if rf, ok := ret.Get(0).(func(context.Context, uint64, state_stream.AccountStatusFilter) subscription.Subscription[*state_stream.AccountStatusesResponse]); ok { r0 = rf(ctx, startHeight, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.AccountStatusesResponse]) } } @@ -155,19 +155,19 @@ func (_m *API) SubscribeAccountStatusesFromStartHeight(ctx context.Context, star } // SubscribeEvents provides a mock function with given fields: ctx, startBlockID, startHeight, filter -func (_m *API) SubscribeEvents(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription { +func (_m *API) SubscribeEvents(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { ret := _m.Called(ctx, startBlockID, startHeight, filter) if len(ret) == 0 { panic("no return value specified for SubscribeEvents") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64, state_stream.EventFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { r0 = rf(ctx, startBlockID, startHeight, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) } } @@ -175,19 +175,19 @@ func (_m *API) SubscribeEvents(ctx context.Context, startBlockID flow.Identifier } // SubscribeEventsFromLatest provides a mock function with given fields: ctx, filter -func (_m *API) SubscribeEventsFromLatest(ctx context.Context, filter state_stream.EventFilter) subscription.Subscription { +func (_m *API) SubscribeEventsFromLatest(ctx context.Context, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { ret := _m.Called(ctx, filter) if len(ret) == 0 { panic("no return value specified for SubscribeEventsFromLatest") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, state_stream.EventFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { r0 = rf(ctx, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) } } @@ -195,19 +195,19 @@ func (_m *API) SubscribeEventsFromLatest(ctx context.Context, filter state_strea } // SubscribeEventsFromStartBlockID provides a mock function with given fields: ctx, startBlockID, filter -func (_m *API) SubscribeEventsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.EventFilter) subscription.Subscription { +func (_m *API) SubscribeEventsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { ret := _m.Called(ctx, startBlockID, filter) if len(ret) == 0 { panic("no return value specified for SubscribeEventsFromStartBlockID") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, state_stream.EventFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { r0 = rf(ctx, startBlockID, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) } } @@ -215,19 +215,19 @@ func (_m *API) SubscribeEventsFromStartBlockID(ctx context.Context, startBlockID } // SubscribeEventsFromStartHeight provides a mock function with given fields: ctx, startHeight, filter -func (_m *API) SubscribeEventsFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription { +func (_m *API) SubscribeEventsFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { ret := _m.Called(ctx, startHeight, filter) if len(ret) == 0 { panic("no return value specified for SubscribeEventsFromStartHeight") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, uint64, state_stream.EventFilter) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, uint64, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { r0 = rf(ctx, startHeight, filter) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) } } @@ -235,19 +235,19 @@ func (_m *API) SubscribeEventsFromStartHeight(ctx context.Context, startHeight u } // SubscribeExecutionData provides a mock function with given fields: ctx, startBlockID, startBlockHeight -func (_m *API) SubscribeExecutionData(ctx context.Context, startBlockID flow.Identifier, startBlockHeight uint64) subscription.Subscription { +func (_m *API) SubscribeExecutionData(ctx context.Context, startBlockID flow.Identifier, startBlockHeight uint64) subscription.Subscription[*state_stream.ExecutionDataResponse] { ret := _m.Called(ctx, startBlockID, startBlockHeight) if len(ret) == 0 { panic("no return value specified for SubscribeExecutionData") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { r0 = rf(ctx, startBlockID, startBlockHeight) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) } } @@ -255,19 +255,19 @@ func (_m *API) SubscribeExecutionData(ctx context.Context, startBlockID flow.Ide } // SubscribeExecutionDataFromLatest provides a mock function with given fields: ctx -func (_m *API) SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription { +func (_m *API) SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription[*state_stream.ExecutionDataResponse] { ret := _m.Called(ctx) if len(ret) == 0 { panic("no return value specified for SubscribeExecutionDataFromLatest") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) } } @@ -275,19 +275,19 @@ func (_m *API) SubscribeExecutionDataFromLatest(ctx context.Context) subscriptio } // SubscribeExecutionDataFromStartBlockHeight provides a mock function with given fields: ctx, startBlockHeight -func (_m *API) SubscribeExecutionDataFromStartBlockHeight(ctx context.Context, startBlockHeight uint64) subscription.Subscription { +func (_m *API) SubscribeExecutionDataFromStartBlockHeight(ctx context.Context, startBlockHeight uint64) subscription.Subscription[*state_stream.ExecutionDataResponse] { ret := _m.Called(ctx, startBlockHeight) if len(ret) == 0 { panic("no return value specified for SubscribeExecutionDataFromStartBlockHeight") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, uint64) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context, uint64) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { r0 = rf(ctx, startBlockHeight) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) } } @@ -295,19 +295,19 @@ func (_m *API) SubscribeExecutionDataFromStartBlockHeight(ctx context.Context, s } // SubscribeExecutionDataFromStartBlockID provides a mock function with given fields: ctx, startBlockID -func (_m *API) SubscribeExecutionDataFromStartBlockID(ctx context.Context, startBlockID flow.Identifier) subscription.Subscription { +func (_m *API) SubscribeExecutionDataFromStartBlockID(ctx context.Context, startBlockID flow.Identifier) subscription.Subscription[*state_stream.ExecutionDataResponse] { ret := _m.Called(ctx, startBlockID) if len(ret) == 0 { panic("no return value specified for SubscribeExecutionDataFromStartBlockID") } - var r0 subscription.Subscription - if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier) subscription.Subscription); ok { + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { r0 = rf(ctx, startBlockID) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(subscription.Subscription) + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) } } diff --git a/engine/access/state_stream/mock/events_stream_api.go b/engine/access/state_stream/mock/events_stream_api.go new file mode 100644 index 00000000000..0c4e66b0023 --- /dev/null +++ b/engine/access/state_stream/mock/events_stream_api.go @@ -0,0 +1,113 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" + + state_stream "github.com/onflow/flow-go/engine/access/state_stream" + + subscription "github.com/onflow/flow-go/engine/access/subscription" +) + +// EventsStreamAPI is an autogenerated mock type for the EventsStreamAPI type +type EventsStreamAPI struct { + mock.Mock +} + +// SubscribeEvents provides a mock function with given fields: ctx, startBlockID, startHeight, filter +func (_m *EventsStreamAPI) SubscribeEvents(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { + ret := _m.Called(ctx, startBlockID, startHeight, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeEvents") + } + + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { + r0 = rf(ctx, startBlockID, startHeight, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) + } + } + + return r0 +} + +// SubscribeEventsFromLatest provides a mock function with given fields: ctx, filter +func (_m *EventsStreamAPI) SubscribeEventsFromLatest(ctx context.Context, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { + ret := _m.Called(ctx, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeEventsFromLatest") + } + + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { + r0 = rf(ctx, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) + } + } + + return r0 +} + +// SubscribeEventsFromStartBlockID provides a mock function with given fields: ctx, startBlockID, filter +func (_m *EventsStreamAPI) SubscribeEventsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { + ret := _m.Called(ctx, startBlockID, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeEventsFromStartBlockID") + } + + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { + r0 = rf(ctx, startBlockID, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) + } + } + + return r0 +} + +// SubscribeEventsFromStartHeight provides a mock function with given fields: ctx, startHeight, filter +func (_m *EventsStreamAPI) SubscribeEventsFromStartHeight(ctx context.Context, startHeight uint64, filter state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse] { + ret := _m.Called(ctx, startHeight, filter) + + if len(ret) == 0 { + panic("no return value specified for SubscribeEventsFromStartHeight") + } + + var r0 subscription.Subscription[*state_stream.EventsResponse] + if rf, ok := ret.Get(0).(func(context.Context, uint64, state_stream.EventFilter) subscription.Subscription[*state_stream.EventsResponse]); ok { + r0 = rf(ctx, startHeight, filter) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.EventsResponse]) + } + } + + return r0 +} + +// NewEventsStreamAPI creates a new instance of EventsStreamAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEventsStreamAPI(t interface { + mock.TestingT + Cleanup(func()) +}) *EventsStreamAPI { + mock := &EventsStreamAPI{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/state_stream/mock/execution_data_stream_api.go b/engine/access/state_stream/mock/execution_data_stream_api.go new file mode 100644 index 00000000000..2888ba92174 --- /dev/null +++ b/engine/access/state_stream/mock/execution_data_stream_api.go @@ -0,0 +1,159 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + access "github.com/onflow/flow-go/model/access" + + execution_data "github.com/onflow/flow-go/module/executiondatasync/execution_data" + + flow "github.com/onflow/flow-go/model/flow" + + mock "github.com/stretchr/testify/mock" + + optimistic_sync "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" + + state_stream "github.com/onflow/flow-go/engine/access/state_stream" + + subscription "github.com/onflow/flow-go/engine/access/subscription" +) + +// ExecutionDataStreamAPI is an autogenerated mock type for the ExecutionDataStreamAPI type +type ExecutionDataStreamAPI struct { + mock.Mock +} + +// GetExecutionDataByBlockID provides a mock function with given fields: ctx, blockID, criteria +func (_m *ExecutionDataStreamAPI) GetExecutionDataByBlockID(ctx context.Context, blockID flow.Identifier, criteria optimistic_sync.Criteria) (*execution_data.BlockExecutionData, *access.ExecutorMetadata, error) { + ret := _m.Called(ctx, blockID, criteria) + + if len(ret) == 0 { + panic("no return value specified for GetExecutionDataByBlockID") + } + + var r0 *execution_data.BlockExecutionData + var r1 *access.ExecutorMetadata + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, optimistic_sync.Criteria) (*execution_data.BlockExecutionData, *access.ExecutorMetadata, error)); ok { + return rf(ctx, blockID, criteria) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, optimistic_sync.Criteria) *execution_data.BlockExecutionData); ok { + r0 = rf(ctx, blockID, criteria) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*execution_data.BlockExecutionData) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Identifier, optimistic_sync.Criteria) *access.ExecutorMetadata); ok { + r1 = rf(ctx, blockID, criteria) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*access.ExecutorMetadata) + } + } + + if rf, ok := ret.Get(2).(func(context.Context, flow.Identifier, optimistic_sync.Criteria) error); ok { + r2 = rf(ctx, blockID, criteria) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// SubscribeExecutionData provides a mock function with given fields: ctx, startBlockID, startBlockHeight +func (_m *ExecutionDataStreamAPI) SubscribeExecutionData(ctx context.Context, startBlockID flow.Identifier, startBlockHeight uint64) subscription.Subscription[*state_stream.ExecutionDataResponse] { + ret := _m.Called(ctx, startBlockID, startBlockHeight) + + if len(ret) == 0 { + panic("no return value specified for SubscribeExecutionData") + } + + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { + r0 = rf(ctx, startBlockID, startBlockHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) + } + } + + return r0 +} + +// SubscribeExecutionDataFromLatest provides a mock function with given fields: ctx +func (_m *ExecutionDataStreamAPI) SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription[*state_stream.ExecutionDataResponse] { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SubscribeExecutionDataFromLatest") + } + + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) + } + } + + return r0 +} + +// SubscribeExecutionDataFromStartBlockHeight provides a mock function with given fields: ctx, startBlockHeight +func (_m *ExecutionDataStreamAPI) SubscribeExecutionDataFromStartBlockHeight(ctx context.Context, startBlockHeight uint64) subscription.Subscription[*state_stream.ExecutionDataResponse] { + ret := _m.Called(ctx, startBlockHeight) + + if len(ret) == 0 { + panic("no return value specified for SubscribeExecutionDataFromStartBlockHeight") + } + + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context, uint64) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { + r0 = rf(ctx, startBlockHeight) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) + } + } + + return r0 +} + +// SubscribeExecutionDataFromStartBlockID provides a mock function with given fields: ctx, startBlockID +func (_m *ExecutionDataStreamAPI) SubscribeExecutionDataFromStartBlockID(ctx context.Context, startBlockID flow.Identifier) subscription.Subscription[*state_stream.ExecutionDataResponse] { + ret := _m.Called(ctx, startBlockID) + + if len(ret) == 0 { + panic("no return value specified for SubscribeExecutionDataFromStartBlockID") + } + + var r0 subscription.Subscription[*state_stream.ExecutionDataResponse] + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier) subscription.Subscription[*state_stream.ExecutionDataResponse]); ok { + r0 = rf(ctx, startBlockID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(subscription.Subscription[*state_stream.ExecutionDataResponse]) + } + } + + return r0 +} + +// NewExecutionDataStreamAPI creates a new instance of ExecutionDataStreamAPI. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewExecutionDataStreamAPI(t interface { + mock.TestingT + Cleanup(func()) +}) *ExecutionDataStreamAPI { + mock := &ExecutionDataStreamAPI{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/state_stream/response.go b/engine/access/state_stream/response.go new file mode 100644 index 00000000000..b254ac29b1a --- /dev/null +++ b/engine/access/state_stream/response.go @@ -0,0 +1,29 @@ +package state_stream + +import ( + "time" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/executiondatasync/execution_data" +) + +type AccountStatusesResponse struct { + BlockID flow.Identifier + Height uint64 + AccountEvents map[string]flow.EventsList +} + +// EventsResponse represents the response containing events for a specific block. +type EventsResponse struct { + BlockID flow.Identifier + Height uint64 + Events flow.EventsList + BlockTimestamp time.Time +} + +// ExecutionDataResponse bundles the execution data returned for a single block. +type ExecutionDataResponse struct { + Height uint64 + ExecutionData *execution_data.BlockExecutionData + BlockTimestamp time.Time +} diff --git a/engine/access/state_stream/state_stream.go b/engine/access/state_stream/state_stream.go index 7beafb8c679..35f4fda0ef9 100644 --- a/engine/access/state_stream/state_stream.go +++ b/engine/access/state_stream/state_stream.go @@ -15,8 +15,32 @@ const ( DefaultRegisterIDsRequestLimit = 100 ) -// API represents an interface that defines methods for interacting with a blockchain's execution data and events. -type API interface { +type AccountStatusesStreamAPI interface { + // SubscribeAccountStatusesFromStartBlockID subscribes to the streaming of account status changes starting from + // a specific block ID with an optional status filter. + SubscribeAccountStatusesFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + filter AccountStatusFilter, + ) subscription.Subscription[*AccountStatusesResponse] + + // SubscribeAccountStatusesFromStartHeight subscribes to the streaming of account status changes starting from + // a specific block height, with an optional status filter. + SubscribeAccountStatusesFromStartHeight( + ctx context.Context, + startHeight uint64, + filter AccountStatusFilter, + ) subscription.Subscription[*AccountStatusesResponse] + + // SubscribeAccountStatusesFromLatestBlock subscribes to the streaming of account status changes starting from a + // latest sealed block, with an optional status filter. + SubscribeAccountStatusesFromLatestBlock( + ctx context.Context, + filter AccountStatusFilter, + ) subscription.Subscription[*AccountStatusesResponse] +} + +type ExecutionDataStreamAPI interface { // GetExecutionDataByBlockID retrieves execution data for a specific block by its block ID. // // CAUTION: this layer SIMPLIFIES the ERROR HANDLING convention @@ -25,18 +49,39 @@ type API interface { // // Expected errors: // - [access.DataNotFoundError]: when data required to process the request is not available. - GetExecutionDataByBlockID(ctx context.Context, blockID flow.Identifier, criteria optimistic_sync.Criteria) (*execution_data.BlockExecutionData, *accessmodel.ExecutorMetadata, error) + GetExecutionDataByBlockID( + ctx context.Context, + blockID flow.Identifier, + criteria optimistic_sync.Criteria, + ) (*execution_data.BlockExecutionData, *accessmodel.ExecutorMetadata, error) + // SubscribeExecutionData is deprecated and will be removed in future versions. // Use SubscribeExecutionDataFromStartBlockID, SubscribeExecutionDataFromStartBlockHeight or SubscribeExecutionDataFromLatest. // // SubscribeExecutionData subscribes to execution data starting from a specific block ID and block height. - SubscribeExecutionData(ctx context.Context, startBlockID flow.Identifier, startBlockHeight uint64) subscription.Subscription + SubscribeExecutionData( + ctx context.Context, + startBlockID flow.Identifier, + startBlockHeight uint64, + ) subscription.Subscription[*ExecutionDataResponse] + // SubscribeExecutionDataFromStartBlockID subscribes to execution data starting from a specific block id. - SubscribeExecutionDataFromStartBlockID(ctx context.Context, startBlockID flow.Identifier) subscription.Subscription + SubscribeExecutionDataFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + ) subscription.Subscription[*ExecutionDataResponse] + // SubscribeExecutionDataFromStartBlockHeight subscribes to execution data starting from a specific block height. - SubscribeExecutionDataFromStartBlockHeight(ctx context.Context, startBlockHeight uint64) subscription.Subscription + SubscribeExecutionDataFromStartBlockHeight( + ctx context.Context, + startBlockHeight uint64, + ) subscription.Subscription[*ExecutionDataResponse] + // SubscribeExecutionDataFromLatest subscribes to execution data starting from latest block. - SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription + SubscribeExecutionDataFromLatest(ctx context.Context) subscription.Subscription[*ExecutionDataResponse] +} + +type EventsStreamAPI interface { // SubscribeEvents is deprecated and will be removed in a future version. // Use SubscribeEventsFromStartBlockID, SubscribeEventsFromStartHeight or SubscribeEventsFromLatest. // @@ -59,7 +104,13 @@ type API interface { // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEvents will return a failed subscription. - SubscribeEvents(ctx context.Context, startBlockID flow.Identifier, startHeight uint64, filter EventFilter) subscription.Subscription + SubscribeEvents( + ctx context.Context, + startBlockID flow.Identifier, + startHeight uint64, + filter EventFilter, + ) subscription.Subscription[*EventsResponse] + // SubscribeEventsFromStartBlockID streams events starting at the specified block ID, // up until the latest available block. Once the latest is // reached, the stream will remain open and responses are sent for each new @@ -75,7 +126,12 @@ type API interface { // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEventsFromStartBlockID will return a failed subscription. - SubscribeEventsFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter EventFilter) subscription.Subscription + SubscribeEventsFromStartBlockID( + ctx context.Context, + startBlockID flow.Identifier, + filter EventFilter, + ) subscription.Subscription[*EventsResponse] + // SubscribeEventsFromStartHeight streams events starting at the specified block height, // up until the latest available block. Once the latest is // reached, the stream will remain open and responses are sent for each new @@ -91,7 +147,12 @@ type API interface { // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEventsFromStartHeight will return a failed subscription. - SubscribeEventsFromStartHeight(ctx context.Context, startHeight uint64, filter EventFilter) subscription.Subscription + SubscribeEventsFromStartHeight( + ctx context.Context, + startHeight uint64, + filter EventFilter, + ) subscription.Subscription[*EventsResponse] + // SubscribeEventsFromLatest subscribes to events starting at the latest sealed block, // up until the latest available block. Once the latest is // reached, the stream will remain open and responses are sent for each new @@ -106,16 +167,15 @@ type API interface { // - filter: The event filter used to filter events. // // If invalid parameters will be supplied SubscribeEventsFromLatest will return a failed subscription. - SubscribeEventsFromLatest(ctx context.Context, filter EventFilter) subscription.Subscription + SubscribeEventsFromLatest(ctx context.Context, filter EventFilter) subscription.Subscription[*EventsResponse] +} + +// API represents an interface that defines methods for interacting with a blockchain's execution data and events. +type API interface { + AccountStatusesStreamAPI + ExecutionDataStreamAPI + EventsStreamAPI + // GetRegisterValues returns register values for a set of register IDs at the provided block height. GetRegisterValues(registerIDs flow.RegisterIDs, height uint64) ([]flow.RegisterValue, error) - // SubscribeAccountStatusesFromStartBlockID subscribes to the streaming of account status changes starting from - // a specific block ID with an optional status filter. - SubscribeAccountStatusesFromStartBlockID(ctx context.Context, startBlockID flow.Identifier, filter AccountStatusFilter) subscription.Subscription - // SubscribeAccountStatusesFromStartHeight subscribes to the streaming of account status changes starting from - // a specific block height, with an optional status filter. - SubscribeAccountStatusesFromStartHeight(ctx context.Context, startHeight uint64, filter AccountStatusFilter) subscription.Subscription - // SubscribeAccountStatusesFromLatestBlock subscribes to the streaming of account status changes starting from a - // latest sealed block, with an optional status filter. - SubscribeAccountStatusesFromLatestBlock(ctx context.Context, filter AccountStatusFilter) subscription.Subscription } diff --git a/engine/access/subscription/height_source.go b/engine/access/subscription/height_source.go new file mode 100644 index 00000000000..9124cb47966 --- /dev/null +++ b/engine/access/subscription/height_source.go @@ -0,0 +1,30 @@ +package subscription + +import ( + "context" + "errors" +) + +// HeightSource TODO: this interface is only needed for tests. +// As each streamer is anyway coupled to a specific data source. +// So, this interface might be useless. However, I guess it'll be used in tests. +type HeightSource[T any] interface { + // StartHeight first height to stream from + StartHeight() uint64 + + // EndHeight is optional bound; 0 = unbounded. + EndHeight() uint64 + + // ReadyUpToHeight returns the highest height safe to serve for this source + ReadyUpToHeight() (uint64, error) + + // GetItemAtHeight returns an item by height. + // Returns ErrBlockNotReady if data is not available yet. + GetItemAtHeight(ctx context.Context, height uint64) (T, error) +} + +type GetItemAtHeightFunc[T any] func(ctx context.Context, height uint64) (T, error) + +var ErrBlockNotReady = errors.New("data item is not ingested yet") + +var ErrEndOfData = errors.New("end of data") diff --git a/engine/access/subscription/height_source/height_source.go b/engine/access/subscription/height_source/height_source.go new file mode 100644 index 00000000000..59d34a1d5d7 --- /dev/null +++ b/engine/access/subscription/height_source/height_source.go @@ -0,0 +1,58 @@ +package height_source + +import ( + "context" + + "github.com/onflow/flow-go/engine/access/subscription" +) + +type HeightSource[T any] struct { + startHeight uint64 + endHeight uint64 // 0 if unbounded + readyUpToHeightFunc func() (uint64, error) + getItemAtHeightFunc func(ctx context.Context, height uint64) (T, error) +} + +func NewHeightSource[T any]( + start uint64, + end uint64, + readyUpToHeightFunc func() (uint64, error), + getItemAtHeightFunc func(ctx context.Context, height uint64) (T, error), +) *HeightSource[T] { + return &HeightSource[T]{ + startHeight: start, + endHeight: end, + readyUpToHeightFunc: readyUpToHeightFunc, + getItemAtHeightFunc: getItemAtHeightFunc, + } +} + +var _ subscription.HeightSource[any] = (*HeightSource[any])(nil) + +func (h *HeightSource[T]) StartHeight() uint64 { + return h.startHeight +} + +func (h *HeightSource[T]) EndHeight() uint64 { + return h.endHeight +} + +func (h *HeightSource[T]) ReadyUpToHeight() (uint64, error) { + return h.readyUpToHeightFunc() +} + +func (h *HeightSource[T]) GetItemAtHeight(ctx context.Context, height uint64) (T, error) { + var empty T + + select { + case <-ctx.Done(): + return empty, ctx.Err() + default: + item, err := h.getItemAtHeightFunc(ctx, height) + if err != nil { + return empty, err + } + + return item, nil + } +} diff --git a/engine/access/subscription/height_source/height_source_test.go b/engine/access/subscription/height_source/height_source_test.go new file mode 100644 index 00000000000..9f328d281a5 --- /dev/null +++ b/engine/access/subscription/height_source/height_source_test.go @@ -0,0 +1,86 @@ +package height_source + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + subpkg "github.com/onflow/flow-go/engine/access/subscription" +) + +func TestHeightSource_ReadyAndGet(t *testing.T) { + t.Parallel() + + readyCalled := 0 + var readyUpTo uint64 = 10 + hs := NewHeightSource[int]( + 1, + 5, + func() (uint64, error) { + readyCalled++ + return readyUpTo, nil + }, + func(ctx context.Context, h uint64) (int, error) { return int(h), nil }, + ) + + assert.Equal(t, uint64(1), hs.StartHeight()) + assert.Equal(t, uint64(5), hs.EndHeight()) + + r, err := hs.ReadyUpToHeight() + require.NoError(t, err) + assert.Equal(t, readyUpTo, r) + assert.Equal(t, 1, readyCalled) + + ctx := context.Background() + val, err := hs.GetItemAtHeight(ctx, 3) + require.NoError(t, err) + assert.Equal(t, 3, val) +} + +func TestHeightSource_GetItemAtHeight_ContextCancel(t *testing.T) { + t.Parallel() + + hs := NewHeightSource[int]( + 0, + 0, + func() (uint64, error) { return 0, nil }, + func(ctx context.Context, h uint64) (int, error) { + // wait so we can cancel + select { + case <-ctx.Done(): + return 0, ctx.Err() + case <-time.After(100 * time.Millisecond): + return int(h), nil + } + }, + ) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // cancel immediately + _, err := hs.GetItemAtHeight(ctx, 1) + require.Error(t, err) +} + +func TestHeightSource_GetItemAtHeight_ErrorPropagation(t *testing.T) { + t.Parallel() + + sentinel := errors.New("boom") + + hs := NewHeightSource[int]( + 0, + 0, + func() (uint64, error) { return 0, nil }, + func(ctx context.Context, h uint64) (int, error) { return 0, sentinel }, + ) + + _, err := hs.GetItemAtHeight(context.Background(), 5) + require.Error(t, err) + // current implementation may wrap with ErrBlockNotReady; ensure we can still detect the sentinel + assert.ErrorIs(t, err, sentinel) + // and often also tag as not ingested; allow either behavior here + _ = subpkg.ErrBlockNotReady +} diff --git a/engine/access/subscription/mock/block_tracker.go b/engine/access/subscription/mock/block_tracker.go new file mode 100644 index 00000000000..98c57251aca --- /dev/null +++ b/engine/access/subscription/mock/block_tracker.go @@ -0,0 +1,187 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" +) + +// BlockTracker is an autogenerated mock type for the BlockTracker type +type BlockTracker struct { + mock.Mock +} + +// GetHighestHeight provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetHighestHeight(_a0 flow.BlockStatus) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetHighestHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.BlockStatus) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.BlockStatus) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.BlockStatus) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeight provides a mock function with given fields: _a0, _a1, _a2 +func (_m *BlockTracker) GetStartHeight(_a0 context.Context, _a1 flow.Identifier, _a2 uint64) (uint64, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) (uint64, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) uint64); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Identifier, uint64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromBlockID provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetStartHeightFromBlockID(_a0 flow.Identifier) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromBlockID") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.Identifier) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.Identifier) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.Identifier) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromHeight provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetStartHeightFromHeight(_a0 uint64) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(uint64) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromLatest provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetStartHeightFromLatest(_a0 context.Context) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromLatest") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProcessOnFinalizedBlock provides a mock function with no fields +func (_m *BlockTracker) ProcessOnFinalizedBlock() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ProcessOnFinalizedBlock") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewBlockTracker creates a new instance of BlockTracker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBlockTracker(t interface { + mock.TestingT + Cleanup(func()) +}) *BlockTracker { + mock := &BlockTracker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription/mock/execution_data_tracker.go b/engine/access/subscription/mock/execution_data_tracker.go new file mode 100644 index 00000000000..ccfad6bc8b4 --- /dev/null +++ b/engine/access/subscription/mock/execution_data_tracker.go @@ -0,0 +1,166 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + execution_data "github.com/onflow/flow-go/module/executiondatasync/execution_data" + + mock "github.com/stretchr/testify/mock" +) + +// ExecutionDataTracker is an autogenerated mock type for the ExecutionDataTracker type +type ExecutionDataTracker struct { + mock.Mock +} + +// GetHighestHeight provides a mock function with no fields +func (_m *ExecutionDataTracker) GetHighestHeight() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetHighestHeight") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// GetStartHeight provides a mock function with given fields: _a0, _a1, _a2 +func (_m *ExecutionDataTracker) GetStartHeight(_a0 context.Context, _a1 flow.Identifier, _a2 uint64) (uint64, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) (uint64, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) uint64); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Identifier, uint64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromBlockID provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) GetStartHeightFromBlockID(_a0 flow.Identifier) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromBlockID") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.Identifier) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.Identifier) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.Identifier) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromHeight provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) GetStartHeightFromHeight(_a0 uint64) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(uint64) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromLatest provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) GetStartHeightFromLatest(_a0 context.Context) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromLatest") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OnExecutionData provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) OnExecutionData(_a0 *execution_data.BlockExecutionDataEntity) { + _m.Called(_a0) +} + +// NewExecutionDataTracker creates a new instance of ExecutionDataTracker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewExecutionDataTracker(t interface { + mock.TestingT + Cleanup(func()) +}) *ExecutionDataTracker { + mock := &ExecutionDataTracker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription/mock/height_source.go b/engine/access/subscription/mock/height_source.go new file mode 100644 index 00000000000..8fe7b6dbe6d --- /dev/null +++ b/engine/access/subscription/mock/height_source.go @@ -0,0 +1,122 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// HeightSource is an autogenerated mock type for the HeightSource type +type HeightSource[T any] struct { + mock.Mock +} + +// EndHeight provides a mock function with no fields +func (_m *HeightSource[T]) EndHeight() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for EndHeight") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// GetItemAtHeight provides a mock function with given fields: ctx, height +func (_m *HeightSource[T]) GetItemAtHeight(ctx context.Context, height uint64) (T, error) { + ret := _m.Called(ctx, height) + + if len(ret) == 0 { + panic("no return value specified for GetItemAtHeight") + } + + var r0 T + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64) (T, error)); ok { + return rf(ctx, height) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64) T); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(T) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReadyUpToHeight provides a mock function with no fields +func (_m *HeightSource[T]) ReadyUpToHeight() (uint64, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ReadyUpToHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func() (uint64, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// StartHeight provides a mock function with no fields +func (_m *HeightSource[T]) StartHeight() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for StartHeight") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// NewHeightSource creates a new instance of HeightSource. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewHeightSource[T any](t interface { + mock.TestingT + Cleanup(func()) +}) *HeightSource[T] { + mock := &HeightSource[T]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription/mock/height_tracker.go b/engine/access/subscription/mock/height_tracker.go new file mode 100644 index 00000000000..b9d1f6e9276 --- /dev/null +++ b/engine/access/subscription/mock/height_tracker.go @@ -0,0 +1,141 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" +) + +// HeightTracker is an autogenerated mock type for the HeightTracker type +type HeightTracker struct { + mock.Mock +} + +// GetStartHeight provides a mock function with given fields: _a0, _a1, _a2 +func (_m *HeightTracker) GetStartHeight(_a0 context.Context, _a1 flow.Identifier, _a2 uint64) (uint64, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) (uint64, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) uint64); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Identifier, uint64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromBlockID provides a mock function with given fields: _a0 +func (_m *HeightTracker) GetStartHeightFromBlockID(_a0 flow.Identifier) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromBlockID") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.Identifier) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.Identifier) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.Identifier) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromHeight provides a mock function with given fields: _a0 +func (_m *HeightTracker) GetStartHeightFromHeight(_a0 uint64) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(uint64) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromLatest provides a mock function with given fields: _a0 +func (_m *HeightTracker) GetStartHeightFromLatest(_a0 context.Context) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromLatest") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewHeightTracker creates a new instance of HeightTracker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewHeightTracker(t interface { + mock.TestingT + Cleanup(func()) +}) *HeightTracker { + mock := &HeightTracker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription/mock/streamer.go b/engine/access/subscription/mock/streamer.go new file mode 100644 index 00000000000..a6cc3296c8f --- /dev/null +++ b/engine/access/subscription/mock/streamer.go @@ -0,0 +1,33 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// Streamer is an autogenerated mock type for the Streamer type +type Streamer struct { + mock.Mock +} + +// Stream provides a mock function with given fields: ctx +func (_m *Streamer) Stream(ctx context.Context) { + _m.Called(ctx) +} + +// NewStreamer creates a new instance of Streamer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStreamer(t interface { + mock.TestingT + Cleanup(func()) +}) *Streamer { + mock := &Streamer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription/mock/subscription.go b/engine/access/subscription/mock/subscription.go index 467cd80f7cc..f7792b409e0 100644 --- a/engine/access/subscription/mock/subscription.go +++ b/engine/access/subscription/mock/subscription.go @@ -2,35 +2,51 @@ package mock -import mock "github.com/stretchr/testify/mock" +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + time "time" +) // Subscription is an autogenerated mock type for the Subscription type -type Subscription struct { +type Subscription[T any] struct { mock.Mock } // Channel provides a mock function with no fields -func (_m *Subscription) Channel() <-chan interface{} { +func (_m *Subscription[T]) Channel() <-chan T { ret := _m.Called() if len(ret) == 0 { panic("no return value specified for Channel") } - var r0 <-chan interface{} - if rf, ok := ret.Get(0).(func() <-chan interface{}); ok { + var r0 <-chan T + if rf, ok := ret.Get(0).(func() <-chan T); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(<-chan interface{}) + r0 = ret.Get(0).(<-chan T) } } return r0 } +// Close provides a mock function with no fields +func (_m *Subscription[T]) Close() { + _m.Called() +} + +// CloseWithError provides a mock function with given fields: _a0 +func (_m *Subscription[T]) CloseWithError(_a0 error) { + _m.Called(_a0) +} + // Err provides a mock function with no fields -func (_m *Subscription) Err() error { +func (_m *Subscription[T]) Err() error { ret := _m.Called() if len(ret) == 0 { @@ -48,7 +64,7 @@ func (_m *Subscription) Err() error { } // ID provides a mock function with no fields -func (_m *Subscription) ID() string { +func (_m *Subscription[T]) ID() string { ret := _m.Called() if len(ret) == 0 { @@ -65,13 +81,31 @@ func (_m *Subscription) ID() string { return r0 } +// Send provides a mock function with given fields: ctx, value, timeout +func (_m *Subscription[T]) Send(ctx context.Context, value T, timeout time.Duration) error { + ret := _m.Called(ctx, value, timeout) + + if len(ret) == 0 { + panic("no return value specified for Send") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, T, time.Duration) error); ok { + r0 = rf(ctx, value, timeout) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // NewSubscription creates a new instance of Subscription. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewSubscription(t interface { +func NewSubscription[T any](t interface { mock.TestingT Cleanup(func()) -}) *Subscription { - mock := &Subscription{} +}) *Subscription[T] { + mock := &Subscription[T]{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/engine/access/subscription/streamer.go b/engine/access/subscription/streamer.go index 437028edc6c..97b49cc8888 100644 --- a/engine/access/subscription/streamer.go +++ b/engine/access/subscription/streamer.go @@ -2,141 +2,8 @@ package subscription import ( "context" - "errors" - "fmt" - "time" - - "github.com/rs/zerolog" - "golang.org/x/time/rate" - - "github.com/onflow/flow-go/engine" ) -// ErrBlockNotReady represents an error indicating that a block is not yet available or ready. -var ErrBlockNotReady = errors.New("block not ready") - -// ErrEndOfData represents an error indicating that no more data available for streaming. -var ErrEndOfData = errors.New("end of data") - -// Streamer represents a streaming subscription that delivers data to clients. -type Streamer struct { - log zerolog.Logger - sub Streamable - broadcaster *engine.Broadcaster - sendTimeout time.Duration - limiter *rate.Limiter -} - -// NewStreamer creates a new Streamer instance. -func NewStreamer( - log zerolog.Logger, - broadcaster *engine.Broadcaster, - sendTimeout time.Duration, - limit float64, - sub Streamable, -) *Streamer { - var limiter *rate.Limiter - if limit > 0 { - // allows for 1 response per call, averaging `limit` responses per second over longer time frames - limiter = rate.NewLimiter(rate.Limit(limit), 1) - } - - return &Streamer{ - log: log.With().Str("sub_id", sub.ID()).Logger(), - broadcaster: broadcaster, - sendTimeout: sendTimeout, - limiter: limiter, - sub: sub, - } -} - -// Stream is a blocking method that streams data to the subscription until either the context is -// cancelled or it encounters an error. -// This function follows a somewhat unintuitive contract: if the context is canceled, -// it is treated as an error and written to the subscription. However, you can rely on -// this behavior in the subscription to handle it as a graceful shutdown. -func (s *Streamer) Stream(ctx context.Context) { - s.log.Debug().Msg("starting streaming") - defer s.log.Debug().Msg("finished streaming") - - notifier := engine.NewNotifier() - s.broadcaster.Subscribe(notifier) - - // always check the first time. This ensures that streaming continues to work even if the - // execution sync is not functioning (e.g. on a past spork network, or during an temporary outage) - notifier.Notify() - - for { - select { - case <-ctx.Done(): - s.sub.Fail(fmt.Errorf("client disconnected: %w", ctx.Err())) - return - case <-notifier.Channel(): - s.log.Debug().Msg("received broadcast notification") - } - - err := s.sendAllAvailable(ctx) - - if err != nil { - // TODO: The functionality to graceful shutdown on demand should be improved with https://github.com/onflow/flow-go/issues/5561 - if errors.Is(err, ErrEndOfData) { - s.sub.Close() - return - } - if errors.Is(err, context.Canceled) { - s.sub.Fail(fmt.Errorf("client disconnected: %w", ctx.Err())) - return - } - s.log.Err(err).Msg("error sending response") - s.sub.Fail(err) - return - } - } -} - -// sendAllAvailable reads data from the streamable and sends it to the client until no more data is available. -func (s *Streamer) sendAllAvailable(ctx context.Context) error { - for { - // blocking wait for the streamer's rate limit to have available capacity - if err := s.checkRateLimit(ctx); err != nil { - return fmt.Errorf("error waiting for response capacity: %w", err) - } - - response, err := s.sub.Next(ctx) - - if response == nil && err == nil { - continue - } - - if err != nil { - if errors.Is(err, ErrBlockNotReady) { - // no more available - return nil - } - - return fmt.Errorf("could not get response: %w", err) - } - - if ssub, ok := s.sub.(*HeightBasedSubscription); ok { - s.log.Trace(). - Uint64("next_height", ssub.nextHeight). - Msg("sending response") - } - - err = s.sub.Send(ctx, response, s.sendTimeout) - if err != nil { - return err - } - } -} - -// checkRateLimit checks the stream's rate limit and blocks until there is room to send a response. -// An error is returned if the context is canceled or the expected wait time exceeds the context's -// deadline. -func (s *Streamer) checkRateLimit(ctx context.Context) error { - if s.limiter == nil { - return nil - } - - return s.limiter.WaitN(ctx, 1) +type Streamer interface { + Stream(ctx context.Context) } diff --git a/engine/access/subscription/streamer/height_based_streamer.go b/engine/access/subscription/streamer/height_based_streamer.go new file mode 100644 index 00000000000..f57b59915d8 --- /dev/null +++ b/engine/access/subscription/streamer/height_based_streamer.go @@ -0,0 +1,137 @@ +package streamer + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/rs/zerolog" + "golang.org/x/time/rate" + + "github.com/onflow/flow-go/engine" + "github.com/onflow/flow-go/engine/access/subscription" +) + +type HeightBasedStreamer[T any] struct { + log zerolog.Logger + options *StreamOptions + limiter *rate.Limiter + subscription subscription.Subscription[T] + broadcaster *engine.Broadcaster + heightSource subscription.HeightSource[T] +} + +var _ subscription.Streamer = (*HeightBasedStreamer[any])(nil) + +func NewHeightBasedStreamer[T any]( + log zerolog.Logger, + broadcaster *engine.Broadcaster, + subscription subscription.Subscription[T], + heightSource subscription.HeightSource[T], + options *StreamOptions, +) *HeightBasedStreamer[T] { + var limiter *rate.Limiter + if options.ResponseLimit > 0 { + limiter = rate.NewLimiter(rate.Limit(options.ResponseLimit), 1) + } + + if options.Heartbeat <= 0 { + options.Heartbeat = 2 * time.Second + } + + return &HeightBasedStreamer[T]{ + log: log, + limiter: limiter, + options: options, + broadcaster: broadcaster, + subscription: subscription, + heightSource: heightSource, + } +} + +func (s *HeightBasedStreamer[T]) Stream(ctx context.Context) { + newDataAvailableNotifier := engine.NewNotifier() + s.broadcaster.Subscribe(newDataAvailableNotifier) //TODO: we never unsubscribe but it is expected? + newDataAvailableNotifier.Notify() + + heartbeatTicker := time.NewTicker(s.options.Heartbeat) // liveness fallback + defer heartbeatTicker.Stop() + + next := s.heightSource.StartHeight() + end := s.heightSource.EndHeight() + + // helper: send all currently available items before blocking + sendReady := func() (done bool) { + for { + // EOF. all data has been sent + if end != 0 && next > end { + s.subscription.Close() + return true + } + + readyTo, err := s.heightSource.ReadyUpToHeight() + if err != nil { + s.subscription.CloseWithError(err) + return true + } + if next > readyTo { + return false + } + + item, err := s.heightSource.GetItemAtHeight(ctx, next) + if err != nil { + if errors.Is(err, subscription.ErrBlockNotReady) { + return false // not ingested yet; try again later + } + // Treat end-of-data as a graceful completion + if errors.Is(err, subscription.ErrEndOfData) { + s.subscription.Close() + return true + } + + s.subscription.CloseWithError(err) + return true + } + + if err := s.send(ctx, item); err != nil { + s.subscription.CloseWithError(err) + return true + } + + next++ + } + } + + // Drain available items immediately on start so consumers don't wait for a notifier/heartbeat + if done := sendReady(); done { + return + } + + for { + select { + case <-ctx.Done(): + s.subscription.CloseWithError(fmt.Errorf("client disconnected: %w", ctx.Err())) + return + case <-newDataAvailableNotifier.Channel(): + case <-heartbeatTicker.C: + } + + if done := sendReady(); done { + return + } + } +} + +func (s *HeightBasedStreamer[T]) send(ctx context.Context, value T) error { + // if the limiter is not set, just send + if s.limiter == nil { + return s.subscription.Send(ctx, value, s.options.SendTimeout) + } + + if err := s.limiter.WaitN(ctx, 1); err != nil { + return fmt.Errorf("rate limit error: %w", err) + } + + return s.subscription.Send(ctx, value, s.options.SendTimeout) +} diff --git a/engine/access/subscription/streamer/height_based_streamer_test.go b/engine/access/subscription/streamer/height_based_streamer_test.go new file mode 100644 index 00000000000..41d6c185453 --- /dev/null +++ b/engine/access/subscription/streamer/height_based_streamer_test.go @@ -0,0 +1,179 @@ +package streamer + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/engine" + "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription/height_source" + subimpl "github.com/onflow/flow-go/engine/access/subscription/subscription" + "github.com/onflow/flow-go/utils/unittest" +) + +func collectUntilClosed[T any](ch <-chan T, timeout time.Duration) ([]T, error) { + var out []T + t := time.NewTimer(timeout) + defer t.Stop() + for { + select { + case v, ok := <-ch: + if !ok { + return out, nil + } + out = append(out, v) + case <-t.C: + return out, fmt.Errorf("timeout waiting for channel close") + } + } +} + +func TestHeightBasedStreamer_BoundedHappyPath(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer cancel() + + start := uint64(3) + end := uint64(10) + + // ready up to a large value + ready := func() (uint64, error) { return 100, nil } + get := func(ctx context.Context, h uint64) (int, error) { return int(h), nil } + + hs := height_source.NewHeightSource[int](start, end, ready, get) + sub := subimpl.NewSubscription[int](64) + + bc := engine.NewBroadcaster() + opts := NewDefaultStreamOptions() + opts.Heartbeat = 50 * time.Millisecond + opts.ResponseLimit = 0 + + s := NewHeightBasedStreamer[int](unittest.Logger(), bc, sub, hs, opts) + go s.Stream(ctx) + + // kick once + bc.Publish() + + items, err := collectUntilClosed(sub.Channel(), 2*time.Second) + require.NoError(t, err) + + // Expect [start..end] + expected := make([]int, 0, end-start+1) + for i := start; i <= end; i++ { + expected = append(expected, int(i)) + } + assert.Equal(t, expected, items) + assert.NoError(t, sub.Err()) +} + +func TestHeightBasedStreamer_ErrorsHandling(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer cancel() + + start := uint64(1) + end := uint64(5) + + sentinel := errors.New("boom") + ready := func() (uint64, error) { return 100, nil } + get := func(ctx context.Context, h uint64) (int, error) { + if h == 3 { + return 0, sentinel + } + return int(h), nil + } + + hs := height_source.NewHeightSource[int](start, end, ready, get) + sub := subimpl.NewSubscription[int](10) + bc := engine.NewBroadcaster() + opts := NewDefaultStreamOptions() + opts.Heartbeat = 20 * time.Millisecond + opts.ResponseLimit = 0 + + s := NewHeightBasedStreamer[int](unittest.Logger(), bc, sub, hs, opts) + go s.Stream(ctx) + + bc.Publish() + + // Read until channel closes + _, err := collectUntilClosed(sub.Channel(), 2*time.Second) + require.NoError(t, err) + assert.ErrorIs(t, sub.Err(), sentinel) +} + +func TestHeightBasedStreamer_NotIngestedThenAvailable(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) + defer cancel() + + start := uint64(1) + end := uint64(0) // unbounded + + var readyTo uint64 = 1 // only first item initially + ready := func() (uint64, error) { return readyTo, nil } + + var nextErrHeight uint64 = 2 + get := func(ctx context.Context, h uint64) (int, error) { + if h == nextErrHeight { + return 0, subscription.ErrBlockNotReady + } + return int(h), nil + } + + hs := height_source.NewHeightSource[int](start, end, ready, get) + sub := subimpl.NewSubscription[int](10) + bc := engine.NewBroadcaster() + opts := NewDefaultStreamOptions() + opts.Heartbeat = 50 * time.Millisecond + opts.ResponseLimit = 0 + + s := NewHeightBasedStreamer[int](unittest.Logger(), bc, sub, hs, opts) + go s.Stream(ctx) + + bc.Publish() + + // Expect to receive h=1 then block on h=2 not ingested. After we advance readiness and clear error, more data should flow. + select { + case v := <-sub.Channel(): + assert.Equal(t, 1, v) + case <-time.After(500 * time.Millisecond): + t.Fatalf("did not receive initial item") + } + + // Now make height 2 available + nextErrHeight = 0 // no longer error + readyTo = 3 // allow up to 3 + bc.Publish() + + // Receive 2 and 3 + got2 := false + got3 := false + for !(got2 && got3) { + select { + case v := <-sub.Channel(): + if v == 2 { + got2 = true + } + if v == 3 { + got3 = true + } + case <-time.After(1 * time.Second): + t.Fatalf("did not receive follow-up items in time") + } + } + + // cancel the stream to stop + cancel() + // streamer should close with error set + time.Sleep(50 * time.Millisecond) + assert.Error(t, sub.Err()) +} diff --git a/engine/access/subscription/streamer/options.go b/engine/access/subscription/streamer/options.go new file mode 100644 index 00000000000..3bb4bff4805 --- /dev/null +++ b/engine/access/subscription/streamer/options.go @@ -0,0 +1,43 @@ +package streamer + +import ( + "time" +) + +const ( + // DefaultSendBufferSize is the default buffer size for the subscription's send channel. + // The size is chosen to balance memory overhead from each subscription with performance when + // streaming existing data. + DefaultSendBufferSize = 10 + + // DefaultSendTimeout is the default timeout for sending a message to the client. After the timeout + // expires, the connection is closed. + DefaultSendTimeout = 30 * time.Second + + // DefaultResponseLimit is default max responses per second allowed on a stream. After exceeding + // the limit, the stream is paused until more capacity is available. + DefaultResponseLimit = 0 + + // DefaultHeartbeatInterval specifies the block interval at which heartbeat messages should be sent. + DefaultHeartbeatInterval = 1 +) + +type StreamOptions struct { + SendTimeout time.Duration + SendBufferSize int + Heartbeat time.Duration + + // ResponseLimit is the max responses per second allowed on a stream. After exceeding the limit, + // the stream is paused until more capacity is available. Searches of past data can be CPU + // intensive, so this helps manage the impact. + ResponseLimit int +} + +func NewDefaultStreamOptions() *StreamOptions { + return &StreamOptions{ + SendTimeout: DefaultSendTimeout, + SendBufferSize: DefaultSendBufferSize, + Heartbeat: DefaultHeartbeatInterval, + ResponseLimit: DefaultResponseLimit, + } +} diff --git a/engine/access/subscription/subscription.go b/engine/access/subscription/subscription.go index 3c5a12cee31..854bb543bd9 100644 --- a/engine/access/subscription/subscription.go +++ b/engine/access/subscription/subscription.go @@ -2,12 +2,7 @@ package subscription import ( "context" - "fmt" - "sync" "time" - - "github.com/google/uuid" - "google.golang.org/grpc/status" ) const ( @@ -34,159 +29,15 @@ const ( DefaultHeartbeatInterval = 1 ) -// GetDataByHeightFunc is a callback used by subscriptions to retrieve data for a given height. -// Expected errors: -// - storage.ErrNotFound -// - execution_data.BlobNotFoundError -// All other errors are considered exceptions -type GetDataByHeightFunc func(ctx context.Context, height uint64) (interface{}, error) +// TODO: I'm not sure subscription should be an interface. The impl will unlikely change. +// I guess it is because lots of code depends on it. We don't want to depend on impl details. -// Subscription represents a streaming request, and handles the communication between the grpc handler -// and the backend implementation. -type Subscription interface { - // ID returns the unique identifier for this subscription used for logging +type Subscription[T any] interface { ID() string - - // Channel returns the channel from which subscription data can be read - Channel() <-chan interface{} - - // Err returns the error that caused the subscription to fail + Channel() <-chan T Err() error -} -// Streamable represents a subscription that can be streamed. -type Streamable interface { - // ID returns the subscription ID - // Note: this is not a cryptographic hash - ID() string - // Close is called when a subscription ends gracefully, and closes the subscription channel + Send(ctx context.Context, value T, timeout time.Duration) error + CloseWithError(error) Close() - // Fail registers an error and closes the subscription channel - Fail(error) - // Send sends a value to the subscription channel or returns an error - // Expected errors: - // - context.DeadlineExceeded if send timed out - // - context.Canceled if the client disconnected - Send(context.Context, interface{}, time.Duration) error - // Next returns the value for the next height from the subscription - Next(context.Context) (interface{}, error) -} - -var _ Subscription = (*SubscriptionImpl)(nil) - -type SubscriptionImpl struct { - id string - - // ch is the channel used to pass data to the receiver - ch chan interface{} - - // err is the error that caused the subscription to fail - err error - - // once is used to ensure that the channel is only closed once - once sync.Once - - // closed tracks whether or not the subscription has been closed - closed bool -} - -func NewSubscription(bufferSize int) *SubscriptionImpl { - return &SubscriptionImpl{ - id: uuid.New().String(), - ch: make(chan interface{}, bufferSize), - } -} - -// ID returns the subscription ID -// Note: this is not a cryptographic hash -func (sub *SubscriptionImpl) ID() string { - return sub.id -} - -// Channel returns the channel from which subscription data can be read -func (sub *SubscriptionImpl) Channel() <-chan interface{} { - return sub.ch -} - -// Err returns the error that caused the subscription to fail -func (sub *SubscriptionImpl) Err() error { - return sub.err -} - -// Fail registers an error and closes the subscription channel -func (sub *SubscriptionImpl) Fail(err error) { - sub.err = err - sub.Close() -} - -// Close is called when a subscription ends gracefully, and closes the subscription channel -func (sub *SubscriptionImpl) Close() { - sub.once.Do(func() { - close(sub.ch) - sub.closed = true - }) -} - -// Send sends a value to the subscription channel or returns an error -// Expected errors: -// - context.DeadlineExceeded if send timed out -// - context.Canceled if the client disconnected -func (sub *SubscriptionImpl) Send(ctx context.Context, v interface{}, timeout time.Duration) error { - if sub.closed { - return fmt.Errorf("subscription closed") - } - - waitCtx, cancel := context.WithTimeout(ctx, timeout) - defer cancel() - - select { - case <-waitCtx.Done(): - return waitCtx.Err() - case sub.ch <- v: - return nil - } -} - -// NewFailedSubscription returns a new subscription that has already failed with the given error and -// message. This is useful to return an error that occurred during subscription setup. -func NewFailedSubscription(err error, msg string) *SubscriptionImpl { - sub := NewSubscription(0) - - // if error is a grpc error, wrap it to preserve the error code - if st, ok := status.FromError(err); ok { - sub.Fail(status.Errorf(st.Code(), "%s: %s", msg, st.Message())) - return sub - } - - // otherwise, return wrap the message normally - sub.Fail(fmt.Errorf("%s: %w", msg, err)) - return sub -} - -var _ Subscription = (*HeightBasedSubscription)(nil) -var _ Streamable = (*HeightBasedSubscription)(nil) - -// HeightBasedSubscription is a subscription that retrieves data sequentially by block height -type HeightBasedSubscription struct { - *SubscriptionImpl - nextHeight uint64 - getData GetDataByHeightFunc -} - -func NewHeightBasedSubscription(bufferSize int, firstHeight uint64, getData GetDataByHeightFunc) *HeightBasedSubscription { - return &HeightBasedSubscription{ - SubscriptionImpl: NewSubscription(bufferSize), - nextHeight: firstHeight, - getData: getData, - } -} - -// Next returns the value for the next height from the subscription -func (s *HeightBasedSubscription) Next(ctx context.Context) (interface{}, error) { - v, err := s.getData(ctx, s.nextHeight) - if err != nil { - return nil, fmt.Errorf("could not get data for height %d: %w", s.nextHeight, err) - } - s.nextHeight++ - return v, nil } diff --git a/engine/access/subscription/subscription/subscription.go b/engine/access/subscription/subscription/subscription.go new file mode 100644 index 00000000000..fb6af2e34b2 --- /dev/null +++ b/engine/access/subscription/subscription/subscription.go @@ -0,0 +1,89 @@ +package subscription + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/status" + + "github.com/onflow/flow-go/engine/access/subscription" +) + +// TODO: add a comment that Subscription is not thread safe + +type Subscription[T any] struct { + id string + ch chan T + err error + once sync.Once + closed bool +} + +var _ subscription.Subscription[any] = (*Subscription[any])(nil) + +func NewSubscription[T any](bufferSize int) *Subscription[T] { + if bufferSize <= 0 { + bufferSize = 1 + } + + return &Subscription[T]{ + id: uuid.New().String(), + ch: make(chan T, bufferSize), + } +} + +func NewFailedSubscription[T any](err error, msg string) *Subscription[T] { + sub := NewSubscription[T](0) + + // if the error is a grpc error, wrap it to preserve the error code + if st, ok := status.FromError(err); ok { + sub.CloseWithError(status.Errorf(st.Code(), "%s: %s", msg, st.Message())) + return sub + } + + sub.CloseWithError(fmt.Errorf("%s: %w", msg, err)) + return sub +} + +func (s *Subscription[T]) ID() string { + return s.id +} + +func (s *Subscription[T]) Channel() <-chan T { + return s.ch +} + +func (s *Subscription[T]) Err() error { + return s.err +} + +func (s *Subscription[T]) Close() { + // TODO: do we need to use sync.Once ? the abstraction is not thread safe anyway + s.once.Do(func() { + close(s.ch) + s.closed = true + }) +} +func (s *Subscription[T]) CloseWithError(err error) { + s.err = err + s.Close() +} + +func (s *Subscription[T]) Send(ctx context.Context, value T, timeout time.Duration) error { + if s.closed { + return fmt.Errorf("subscription closed") + } + + waitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + select { + case <-waitCtx.Done(): + return waitCtx.Err() + case s.ch <- value: + return nil + } +} diff --git a/engine/access/subscription/subscription/subscription_test.go b/engine/access/subscription/subscription/subscription_test.go new file mode 100644 index 00000000000..97aa5bf5085 --- /dev/null +++ b/engine/access/subscription/subscription/subscription_test.go @@ -0,0 +1,94 @@ +package subscription + +import ( + "context" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// TestSubscription_SendReceive verifies ordering and delivery over the channel. +func TestSubscription_SendReceive(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + sub := NewSubscription[string](1) + assert.NotEmpty(t, sub.ID()) + + messages := []string{"a", "b", "c", "d", "e"} + + // reader + recv := make([]string, 0, len(messages)) + done := make(chan struct{}) + go func() { + for v := range sub.Channel() { + recv = append(recv, v) + } + close(done) + }() + + for _, m := range messages { + require.NoError(t, sub.Send(ctx, m, 100*time.Millisecond)) + } + sub.Close() + + select { + case <-done: + case <-time.After(500 * time.Millisecond): + t.Fatalf("receiver did not finish in time") + } + + assert.NoError(t, sub.Err()) + assert.Equal(t, messages, recv) +} + +func TestSubscription_Failures(t *testing.T) { + t.Parallel() + + t.Run("close idempotent", func(t *testing.T) { + sub := NewSubscription[int](1) + sub.Close() + sub.Close() + assert.NoError(t, sub.Err()) + }) + + t.Run("close with error preserved", func(t *testing.T) { + sub := NewSubscription[int](1) + err := io.EOF + sub.CloseWithError(err) + assert.ErrorIs(t, sub.Err(), err) + // additional close should be safe + sub.Close() + }) + + t.Run("send after close returns error and keeps Err", func(t *testing.T) { + sub := NewSubscription[string](1) + sub.CloseWithError(io.ErrUnexpectedEOF) + err := sub.Send(context.Background(), "x", 10*time.Millisecond) + assert.Error(t, err) + assert.ErrorIs(t, sub.Err(), io.ErrUnexpectedEOF) + }) +} + +func TestNewFailedSubscription_PreservesGRPCStatus(t *testing.T) { + t.Parallel() + + base := status.Error(codes.InvalidArgument, "bad input") + sub := NewFailedSubscription[int](base, "wrap msg") + + err := sub.Err() + st, ok := status.FromError(err) + if !ok { + t.Fatalf("expected grpc status error, got: %v", err) + } + assert.Equal(t, codes.InvalidArgument, st.Code()) + // message should include our wrap text and original message + assert.Contains(t, st.Message(), "wrap msg") + assert.Contains(t, st.Message(), "bad input") +} diff --git a/engine/access/subscription/tracker.go b/engine/access/subscription/tracker.go new file mode 100644 index 00000000000..87de5ee99c8 --- /dev/null +++ b/engine/access/subscription/tracker.go @@ -0,0 +1,102 @@ +package subscription + +import ( + "context" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/executiondatasync/execution_data" +) + +// HeightTracker is an interface for a tracker that provides base GetStartHeight method related +// to both blocks and execution data tracking. +type HeightTracker interface { + // GetStartHeightFromBlockID returns the start height based on the provided starting block ID. + // If the start block is the root block, skip it and begin from the next block. + // + // Parameters: + // - startBlockID: The identifier of the starting block. + // + // Returns: + // - uint64: The start height associated with the provided block ID. + // - error: An error indicating any issues with retrieving the start height. + // + // Expected errors during normal operation: + // - codes.NotFound - if the block was not found in storage + // - codes.Internal - for any other error + GetStartHeightFromBlockID(flow.Identifier) (uint64, error) + + // GetStartHeightFromHeight returns the start height based on the provided starting block height. + // If the start block is the root block, skip it and begin from the next block. + // + // Parameters: + // - startHeight: The height of the starting block. + // + // Returns: + // - uint64: The start height associated with the provided block height. + // - error: An error indicating any issues with retrieving the start height. + // + // Expected errors during normal operation: + // - codes.InvalidArgument - if the start height is less than the root block height. + // - codes.NotFound - if the header was not found in storage. + GetStartHeightFromHeight(uint64) (uint64, error) + + // GetStartHeightFromLatest returns the start height based on the latest sealed block. + // If the start block is the root block, skip it and begin from the next block. + // + // Parameters: + // - ctx: Context for the operation. + // + // No errors are expected during normal operation. + GetStartHeightFromLatest(context.Context) (uint64, error) + + // GetStartHeight returns the start height to use when searching. + // Only one of startBlockID and startHeight may be set. Otherwise, an InvalidArgument error is returned. + // If a block is provided and does not exist, a NotFound error is returned. + // If neither startBlockID nor startHeight is provided, the latest sealed block is used. + // If the start block is the root block, skip it and begin from the next block. + // + // Parameters: + // - ctx: Context for the operation. + // - startBlockID: The identifier of the starting block. If provided, startHeight should be 0. + // - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. + // + // Returns: + // - uint64: The start height for searching. + // - error: An error indicating the result of the operation, if any. + // + // Expected errors during normal operation: + // - codes.InvalidArgument - if both startBlockID and startHeight are provided, + // if the start height is less than the root block height, + // if the start height is out of bounds based on indexed heights (when index is used). + // - codes.NotFound - if a block is provided and does not exist. + // - codes.Internal - if there is an internal error. + GetStartHeight(context.Context, flow.Identifier, uint64) (uint64, error) +} + +// BlockTracker is an interface for tracking blocks and handling block-related operations. +type BlockTracker interface { + HeightTracker + + // GetHighestHeight returns the highest height based on the specified block status which + // could be only BlockStatusSealed or BlockStatusFinalized. + // No errors are expected during normal operation. + GetHighestHeight(flow.BlockStatus) (uint64, error) + + // ProcessOnFinalizedBlock drives the subscription logic when a block is finalized. + // The input to this callback is treated as trusted. This method should be executed on + // `OnFinalizedBlock` notifications from the node-internal consensus instance. + // No errors are expected during normal operation. + ProcessOnFinalizedBlock() error +} + +// ExecutionDataTracker is an interface for tracking the highest consecutive block height for which we have received a +// new Execution Data notification +type ExecutionDataTracker interface { + HeightTracker + + // GetHighestHeight returns the highest height that we have consecutive execution data for. + GetHighestHeight() uint64 + + // OnExecutionData is used to notify the tracker when a new execution data is received. + OnExecutionData(*execution_data.BlockExecutionDataEntity) +} diff --git a/engine/access/subscription/tracker/block_tracker.go b/engine/access/subscription/tracker/block_tracker.go index ecdfa8744ca..34b1d11e3cd 100644 --- a/engine/access/subscription/tracker/block_tracker.go +++ b/engine/access/subscription/tracker/block_tracker.go @@ -5,6 +5,7 @@ import ( "google.golang.org/grpc/status" "github.com/onflow/flow-go/engine" + "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module/counters" "github.com/onflow/flow-go/module/irrecoverable" @@ -12,27 +13,10 @@ import ( "github.com/onflow/flow-go/storage" ) -// BlockTracker is an interface for tracking blocks and handling block-related operations. -type BlockTracker interface { - BaseTracker +// BlockTracker is an implementation of the BlockTracker interface. +type BlockTracker struct { + *HeightTracker - // GetHighestHeight returns the highest height based on the specified block status which could be only BlockStatusSealed - // or BlockStatusFinalized. - // No errors are expected during normal operation. - GetHighestHeight(flow.BlockStatus) (uint64, error) - - // ProcessOnFinalizedBlock drives the subscription logic when a block is finalized. - // The input to this callback is treated as trusted. This method should be executed on - // `OnFinalizedBlock` notifications from the node-internal consensus instance. - // No errors are expected during normal operation. - ProcessOnFinalizedBlock() error -} - -var _ BlockTracker = (*BlockTrackerImpl)(nil) - -// BlockTrackerImpl is an implementation of the BlockTracker interface. -type BlockTrackerImpl struct { - BaseTracker state protocol.State broadcaster *engine.Broadcaster @@ -42,15 +26,23 @@ type BlockTrackerImpl struct { sealedHighestHeight counters.StrictMonotonicCounter } -// NewBlockTracker creates a new BlockTrackerImpl instance. +var _ subscription.BlockTracker = (*BlockTracker)(nil) + +// NewBlockTracker creates a new BlockTracker instance. +// +// Parameters: +// - state: The protocol state used for retrieving block information. +// - rootHeight: The root block height, serving as the baseline for calculating the start height. +// - headers: The storage headers for accessing block headers. +// - broadcaster: The engine broadcaster for publishing notifications. // // No errors are expected during normal operation. func NewBlockTracker( state protocol.State, - sealedRootHeight uint64, + rootHeight uint64, headers storage.Headers, broadcaster *engine.Broadcaster, -) (*BlockTrackerImpl, error) { +) (*BlockTracker, error) { lastFinalized, err := state.Final().Head() if err != nil { // this header MUST exist in the db, otherwise the node likely has inconsistent state. @@ -63,8 +55,8 @@ func NewBlockTracker( return nil, irrecoverable.NewExceptionf("could not retrieve last sealed block: %w", err) } - return &BlockTrackerImpl{ - BaseTracker: NewBaseTrackerImpl(sealedRootHeight, state, headers), + return &BlockTracker{ + HeightTracker: NewHeightTracker(rootHeight, state, headers), state: state, finalizedHighestHeight: counters.NewMonotonicCounter(lastFinalized.Height), sealedHighestHeight: counters.NewMonotonicCounter(lastSealed.Height), @@ -79,14 +71,15 @@ func NewBlockTracker( // // Expected errors during normal operation: // - codes.InvalidArgument - if block status is flow.BlockStatusUnknown. -func (b *BlockTrackerImpl) GetHighestHeight(blockStatus flow.BlockStatus) (uint64, error) { +func (b *BlockTracker) GetHighestHeight(blockStatus flow.BlockStatus) (uint64, error) { switch blockStatus { case flow.BlockStatusFinalized: return b.finalizedHighestHeight.Value(), nil case flow.BlockStatusSealed: return b.sealedHighestHeight.Value(), nil + default: + return 0, status.Errorf(codes.InvalidArgument, "invalid block status: %s", blockStatus) } - return 0, status.Errorf(codes.InvalidArgument, "invalid block status: %s", blockStatus) } // ProcessOnFinalizedBlock drives the subscription logic when a block is finalized. @@ -94,7 +87,7 @@ func (b *BlockTrackerImpl) GetHighestHeight(blockStatus flow.BlockStatus) (uint6 // `OnFinalizedBlock` notifications from the node-internal consensus instance. // No errors are expected during normal operation. Any errors encountered should be // treated as an exception. -func (b *BlockTrackerImpl) ProcessOnFinalizedBlock() error { +func (b *BlockTracker) ProcessOnFinalizedBlock() error { // get the finalized header from state finalizedHeader, err := b.state.Final().Head() if err != nil { diff --git a/engine/access/subscription/tracker/execution_data_tracker.go b/engine/access/subscription/tracker/execution_data_tracker.go index f797e775652..59dd71cb63d 100644 --- a/engine/access/subscription/tracker/execution_data_tracker.go +++ b/engine/access/subscription/tracker/execution_data_tracker.go @@ -9,6 +9,7 @@ import ( "google.golang.org/grpc/status" "github.com/onflow/flow-go/engine" + "github.com/onflow/flow-go/engine/access/subscription" "github.com/onflow/flow-go/engine/common/rpc" "github.com/onflow/flow-go/fvm/errors" "github.com/onflow/flow-go/model/flow" @@ -29,44 +30,10 @@ const ( maxIndexBlockDiff = 30 ) -// ExecutionDataTracker is an interface for tracking the highest consecutive block height for which we have received a -// new Execution Data notification -type ExecutionDataTracker interface { - BaseTracker +// ExecutionDataTracker is an implementation of the ExecutionDataTracker interface. +type ExecutionDataTracker struct { + *HeightTracker - // GetStartHeight returns the start height to use when searching. - // Only one of startBlockID and startHeight may be set. Otherwise, an InvalidArgument error is returned. - // If a block is provided and does not exist, a NotFound error is returned. - // If neither startBlockID nor startHeight is provided, the latest sealed block is used. - // - // Parameters: - // - ctx: Context for the operation. - // - startBlockID: The identifier of the starting block. If provided, startHeight should be 0. - // - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. - // - // Returns: - // - uint64: The start height for searching. - // - error: An error indicating the result of the operation, if any. - // - // Expected errors during normal operation: - // - codes.InvalidArgument - if both startBlockID and startHeight are provided, if the start height is less than the root block height, - // if the start height is out of bounds based on indexed heights (when index is used). - // - codes.NotFound - if a block is provided and does not exist. - // - codes.Internal - if there is an internal error. - GetStartHeight(context.Context, flow.Identifier, uint64) (uint64, error) - - // GetHighestHeight returns the highest height that we have consecutive execution data for. - GetHighestHeight() uint64 - - // OnExecutionData is used to notify the tracker when a new execution data is received. - OnExecutionData(*execution_data.BlockExecutionDataEntity) -} - -var _ ExecutionDataTracker = (*ExecutionDataTrackerImpl)(nil) - -// ExecutionDataTrackerImpl is an implementation of the ExecutionDataTracker interface. -type ExecutionDataTrackerImpl struct { - BaseTracker log zerolog.Logger headers storage.Headers broadcaster *engine.Broadcaster @@ -77,7 +44,9 @@ type ExecutionDataTrackerImpl struct { highestHeight counters.StrictMonotonicCounter } -// NewExecutionDataTracker creates a new ExecutionDataTrackerImpl instance. +var _ subscription.ExecutionDataTracker = (*ExecutionDataTracker)(nil) + +// NewExecutionDataTracker creates a new ExecutionDataTracker instance. // // Parameters: // - log: The logger to use for logging. @@ -90,7 +59,7 @@ type ExecutionDataTrackerImpl struct { // - useIndex: A flag indicating whether to use indexed block heights for validation. // // Returns: -// - *ExecutionDataTrackerImpl: A new instance of ExecutionDataTrackerImpl. +// - *ExecutionDataTracker: A new instance of ExecutionDataTracker. func NewExecutionDataTracker( log zerolog.Logger, state protocol.State, @@ -100,9 +69,9 @@ func NewExecutionDataTracker( highestAvailableFinalizedHeight uint64, indexReporter state_synchronization.IndexReporter, useIndex bool, -) *ExecutionDataTrackerImpl { - return &ExecutionDataTrackerImpl{ - BaseTracker: NewBaseTrackerImpl(rootHeight, state, headers), +) *ExecutionDataTracker { + return &ExecutionDataTracker{ + HeightTracker: NewHeightTracker(rootHeight, state, headers), log: log, headers: headers, broadcaster: broadcaster, @@ -112,43 +81,6 @@ func NewExecutionDataTracker( } } -// GetStartHeight returns the start height to use when searching. -// Only one of startBlockID and startHeight may be set. Otherwise, an InvalidArgument error is returned. -// If a block is provided and does not exist, a NotFound error is returned. -// If neither startBlockID nor startHeight is provided, the latest sealed block is used. -// -// Parameters: -// - ctx: Context for the operation. -// - startBlockID: The identifier of the starting block. If provided, startHeight should be 0. -// - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. -// -// Returns: -// - uint64: The start height for searching. -// - error: An error indicating the result of the operation, if any. -// -// Expected errors during normal operation: -// - codes.InvalidArgument - if both startBlockID and startHeight are provided, if the start height is less than the root block height, -// if the start height is out of bounds based on indexed heights (when index is used). -// - codes.NotFound - if a block is provided and does not exist. -// - codes.Internal - if there is an internal error. -func (e *ExecutionDataTrackerImpl) GetStartHeight(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) (uint64, error) { - if startBlockID != flow.ZeroID && startHeight > 0 { - return 0, status.Errorf(codes.InvalidArgument, "only one of start block ID and start height may be provided") - } - - // get the start height based on the provided starting block ID - if startBlockID != flow.ZeroID { - return e.GetStartHeightFromBlockID(startBlockID) - } - - // get start height based on the provided starting block height - if startHeight > 0 { - return e.GetStartHeightFromHeight(startHeight) - } - - return e.GetStartHeightFromLatest(ctx) -} - // GetStartHeightFromBlockID returns the start height based on the provided starting block ID. // // Parameters: @@ -163,9 +95,9 @@ func (e *ExecutionDataTrackerImpl) GetStartHeight(ctx context.Context, startBloc // - codes.InvalidArgument - if the start height is out of bounds based on indexed heights. // - codes.FailedPrecondition - if the index reporter is not ready yet. // - codes.Internal - for any other error during validation. -func (e *ExecutionDataTrackerImpl) GetStartHeightFromBlockID(startBlockID flow.Identifier) (uint64, error) { +func (e *ExecutionDataTracker) GetStartHeightFromBlockID(startBlockID flow.Identifier) (uint64, error) { // get start height based on the provided starting block id - height, err := e.BaseTracker.GetStartHeightFromBlockID(startBlockID) + height, err := e.HeightTracker.GetStartHeightFromBlockID(startBlockID) if err != nil { return 0, err } @@ -188,9 +120,9 @@ func (e *ExecutionDataTrackerImpl) GetStartHeightFromBlockID(startBlockID flow.I // - codes.NotFound - if the header was not found in storage. // - codes.FailedPrecondition - if the index reporter is not ready yet. // - codes.Internal - for any other error during validation. -func (e *ExecutionDataTrackerImpl) GetStartHeightFromHeight(startHeight uint64) (uint64, error) { +func (e *ExecutionDataTracker) GetStartHeightFromHeight(startHeight uint64) (uint64, error) { // get start height based on the provided starting block height - height, err := e.BaseTracker.GetStartHeightFromHeight(startHeight) + height, err := e.HeightTracker.GetStartHeightFromHeight(startHeight) if err != nil { return 0, err } @@ -208,9 +140,9 @@ func (e *ExecutionDataTrackerImpl) GetStartHeightFromHeight(startHeight uint64) // - codes.InvalidArgument - if the start height is out of bounds based on indexed heights. // - codes.FailedPrecondition - if the index reporter is not ready yet. // - codes.Internal - for any other error during validation. -func (e *ExecutionDataTrackerImpl) GetStartHeightFromLatest(ctx context.Context) (uint64, error) { +func (e *ExecutionDataTracker) GetStartHeightFromLatest(ctx context.Context) (uint64, error) { // get start height based latest sealed block - height, err := e.BaseTracker.GetStartHeightFromLatest(ctx) + height, err := e.HeightTracker.GetStartHeightFromLatest(ctx) if err != nil { return 0, err } @@ -220,14 +152,13 @@ func (e *ExecutionDataTrackerImpl) GetStartHeightFromLatest(ctx context.Context) } // GetHighestHeight returns the highest height that we have consecutive execution data for. -func (e *ExecutionDataTrackerImpl) GetHighestHeight() uint64 { +func (e *ExecutionDataTracker) GetHighestHeight() uint64 { return e.highestHeight.Value() } -// OnExecutionData is used to notify the tracker when a new execution data is received. -func (e *ExecutionDataTrackerImpl) OnExecutionData(executionData *execution_data.BlockExecutionDataEntity) { +// OnExecutionData is used to notify the tracker when new execution data is received. +func (e *ExecutionDataTracker) OnExecutionData(executionData *execution_data.BlockExecutionDataEntity) { log := e.log.With().Hex("block_id", logging.ID(executionData.BlockID)).Logger() - log.Trace().Msg("received execution data") header, err := e.headers.ByBlockID(executionData.BlockID) @@ -265,7 +196,7 @@ func (e *ExecutionDataTrackerImpl) OnExecutionData(executionData *execution_data // - codes.InvalidArgument - if the start height is out of bounds based on indexed heights. // - codes.FailedPrecondition - if the index reporter is not ready yet. // - codes.Internal - for any other error during validation. -func (e *ExecutionDataTrackerImpl) checkStartHeight(height uint64) (uint64, error) { +func (e *ExecutionDataTracker) checkStartHeight(height uint64) (uint64, error) { if !e.useIndex { return height, nil } @@ -293,7 +224,7 @@ func (e *ExecutionDataTrackerImpl) checkStartHeight(height uint64) (uint64, erro // Expected errors during normal operation: // - codes.FailedPrecondition - if the index reporter is not ready yet. // - codes.Internal - if there was any other error getting the heights. -func (e *ExecutionDataTrackerImpl) getIndexedHeightBound() (uint64, uint64, error) { +func (e *ExecutionDataTracker) getIndexedHeightBound() (uint64, uint64, error) { lowestHeight, err := e.indexReporter.LowestIndexedHeight() if err != nil { if errors.Is(err, storage.ErrHeightNotIndexed) || errors.Is(err, indexer.ErrIndexNotInitialized) { diff --git a/engine/access/subscription/tracker/height_tracker.go b/engine/access/subscription/tracker/height_tracker.go new file mode 100644 index 00000000000..9fec6076e06 --- /dev/null +++ b/engine/access/subscription/tracker/height_tracker.go @@ -0,0 +1,179 @@ +package tracker + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/common/rpc" + + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/irrecoverable" + "github.com/onflow/flow-go/state/protocol" + "github.com/onflow/flow-go/storage" +) + +// HeightTracker is an implementation of the BaseTracker interface. +type HeightTracker struct { + rootBlockHeight uint64 + state protocol.State + headers storage.Headers +} + +var _ subscription.HeightTracker = (*HeightTracker)(nil) + +// NewHeightTracker creates a new instance of HeightTracker. +// +// Parameters: +// - rootBlockHeight: The root block height, which serves as the baseline for calculating the start height. +// - state: The protocol state used for retrieving block information. +// - headers: The storage headers for accessing block headers. +// +// Returns: +// - *HeightTracker: A new instance of HeightTracker. +func NewHeightTracker( + rootBlockHeight uint64, + state protocol.State, + headers storage.Headers, +) *HeightTracker { + return &HeightTracker{ + rootBlockHeight: rootBlockHeight, + state: state, + headers: headers, + } +} + +// GetStartHeightFromBlockID returns the start height based on the provided starting block ID. +// If the start block is the root block, skip it and begin from the next block. +// +// Parameters: +// - startBlockID: The identifier of the starting block. +// +// Returns: +// - uint64: The start height associated with the provided block ID. +// - error: An error indicating any issues with retrieving the start height. +// +// Expected errors during normal operation: +// - codes.NotFound - if the block was not found in storage +// - codes.Internal - for any other error +func (b *HeightTracker) GetStartHeightFromBlockID(startBlockID flow.Identifier) (uint64, error) { + header, err := b.headers.ByBlockID(startBlockID) + if err != nil { + return 0, rpc.ConvertStorageError(fmt.Errorf("could not get header for block %v: %w", startBlockID, err)) + } + + // ensure that the resolved start height is available + return b.getAdjustedHeight(header.Height), nil +} + +// GetStartHeightFromHeight returns the start height based on the provided starting block height. +// If the start block is the root block, skip it and begin from the next block. +// +// Parameters: +// - startHeight: The height of the starting block. +// +// Returns: +// - uint64: The start height associated with the provided block height. +// - error: An error indicating any issues with retrieving the start height. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if the start height is less than the root block height. +// - codes.NotFound - if the header was not found in storage. +func (b *HeightTracker) GetStartHeightFromHeight(startHeight uint64) (uint64, error) { + if startHeight < b.rootBlockHeight { + return 0, status.Errorf(codes.InvalidArgument, "start height must be greater than or equal to the root height %d", b.rootBlockHeight) + } + + header, err := b.headers.ByHeight(startHeight) + if err != nil { + return 0, rpc.ConvertStorageError(fmt.Errorf("could not get header for height %d: %w", startHeight, err)) + } + + // ensure that the resolved start height is available + return b.getAdjustedHeight(header.Height), nil +} + +// GetStartHeightFromLatest returns the start height based on the latest sealed block. +// If the start block is the root block, skip it and begin from the next block. +// +// Parameters: +// - ctx: Context for the operation. +// +// No errors are expected during normal operation. +func (b *HeightTracker) GetStartHeightFromLatest(ctx context.Context) (uint64, error) { + // if no start block was provided, use the latest sealed block + header, err := b.state.Sealed().Head() + if err != nil { + // In the RPC engine, if we encounter an error from the protocol state indicating state corruption, + // we should halt processing requests + err := irrecoverable.NewExceptionf("failed to lookup sealed header: %w", err) + irrecoverable.Throw(ctx, err) + return 0, err + } + + return b.getAdjustedHeight(header.Height), nil +} + +// getAdjustedHeight validates the provided start height and adjusts it if necessary. +// If the start block is the root block, skip it and begin from the next block. +// +// Parameters: +// - height: The start height to be checked. +// +// Returns: +// - uint64: The adjusted start height. +// +// No errors are expected during normal operation. +func (b *HeightTracker) getAdjustedHeight(height uint64) uint64 { + // if the start block is the root block, skip it and begin from the next block. + if height == b.rootBlockHeight { + height = b.rootBlockHeight + 1 + } + + return height +} + +// GetStartHeight returns the start height to use when searching. +// Only one of startBlockID and startHeight may be set. Otherwise, an InvalidArgument error is returned. +// If a block is provided and does not exist, a NotFound error is returned. +// If neither startBlockID nor startHeight is provided, the latest sealed block is used. +// If the start block is the root block, skip it and begin from the next block. +// +// Parameters: +// - ctx: Context for the operation. +// - startBlockID: The identifier of the starting block. If provided, startHeight should be 0. +// - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. +// +// Returns: +// - uint64: The start height for searching. +// - error: An error indicating the result of the operation, if any. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if both startBlockID and startHeight are provided, if the start height is less than the root block height, +// if the start height is out of bounds based on indexed heights (when index is used). +// - codes.NotFound - if a block is provided and does not exist. +// - codes.Internal - if there is an internal error. +func (b *HeightTracker) GetStartHeight( + ctx context.Context, + startBlockID flow.Identifier, + startHeight uint64, +) (uint64, error) { + if startBlockID != flow.ZeroID && startHeight > 0 { + return 0, status.Errorf(codes.InvalidArgument, "only one of start block ID and start height may be provided") + } + + // get the start height based on the provided starting block ID + if startBlockID != flow.ZeroID { + return b.GetStartHeightFromBlockID(startBlockID) + } + + // get start height based on the provided starting block height + if startHeight > 0 { + return b.GetStartHeightFromHeight(startHeight) + } + + return b.GetStartHeightFromLatest(ctx) +} diff --git a/engine/access/subscription/tracker/mock/block_tracker.go b/engine/access/subscription/tracker/mock/block_tracker.go index b1481656bd9..ec29fc4963e 100644 --- a/engine/access/subscription/tracker/mock/block_tracker.go +++ b/engine/access/subscription/tracker/mock/block_tracker.go @@ -1,4 +1,4 @@ -// Code generated by mockery. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mock diff --git a/engine/access/subscription/tracker/mock/execution_data_tracker.go b/engine/access/subscription/tracker/mock/execution_data_tracker.go index ccfad6bc8b4..04137a22cda 100644 --- a/engine/access/subscription/tracker/mock/execution_data_tracker.go +++ b/engine/access/subscription/tracker/mock/execution_data_tracker.go @@ -1,4 +1,4 @@ -// Code generated by mockery. DO NOT EDIT. +// Code generated by mockery v2.53.3. DO NOT EDIT. package mock diff --git a/engine/access/subscription/util.go b/engine/access/subscription/util.go index 6ecfeeb8b16..23eaf754af0 100644 --- a/engine/access/subscription/util.go +++ b/engine/access/subscription/util.go @@ -12,9 +12,9 @@ import ( // - handleResponse: The function responsible for handling the response of the subscribed type. // // No errors are expected during normal operations. -func HandleSubscription[T any](sub Subscription, handleResponse func(resp T) error) error { +func HandleSubscription[T any](sub Subscription[T], handleResponse func(resp T) error) error { for { - v, ok := <-sub.Channel() + resp, ok := <-sub.Channel() if !ok { if sub.Err() != nil { return fmt.Errorf("stream encountered an error: %w", sub.Err()) @@ -22,11 +22,6 @@ func HandleSubscription[T any](sub Subscription, handleResponse func(resp T) err return nil } - resp, ok := v.(T) - if !ok { - return fmt.Errorf("unexpected response type: %T", v) - } - err := handleResponse(resp) if err != nil { return err diff --git a/engine/access/subscription/mock/streamable.go b/engine/access/subscription_old/mock/streamable.go similarity index 100% rename from engine/access/subscription/mock/streamable.go rename to engine/access/subscription_old/mock/streamable.go diff --git a/engine/access/subscription_old/mock/subscription.go b/engine/access/subscription_old/mock/subscription.go new file mode 100644 index 00000000000..467cd80f7cc --- /dev/null +++ b/engine/access/subscription_old/mock/subscription.go @@ -0,0 +1,80 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import mock "github.com/stretchr/testify/mock" + +// Subscription is an autogenerated mock type for the Subscription type +type Subscription struct { + mock.Mock +} + +// Channel provides a mock function with no fields +func (_m *Subscription) Channel() <-chan interface{} { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Channel") + } + + var r0 <-chan interface{} + if rf, ok := ret.Get(0).(func() <-chan interface{}); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(<-chan interface{}) + } + } + + return r0 +} + +// Err provides a mock function with no fields +func (_m *Subscription) Err() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Err") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ID provides a mock function with no fields +func (_m *Subscription) ID() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ID") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// NewSubscription creates a new instance of Subscription. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSubscription(t interface { + mock.TestingT + Cleanup(func()) +}) *Subscription { + mock := &Subscription{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription_old/streamer.go b/engine/access/subscription_old/streamer.go new file mode 100644 index 00000000000..342842daecc --- /dev/null +++ b/engine/access/subscription_old/streamer.go @@ -0,0 +1,142 @@ +package subscription_old + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/rs/zerolog" + "golang.org/x/time/rate" + + "github.com/onflow/flow-go/engine" +) + +// ErrBlockNotReady represents an error indicating that a block is not yet available or ready. +var ErrBlockNotReady = errors.New("block not ready") + +// ErrEndOfData represents an error indicating that no more data available for streaming. +var ErrEndOfData = errors.New("end of data") + +// Streamer represents a streaming subscription that delivers data to clients. +type Streamer struct { + log zerolog.Logger + sub Streamable + broadcaster *engine.Broadcaster + sendTimeout time.Duration + limiter *rate.Limiter +} + +// NewStreamer creates a new Streamer instance. +func NewStreamer( + log zerolog.Logger, + broadcaster *engine.Broadcaster, + sendTimeout time.Duration, + limit float64, + sub Streamable, +) *Streamer { + var limiter *rate.Limiter + if limit > 0 { + // allows for 1 response per call, averaging `limit` responses per second over longer time frames + limiter = rate.NewLimiter(rate.Limit(limit), 1) + } + + return &Streamer{ + log: log.With().Str("sub_id", sub.ID()).Logger(), + broadcaster: broadcaster, + sendTimeout: sendTimeout, + limiter: limiter, + sub: sub, + } +} + +// Stream is a blocking method that streams data to the subscription until either the context is +// cancelled or it encounters an error. +// This function follows a somewhat unintuitive contract: if the context is canceled, +// it is treated as an error and written to the subscription. However, you can rely on +// this behavior in the subscription to handle it as a graceful shutdown. +func (s *Streamer) Stream(ctx context.Context) { + s.log.Debug().Msg("starting streaming") + defer s.log.Debug().Msg("finished streaming") + + notifier := engine.NewNotifier() + s.broadcaster.Subscribe(notifier) + + // always check the first time. This ensures that streaming continues to work even if the + // execution sync is not functioning (e.g. on a past spork network, or during an temporary outage) + notifier.Notify() + + for { + select { + case <-ctx.Done(): + s.sub.Fail(fmt.Errorf("client disconnected: %w", ctx.Err())) + return + case <-notifier.Channel(): + s.log.Debug().Msg("received broadcast notification") + } + + err := s.sendAllAvailable(ctx) + + if err != nil { + // TODO: The functionality to graceful shutdown on demand should be improved with https://github.com/onflow/flow-go/issues/5561 + if errors.Is(err, ErrEndOfData) { + s.sub.Close() + return + } + if errors.Is(err, context.Canceled) { + s.sub.Fail(fmt.Errorf("client disconnected: %w", ctx.Err())) + return + } + s.log.Err(err).Msg("error sending response") + s.sub.Fail(err) + return + } + } +} + +// sendAllAvailable reads data from the streamable and sends it to the client until no more data is available. +func (s *Streamer) sendAllAvailable(ctx context.Context) error { + for { + // blocking wait for the streamer's rate limit to have available capacity + if err := s.checkRateLimit(ctx); err != nil { + return fmt.Errorf("error waiting for response capacity: %w", err) + } + + response, err := s.sub.Next(ctx) + + if response == nil && err == nil { + continue + } + + if err != nil { + if errors.Is(err, ErrBlockNotReady) { + // no more available + return nil + } + + return fmt.Errorf("could not get response: %w", err) + } + + if ssub, ok := s.sub.(*HeightBasedSubscription); ok { + s.log.Trace(). + Uint64("next_height", ssub.nextHeight). + Msg("sending response") + } + + err = s.sub.Send(ctx, response, s.sendTimeout) + if err != nil { + return err + } + } +} + +// checkRateLimit checks the stream's rate limit and blocks until there is room to send a response. +// An error is returned if the context is canceled or the expected wait time exceeds the context's +// deadline. +func (s *Streamer) checkRateLimit(ctx context.Context) error { + if s.limiter == nil { + return nil + } + + return s.limiter.WaitN(ctx, 1) +} diff --git a/engine/access/subscription/streamer_test.go b/engine/access/subscription_old/streamer_test.go similarity index 86% rename from engine/access/subscription/streamer_test.go rename to engine/access/subscription_old/streamer_test.go index bc6bc7df72f..f8bc3d4e21a 100644 --- a/engine/access/subscription/streamer_test.go +++ b/engine/access/subscription_old/streamer_test.go @@ -1,4 +1,4 @@ -package subscription_test +package subscription_old_test import ( "context" @@ -11,8 +11,8 @@ import ( "github.com/stretchr/testify/mock" "github.com/onflow/flow-go/engine" - "github.com/onflow/flow-go/engine/access/subscription" - submock "github.com/onflow/flow-go/engine/access/subscription/mock" + "github.com/onflow/flow-go/engine/access/subscription_old" + submock "github.com/onflow/flow-go/engine/access/subscription_old/mock" "github.com/onflow/flow-go/utils/unittest" ) @@ -27,7 +27,7 @@ func TestStream(t *testing.T) { t.Parallel() ctx := context.Background() - timeout := subscription.DefaultSendTimeout + timeout := subscription_old.DefaultSendTimeout sub := submock.NewStreamable(t) sub.On("ID").Return(uuid.NewString()) @@ -39,7 +39,7 @@ func TestStream(t *testing.T) { tests = append(tests, testData{"", testErr}) broadcaster := engine.NewBroadcaster() - streamer := subscription.NewStreamer(unittest.Logger(), broadcaster, timeout, subscription.DefaultResponseLimit, sub) + streamer := subscription_old.NewStreamer(unittest.Logger(), broadcaster, timeout, subscription_old.DefaultResponseLimit, sub) for _, d := range tests { sub.On("Next", mock.Anything).Return(d.data, d.err).Once() @@ -64,7 +64,7 @@ func TestStreamRatelimited(t *testing.T) { t.Parallel() ctx := context.Background() - timeout := subscription.DefaultSendTimeout + timeout := subscription_old.DefaultSendTimeout duration := 100 * time.Millisecond for _, limit := range []float64{0.2, 3, 20, 500} { @@ -73,7 +73,7 @@ func TestStreamRatelimited(t *testing.T) { sub.On("ID").Return(uuid.NewString()) broadcaster := engine.NewBroadcaster() - streamer := subscription.NewStreamer(unittest.Logger(), broadcaster, timeout, limit, sub) + streamer := subscription_old.NewStreamer(unittest.Logger(), broadcaster, timeout, limit, sub) var nextCalls, sendCalls int sub.On("Next", mock.Anything).Return("data", nil).Run(func(args mock.Arguments) { @@ -115,7 +115,7 @@ func TestLongStreamRatelimited(t *testing.T) { unittest.SkipUnless(t, unittest.TEST_LONG_RUNNING, "skipping long stream rate limit test") ctx := context.Background() - timeout := subscription.DefaultSendTimeout + timeout := subscription_old.DefaultSendTimeout limit := 5.0 duration := 30 * time.Second @@ -124,7 +124,7 @@ func TestLongStreamRatelimited(t *testing.T) { sub.On("ID").Return(uuid.NewString()) broadcaster := engine.NewBroadcaster() - streamer := subscription.NewStreamer(unittest.Logger(), broadcaster, timeout, limit, sub) + streamer := subscription_old.NewStreamer(unittest.Logger(), broadcaster, timeout, limit, sub) var nextCalls, sendCalls int sub.On("Next", mock.Anything).Return("data", nil).Run(func(args mock.Arguments) { diff --git a/engine/access/subscription/streaming_data.go b/engine/access/subscription_old/streaming_data.go similarity index 93% rename from engine/access/subscription/streaming_data.go rename to engine/access/subscription_old/streaming_data.go index 90fc9d0f788..ac4ef0a9bef 100644 --- a/engine/access/subscription/streaming_data.go +++ b/engine/access/subscription_old/streaming_data.go @@ -1,4 +1,4 @@ -package subscription +package subscription_old import ( "sync/atomic" diff --git a/engine/access/subscription/subscribe_handler.go b/engine/access/subscription_old/subscribe_handler.go similarity index 98% rename from engine/access/subscription/subscribe_handler.go rename to engine/access/subscription_old/subscribe_handler.go index 7b72dffad8d..21358cc84e8 100644 --- a/engine/access/subscription/subscribe_handler.go +++ b/engine/access/subscription_old/subscribe_handler.go @@ -1,4 +1,4 @@ -package subscription +package subscription_old import ( "context" diff --git a/engine/access/subscription_old/subscription.go b/engine/access/subscription_old/subscription.go new file mode 100644 index 00000000000..91214517757 --- /dev/null +++ b/engine/access/subscription_old/subscription.go @@ -0,0 +1,192 @@ +package subscription_old + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/status" +) + +const ( + // DefaultSendBufferSize is the default buffer size for the subscription's send channel. + // The size is chosen to balance memory overhead from each subscription with performance when + // streaming existing data. + DefaultSendBufferSize = 10 + + // DefaultMaxGlobalStreams defines the default max number of streams that can be open at the same time. + DefaultMaxGlobalStreams = 1000 + + // DefaultCacheSize defines the default max number of objects for the execution data cache. + DefaultCacheSize = 100 + + // DefaultSendTimeout is the default timeout for sending a message to the client. After the timeout + // expires, the connection is closed. + DefaultSendTimeout = 30 * time.Second + + // DefaultResponseLimit is default max responses per second allowed on a stream. After exceeding + // the limit, the stream is paused until more capacity is available. + DefaultResponseLimit = float64(0) + + // DefaultHeartbeatInterval specifies the block interval at which heartbeat messages should be sent. + DefaultHeartbeatInterval = 1 +) + +// GetDataByHeightFunc is a callback used by subscriptions to retrieve data for a given height. +// Expected errors: +// - storage.ErrNotFound +// - execution_data.BlobNotFoundError +// All other errors are considered exceptions +type GetDataByHeightFunc func(ctx context.Context, height uint64) (interface{}, error) + +// Subscription represents a streaming request, and handles the communication between the grpc handler +// and the backend implementation. +type Subscription interface { + // ID returns the unique identifier for this subscription used for logging + ID() string + + // Channel returns the channel from which subscription data can be read + Channel() <-chan interface{} + + // Err returns the error that caused the subscription to fail + Err() error +} + +// Streamable represents a subscription that can be streamed. +type Streamable interface { + // ID returns the subscription ID + // Note: this is not a cryptographic hash + ID() string + // Close is called when a subscription ends gracefully, and closes the subscription channel + Close() + // Fail registers an error and closes the subscription channel + Fail(error) + // Send sends a value to the subscription channel or returns an error + // Expected errors: + // - context.DeadlineExceeded if send timed out + // - context.Canceled if the client disconnected + Send(context.Context, interface{}, time.Duration) error + // Next returns the value for the next height from the subscription + Next(context.Context) (interface{}, error) +} + +var _ Subscription = (*SubscriptionImpl)(nil) + +type SubscriptionImpl struct { + id string + + // ch is the channel used to pass data to the receiver + ch chan interface{} + + // err is the error that caused the subscription to fail + err error + + // once is used to ensure that the channel is only closed once + once sync.Once + + // closed tracks whether or not the subscription has been closed + closed bool +} + +func NewSubscription(bufferSize int) *SubscriptionImpl { + return &SubscriptionImpl{ + id: uuid.New().String(), + ch: make(chan interface{}, bufferSize), + } +} + +// ID returns the subscription ID +// Note: this is not a cryptographic hash +func (sub *SubscriptionImpl) ID() string { + return sub.id +} + +// Channel returns the channel from which subscription data can be read +func (sub *SubscriptionImpl) Channel() <-chan interface{} { + return sub.ch +} + +// Err returns the error that caused the subscription to fail +func (sub *SubscriptionImpl) Err() error { + return sub.err +} + +// Fail registers an error and closes the subscription channel +func (sub *SubscriptionImpl) Fail(err error) { + sub.err = err + sub.Close() +} + +// Close is called when a subscription ends gracefully, and closes the subscription channel +func (sub *SubscriptionImpl) Close() { + sub.once.Do(func() { + close(sub.ch) + sub.closed = true + }) +} + +// Send sends a value to the subscription channel or returns an error +// Expected errors: +// - context.DeadlineExceeded if send timed out +// - context.Canceled if the client disconnected +func (sub *SubscriptionImpl) Send(ctx context.Context, v interface{}, timeout time.Duration) error { + if sub.closed { + return fmt.Errorf("subscription closed") + } + + waitCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + select { + case <-waitCtx.Done(): + return waitCtx.Err() + case sub.ch <- v: + return nil + } +} + +// NewFailedSubscription returns a new subscription that has already failed with the given error and +// message. This is useful to return an error that occurred during subscription setup. +func NewFailedSubscription(err error, msg string) *SubscriptionImpl { + sub := NewSubscription(0) + + // if error is a grpc error, wrap it to preserve the error code + if st, ok := status.FromError(err); ok { + sub.Fail(status.Errorf(st.Code(), "%s: %s", msg, st.Message())) + return sub + } + + // otherwise, return wrap the message normally + sub.Fail(fmt.Errorf("%s: %w", msg, err)) + return sub +} + +var _ Subscription = (*HeightBasedSubscription)(nil) +var _ Streamable = (*HeightBasedSubscription)(nil) + +// HeightBasedSubscription is a subscription that retrieves data sequentially by block height +type HeightBasedSubscription struct { + *SubscriptionImpl + nextHeight uint64 + getData GetDataByHeightFunc +} + +func NewHeightBasedSubscription(bufferSize int, firstHeight uint64, getData GetDataByHeightFunc) *HeightBasedSubscription { + return &HeightBasedSubscription{ + SubscriptionImpl: NewSubscription(bufferSize), + nextHeight: firstHeight, + getData: getData, + } +} + +// Next returns the value for the next height from the subscription +func (s *HeightBasedSubscription) Next(ctx context.Context) (interface{}, error) { + v, err := s.getData(ctx, s.nextHeight) + if err != nil { + return nil, fmt.Errorf("could not get data for height %d: %w", s.nextHeight, err) + } + s.nextHeight++ + return v, nil +} diff --git a/engine/access/subscription/subscription_test.go b/engine/access/subscription_old/subscription_test.go similarity index 89% rename from engine/access/subscription/subscription_test.go rename to engine/access/subscription_old/subscription_test.go index a86422c17fd..33b294ec260 100644 --- a/engine/access/subscription/subscription_test.go +++ b/engine/access/subscription_old/subscription_test.go @@ -1,4 +1,4 @@ -package subscription_test +package subscription_old_test import ( "context" @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription_old" "github.com/onflow/flow-go/utils/unittest" ) @@ -20,7 +20,7 @@ func TestSubscription_SendReceive(t *testing.T) { ctx := context.Background() - sub := subscription.NewSubscription(1) + sub := subscription_old.NewSubscription(1) assert.NotEmpty(t, sub.ID()) @@ -66,7 +66,7 @@ func TestSubscription_Failures(t *testing.T) { // make sure closing a subscription twice does not cause a panic t.Run("close only called once", func(t *testing.T) { - sub := subscription.NewSubscription(1) + sub := subscription_old.NewSubscription(1) sub.Close() sub.Close() @@ -75,7 +75,7 @@ func TestSubscription_Failures(t *testing.T) { // make sure failing and closing the same subscription does not cause a panic t.Run("close only called once with fail", func(t *testing.T) { - sub := subscription.NewSubscription(1) + sub := subscription_old.NewSubscription(1) sub.Fail(testErr) sub.Close() @@ -84,7 +84,7 @@ func TestSubscription_Failures(t *testing.T) { // make sure an error is returned when sending on a closed subscription t.Run("send after closed returns an error", func(t *testing.T) { - sub := subscription.NewSubscription(1) + sub := subscription_old.NewSubscription(1) sub.Fail(testErr) err := sub.Send(context.Background(), "test", 10*time.Millisecond) @@ -117,7 +117,7 @@ func TestHeightBasedSubscription(t *testing.T) { } // search from [start, last], checking the correct data is returned - sub := subscription.NewHeightBasedSubscription(1, start, getData) + sub := subscription_old.NewHeightBasedSubscription(1, start, getData) for i := start; i <= last; i++ { data, err := sub.Next(ctx) if err != nil { diff --git a/engine/access/subscription/tracker/base_tracker.go b/engine/access/subscription_old/tracker/base_tracker.go similarity index 100% rename from engine/access/subscription/tracker/base_tracker.go rename to engine/access/subscription_old/tracker/base_tracker.go diff --git a/engine/access/subscription_old/tracker/block_tracker.go b/engine/access/subscription_old/tracker/block_tracker.go new file mode 100644 index 00000000000..ecdfa8744ca --- /dev/null +++ b/engine/access/subscription_old/tracker/block_tracker.go @@ -0,0 +1,119 @@ +package tracker + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onflow/flow-go/engine" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/counters" + "github.com/onflow/flow-go/module/irrecoverable" + "github.com/onflow/flow-go/state/protocol" + "github.com/onflow/flow-go/storage" +) + +// BlockTracker is an interface for tracking blocks and handling block-related operations. +type BlockTracker interface { + BaseTracker + + // GetHighestHeight returns the highest height based on the specified block status which could be only BlockStatusSealed + // or BlockStatusFinalized. + // No errors are expected during normal operation. + GetHighestHeight(flow.BlockStatus) (uint64, error) + + // ProcessOnFinalizedBlock drives the subscription logic when a block is finalized. + // The input to this callback is treated as trusted. This method should be executed on + // `OnFinalizedBlock` notifications from the node-internal consensus instance. + // No errors are expected during normal operation. + ProcessOnFinalizedBlock() error +} + +var _ BlockTracker = (*BlockTrackerImpl)(nil) + +// BlockTrackerImpl is an implementation of the BlockTracker interface. +type BlockTrackerImpl struct { + BaseTracker + state protocol.State + broadcaster *engine.Broadcaster + + // finalizedHighestHeight contains the highest consecutive block height for which we have received a new notification. + finalizedHighestHeight counters.StrictMonotonicCounter + // sealedHighestHeight contains the highest consecutive block height for which we have received a new notification. + sealedHighestHeight counters.StrictMonotonicCounter +} + +// NewBlockTracker creates a new BlockTrackerImpl instance. +// +// No errors are expected during normal operation. +func NewBlockTracker( + state protocol.State, + sealedRootHeight uint64, + headers storage.Headers, + broadcaster *engine.Broadcaster, +) (*BlockTrackerImpl, error) { + lastFinalized, err := state.Final().Head() + if err != nil { + // this header MUST exist in the db, otherwise the node likely has inconsistent state. + return nil, irrecoverable.NewExceptionf("could not retrieve last finalized block: %w", err) + } + + lastSealed, err := state.Sealed().Head() + if err != nil { + // this header MUST exist in the db, otherwise the node likely has inconsistent state. + return nil, irrecoverable.NewExceptionf("could not retrieve last sealed block: %w", err) + } + + return &BlockTrackerImpl{ + BaseTracker: NewBaseTrackerImpl(sealedRootHeight, state, headers), + state: state, + finalizedHighestHeight: counters.NewMonotonicCounter(lastFinalized.Height), + sealedHighestHeight: counters.NewMonotonicCounter(lastSealed.Height), + broadcaster: broadcaster, + }, nil +} + +// GetHighestHeight returns the highest height based on the specified block status. +// +// Parameters: +// - blockStatus: The status of the block. It is expected that blockStatus has already been handled for invalid flow.BlockStatusUnknown. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if block status is flow.BlockStatusUnknown. +func (b *BlockTrackerImpl) GetHighestHeight(blockStatus flow.BlockStatus) (uint64, error) { + switch blockStatus { + case flow.BlockStatusFinalized: + return b.finalizedHighestHeight.Value(), nil + case flow.BlockStatusSealed: + return b.sealedHighestHeight.Value(), nil + } + return 0, status.Errorf(codes.InvalidArgument, "invalid block status: %s", blockStatus) +} + +// ProcessOnFinalizedBlock drives the subscription logic when a block is finalized. +// The input to this callback is treated as trusted. This method should be executed on +// `OnFinalizedBlock` notifications from the node-internal consensus instance. +// No errors are expected during normal operation. Any errors encountered should be +// treated as an exception. +func (b *BlockTrackerImpl) ProcessOnFinalizedBlock() error { + // get the finalized header from state + finalizedHeader, err := b.state.Final().Head() + if err != nil { + return irrecoverable.NewExceptionf("unable to get latest finalized header: %w", err) + } + + if !b.finalizedHighestHeight.Set(finalizedHeader.Height) { + return nil + } + + // get the latest seal header from state + sealedHeader, err := b.state.Sealed().Head() + if err != nil { + return irrecoverable.NewExceptionf("unable to get latest sealed header: %w", err) + } + + _ = b.sealedHighestHeight.Set(sealedHeader.Height) + // always publish since there is also a new finalized block. + b.broadcaster.Publish() + + return nil +} diff --git a/engine/access/subscription_old/tracker/execution_data_tracker.go b/engine/access/subscription_old/tracker/execution_data_tracker.go new file mode 100644 index 00000000000..f797e775652 --- /dev/null +++ b/engine/access/subscription_old/tracker/execution_data_tracker.go @@ -0,0 +1,316 @@ +package tracker + +import ( + "context" + + "github.com/rs/zerolog" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onflow/flow-go/engine" + "github.com/onflow/flow-go/engine/common/rpc" + "github.com/onflow/flow-go/fvm/errors" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/module/counters" + "github.com/onflow/flow-go/module/executiondatasync/execution_data" + "github.com/onflow/flow-go/module/state_synchronization" + "github.com/onflow/flow-go/module/state_synchronization/indexer" + "github.com/onflow/flow-go/state/protocol" + "github.com/onflow/flow-go/storage" + "github.com/onflow/flow-go/utils/logging" +) + +const ( + // maxIndexBlockDiff is the maximum difference between the highest indexed block height and the + // provided start height to allow when starting a new stream. + // this is used to account for small delays indexing or requests made to different ANs behind a + // load balancer. The diff will result in the stream waiting a few blocks before starting. + maxIndexBlockDiff = 30 +) + +// ExecutionDataTracker is an interface for tracking the highest consecutive block height for which we have received a +// new Execution Data notification +type ExecutionDataTracker interface { + BaseTracker + + // GetStartHeight returns the start height to use when searching. + // Only one of startBlockID and startHeight may be set. Otherwise, an InvalidArgument error is returned. + // If a block is provided and does not exist, a NotFound error is returned. + // If neither startBlockID nor startHeight is provided, the latest sealed block is used. + // + // Parameters: + // - ctx: Context for the operation. + // - startBlockID: The identifier of the starting block. If provided, startHeight should be 0. + // - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. + // + // Returns: + // - uint64: The start height for searching. + // - error: An error indicating the result of the operation, if any. + // + // Expected errors during normal operation: + // - codes.InvalidArgument - if both startBlockID and startHeight are provided, if the start height is less than the root block height, + // if the start height is out of bounds based on indexed heights (when index is used). + // - codes.NotFound - if a block is provided and does not exist. + // - codes.Internal - if there is an internal error. + GetStartHeight(context.Context, flow.Identifier, uint64) (uint64, error) + + // GetHighestHeight returns the highest height that we have consecutive execution data for. + GetHighestHeight() uint64 + + // OnExecutionData is used to notify the tracker when a new execution data is received. + OnExecutionData(*execution_data.BlockExecutionDataEntity) +} + +var _ ExecutionDataTracker = (*ExecutionDataTrackerImpl)(nil) + +// ExecutionDataTrackerImpl is an implementation of the ExecutionDataTracker interface. +type ExecutionDataTrackerImpl struct { + BaseTracker + log zerolog.Logger + headers storage.Headers + broadcaster *engine.Broadcaster + indexReporter state_synchronization.IndexReporter + useIndex bool + + // highestHeight contains the highest consecutive block height that we have consecutive execution data for + highestHeight counters.StrictMonotonicCounter +} + +// NewExecutionDataTracker creates a new ExecutionDataTrackerImpl instance. +// +// Parameters: +// - log: The logger to use for logging. +// - state: The protocol state used for retrieving block information. +// - rootHeight: The root block height, serving as the baseline for calculating the start height. +// - headers: The storage headers for accessing block headers. +// - broadcaster: The engine broadcaster for publishing notifications. +// - highestAvailableFinalizedHeight: The highest available finalized block height. +// - indexReporter: The index reporter for checking indexed block heights. +// - useIndex: A flag indicating whether to use indexed block heights for validation. +// +// Returns: +// - *ExecutionDataTrackerImpl: A new instance of ExecutionDataTrackerImpl. +func NewExecutionDataTracker( + log zerolog.Logger, + state protocol.State, + rootHeight uint64, + headers storage.Headers, + broadcaster *engine.Broadcaster, + highestAvailableFinalizedHeight uint64, + indexReporter state_synchronization.IndexReporter, + useIndex bool, +) *ExecutionDataTrackerImpl { + return &ExecutionDataTrackerImpl{ + BaseTracker: NewBaseTrackerImpl(rootHeight, state, headers), + log: log, + headers: headers, + broadcaster: broadcaster, + highestHeight: counters.NewMonotonicCounter(highestAvailableFinalizedHeight), + indexReporter: indexReporter, + useIndex: useIndex, + } +} + +// GetStartHeight returns the start height to use when searching. +// Only one of startBlockID and startHeight may be set. Otherwise, an InvalidArgument error is returned. +// If a block is provided and does not exist, a NotFound error is returned. +// If neither startBlockID nor startHeight is provided, the latest sealed block is used. +// +// Parameters: +// - ctx: Context for the operation. +// - startBlockID: The identifier of the starting block. If provided, startHeight should be 0. +// - startHeight: The height of the starting block. If provided, startBlockID should be flow.ZeroID. +// +// Returns: +// - uint64: The start height for searching. +// - error: An error indicating the result of the operation, if any. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if both startBlockID and startHeight are provided, if the start height is less than the root block height, +// if the start height is out of bounds based on indexed heights (when index is used). +// - codes.NotFound - if a block is provided and does not exist. +// - codes.Internal - if there is an internal error. +func (e *ExecutionDataTrackerImpl) GetStartHeight(ctx context.Context, startBlockID flow.Identifier, startHeight uint64) (uint64, error) { + if startBlockID != flow.ZeroID && startHeight > 0 { + return 0, status.Errorf(codes.InvalidArgument, "only one of start block ID and start height may be provided") + } + + // get the start height based on the provided starting block ID + if startBlockID != flow.ZeroID { + return e.GetStartHeightFromBlockID(startBlockID) + } + + // get start height based on the provided starting block height + if startHeight > 0 { + return e.GetStartHeightFromHeight(startHeight) + } + + return e.GetStartHeightFromLatest(ctx) +} + +// GetStartHeightFromBlockID returns the start height based on the provided starting block ID. +// +// Parameters: +// - startBlockID: The identifier of the starting block. +// +// Returns: +// - uint64: The start height associated with the provided block ID. +// - error: An error indicating any issues with retrieving the start height. +// +// Expected errors during normal operation: +// - codes.NotFound - if the block was not found in storage +// - codes.InvalidArgument - if the start height is out of bounds based on indexed heights. +// - codes.FailedPrecondition - if the index reporter is not ready yet. +// - codes.Internal - for any other error during validation. +func (e *ExecutionDataTrackerImpl) GetStartHeightFromBlockID(startBlockID flow.Identifier) (uint64, error) { + // get start height based on the provided starting block id + height, err := e.BaseTracker.GetStartHeightFromBlockID(startBlockID) + if err != nil { + return 0, err + } + + // ensure that the resolved start height is available + return e.checkStartHeight(height) +} + +// GetStartHeightFromHeight returns the start height based on the provided starting block height. +// +// Parameters: +// - startHeight: The height of the starting block. +// +// Returns: +// - uint64: The start height associated with the provided block height. +// - error: An error indicating any issues with retrieving the start height. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if the start height is less than the root block height, if the start height is out of bounds based on indexed heights +// - codes.NotFound - if the header was not found in storage. +// - codes.FailedPrecondition - if the index reporter is not ready yet. +// - codes.Internal - for any other error during validation. +func (e *ExecutionDataTrackerImpl) GetStartHeightFromHeight(startHeight uint64) (uint64, error) { + // get start height based on the provided starting block height + height, err := e.BaseTracker.GetStartHeightFromHeight(startHeight) + if err != nil { + return 0, err + } + + // ensure that the resolved start height is available + return e.checkStartHeight(height) +} + +// GetStartHeightFromLatest returns the start height based on the latest sealed block. +// +// Parameters: +// - ctx: Context for the operation. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if the start height is out of bounds based on indexed heights. +// - codes.FailedPrecondition - if the index reporter is not ready yet. +// - codes.Internal - for any other error during validation. +func (e *ExecutionDataTrackerImpl) GetStartHeightFromLatest(ctx context.Context) (uint64, error) { + // get start height based latest sealed block + height, err := e.BaseTracker.GetStartHeightFromLatest(ctx) + if err != nil { + return 0, err + } + + // ensure that the resolved start height is available + return e.checkStartHeight(height) +} + +// GetHighestHeight returns the highest height that we have consecutive execution data for. +func (e *ExecutionDataTrackerImpl) GetHighestHeight() uint64 { + return e.highestHeight.Value() +} + +// OnExecutionData is used to notify the tracker when a new execution data is received. +func (e *ExecutionDataTrackerImpl) OnExecutionData(executionData *execution_data.BlockExecutionDataEntity) { + log := e.log.With().Hex("block_id", logging.ID(executionData.BlockID)).Logger() + + log.Trace().Msg("received execution data") + + header, err := e.headers.ByBlockID(executionData.BlockID) + if err != nil { + // if the execution data is available, the block must be locally finalized + log.Fatal().Err(err).Msg("failed to notify of new execution data") + return + } + + // sets the highest height for which execution data is available. + _ = e.highestHeight.Set(header.Height) + + e.broadcaster.Publish() +} + +// checkStartHeight validates the provided start height and adjusts it if necessary based on the tracker's configuration. +// +// Parameters: +// - height: The start height to be checked. +// +// Returns: +// - uint64: The adjusted start height, if validation passes. +// - error: An error indicating any issues with the provided start height. +// +// Validation Steps: +// 1. If index usage is disabled, return the original height without further checks. +// 2. Retrieve the lowest and highest indexed block heights. +// 3. Check if the provided height is within the bounds of indexed heights. +// - If below the lowest indexed height, return codes.InvalidArgument error. +// - If above the highest indexed height, return codes.InvalidArgument error. +// +// 4. If validation passes, return the adjusted start height. +// +// Expected errors during normal operation: +// - codes.InvalidArgument - if the start height is out of bounds based on indexed heights. +// - codes.FailedPrecondition - if the index reporter is not ready yet. +// - codes.Internal - for any other error during validation. +func (e *ExecutionDataTrackerImpl) checkStartHeight(height uint64) (uint64, error) { + if !e.useIndex { + return height, nil + } + + lowestHeight, highestHeight, err := e.getIndexedHeightBound() + if err != nil { + return 0, err + } + + if height < lowestHeight { + return 0, status.Errorf(codes.InvalidArgument, "start height %d is lower than lowest indexed height %d", height, lowestHeight) + } + + // allow for a small difference between the highest indexed height and the provided height to + // account for small delays indexing or requests made to different ANs behind a load balancer. + // this will just result in the stream waiting a few blocks before starting. + if height > highestHeight+maxIndexBlockDiff { + return 0, status.Errorf(codes.InvalidArgument, "start height %d is higher than highest indexed height %d (maxIndexBlockDiff: %d)", height, highestHeight, maxIndexBlockDiff) + } + + return height, nil +} + +// getIndexedHeightBound returns the lowest and highest indexed block heights +// Expected errors during normal operation: +// - codes.FailedPrecondition - if the index reporter is not ready yet. +// - codes.Internal - if there was any other error getting the heights. +func (e *ExecutionDataTrackerImpl) getIndexedHeightBound() (uint64, uint64, error) { + lowestHeight, err := e.indexReporter.LowestIndexedHeight() + if err != nil { + if errors.Is(err, storage.ErrHeightNotIndexed) || errors.Is(err, indexer.ErrIndexNotInitialized) { + // the index is not ready yet, but likely will be eventually + return 0, 0, status.Errorf(codes.FailedPrecondition, "failed to get lowest indexed height: %v", err) + } + return 0, 0, rpc.ConvertError(err, "failed to get lowest indexed height", codes.Internal) + } + + highestHeight, err := e.indexReporter.HighestIndexedHeight() + if err != nil { + if errors.Is(err, storage.ErrHeightNotIndexed) || errors.Is(err, indexer.ErrIndexNotInitialized) { + // the index is not ready yet, but likely will be eventually + return 0, 0, status.Errorf(codes.FailedPrecondition, "failed to get highest indexed height: %v", err) + } + return 0, 0, rpc.ConvertError(err, "failed to get highest indexed height", codes.Internal) + } + + return lowestHeight, highestHeight, nil +} diff --git a/engine/access/subscription/tracker/mock/base_tracker.go b/engine/access/subscription_old/tracker/mock/base_tracker.go similarity index 100% rename from engine/access/subscription/tracker/mock/base_tracker.go rename to engine/access/subscription_old/tracker/mock/base_tracker.go diff --git a/engine/access/subscription_old/tracker/mock/block_tracker.go b/engine/access/subscription_old/tracker/mock/block_tracker.go new file mode 100644 index 00000000000..b1481656bd9 --- /dev/null +++ b/engine/access/subscription_old/tracker/mock/block_tracker.go @@ -0,0 +1,159 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" +) + +// BlockTracker is an autogenerated mock type for the BlockTracker type +type BlockTracker struct { + mock.Mock +} + +// GetHighestHeight provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetHighestHeight(_a0 flow.BlockStatus) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetHighestHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.BlockStatus) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.BlockStatus) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.BlockStatus) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromBlockID provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetStartHeightFromBlockID(_a0 flow.Identifier) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromBlockID") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.Identifier) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.Identifier) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.Identifier) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromHeight provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetStartHeightFromHeight(_a0 uint64) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(uint64) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromLatest provides a mock function with given fields: _a0 +func (_m *BlockTracker) GetStartHeightFromLatest(_a0 context.Context) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromLatest") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ProcessOnFinalizedBlock provides a mock function with no fields +func (_m *BlockTracker) ProcessOnFinalizedBlock() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ProcessOnFinalizedBlock") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewBlockTracker creates a new instance of BlockTracker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBlockTracker(t interface { + mock.TestingT + Cleanup(func()) +}) *BlockTracker { + mock := &BlockTracker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription_old/tracker/mock/execution_data_tracker.go b/engine/access/subscription_old/tracker/mock/execution_data_tracker.go new file mode 100644 index 00000000000..ccfad6bc8b4 --- /dev/null +++ b/engine/access/subscription_old/tracker/mock/execution_data_tracker.go @@ -0,0 +1,166 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + flow "github.com/onflow/flow-go/model/flow" + execution_data "github.com/onflow/flow-go/module/executiondatasync/execution_data" + + mock "github.com/stretchr/testify/mock" +) + +// ExecutionDataTracker is an autogenerated mock type for the ExecutionDataTracker type +type ExecutionDataTracker struct { + mock.Mock +} + +// GetHighestHeight provides a mock function with no fields +func (_m *ExecutionDataTracker) GetHighestHeight() uint64 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetHighestHeight") + } + + var r0 uint64 + if rf, ok := ret.Get(0).(func() uint64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint64) + } + + return r0 +} + +// GetStartHeight provides a mock function with given fields: _a0, _a1, _a2 +func (_m *ExecutionDataTracker) GetStartHeight(_a0 context.Context, _a1 flow.Identifier, _a2 uint64) (uint64, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) (uint64, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, flow.Identifier, uint64) uint64); ok { + r0 = rf(_a0, _a1, _a2) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, flow.Identifier, uint64) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromBlockID provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) GetStartHeightFromBlockID(_a0 flow.Identifier) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromBlockID") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(flow.Identifier) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(flow.Identifier) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(flow.Identifier) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromHeight provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) GetStartHeightFromHeight(_a0 uint64) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromHeight") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(uint64) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(uint64) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(uint64) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetStartHeightFromLatest provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) GetStartHeightFromLatest(_a0 context.Context) (uint64, error) { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for GetStartHeightFromLatest") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { + return rf(_a0) + } + if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { + r0 = rf(_a0) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OnExecutionData provides a mock function with given fields: _a0 +func (_m *ExecutionDataTracker) OnExecutionData(_a0 *execution_data.BlockExecutionDataEntity) { + _m.Called(_a0) +} + +// NewExecutionDataTracker creates a new instance of ExecutionDataTracker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewExecutionDataTracker(t interface { + mock.TestingT + Cleanup(func()) +}) *ExecutionDataTracker { + mock := &ExecutionDataTracker{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/engine/access/subscription_old/util.go b/engine/access/subscription_old/util.go new file mode 100644 index 00000000000..4b69219a2d2 --- /dev/null +++ b/engine/access/subscription_old/util.go @@ -0,0 +1,57 @@ +package subscription_old + +import ( + "fmt" +) + +// HandleSubscription is a generic handler for subscriptions to a specific type. It continuously listens to the subscription channel, +// handles the received responses, and sends the processed information to the client via the provided stream using handleResponse. +// +// Parameters: +// - sub: The subscription. +// - handleResponse: The function responsible for handling the response of the subscribed type. +// +// No errors are expected during normal operations. +func HandleSubscription[T any](sub Subscription, handleResponse func(resp T) error) error { + for { + v, ok := <-sub.Channel() + if !ok { + if sub.Err() != nil { + return fmt.Errorf("stream encountered an error: %w", sub.Err()) + } + return nil + } + + resp, ok := v.(T) + if !ok { + return fmt.Errorf("unexpected response type: %T", v) + } + + err := handleResponse(resp) + if err != nil { + return err + } + } +} + +// HandleResponse processes a generic response of type and sends it to the provided channel. +// +// Parameters: +// - send: The channel to which the processed response is sent. +// - transform: A function to transform the response into the expected interface{} type. +// +// No errors are expected during normal operations. +func HandleResponse[T any](send chan<- interface{}, transform func(resp T) (interface{}, error)) func(resp T) error { + return func(response T) error { + // Transform the response + resp, err := transform(response) + if err != nil { + return fmt.Errorf("failed to transform response: %w", err) + } + + // send to the channel + send <- resp + + return nil + } +} diff --git a/integration/go.sum b/integration/go.sum index 9764f7b054b..4e4cb76ec09 100644 --- a/integration/go.sum +++ b/integration/go.sum @@ -1425,8 +1425,6 @@ google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuO google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU= google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250804133106-a7a43d27e69b h1:YzmLjVBzUKrr0zPM1KkGPEicd3WHSccw1k9RivnvngU= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:h6yxum/C2qRb4txaZRLDHK8RyS0H/o2oEDeKY4onY/Y= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8= google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= diff --git a/integration/tests/access/cohort4/grpc_state_stream_test.go b/integration/tests/access/cohort4/grpc_state_stream_test.go index 6c414b4d5e1..879b46861b7 100644 --- a/integration/tests/access/cohort4/grpc_state_stream_test.go +++ b/integration/tests/access/cohort4/grpc_state_stream_test.go @@ -17,7 +17,7 @@ import ( "github.com/onflow/flow-go-sdk/test" - "github.com/onflow/flow-go/engine/access/state_stream/backend" + "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/engine/ghost/client" "github.com/onflow/flow-go/integration/testnet" @@ -38,7 +38,7 @@ var ( // SubscribeEventsResponse represents the subscription response containing events for a specific block and messageIndex type SubscribeEventsResponse struct { - backend.EventsResponse + state_stream.EventsResponse MessageIndex uint64 } @@ -427,7 +427,7 @@ func eventsResponseHandler(msg *executiondata.SubscribeEventsResponse) (*Subscri } return &SubscribeEventsResponse{ - EventsResponse: backend.EventsResponse{ + EventsResponse: state_stream.EventsResponse{ Height: msg.GetBlockHeight(), BlockID: convert.MessageToIdentifier(msg.GetBlockId()), Events: events, diff --git a/integration/tests/access/cohort4/rest_state_stream_test.go b/integration/tests/access/cohort4/rest_state_stream_test.go index a268d815948..e5686390b67 100644 --- a/integration/tests/access/cohort4/rest_state_stream_test.go +++ b/integration/tests/access/cohort4/rest_state_stream_test.go @@ -16,7 +16,7 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/onflow/flow-go/engine/access/rest/http/request" - "github.com/onflow/flow-go/engine/access/state_stream/backend" + "github.com/onflow/flow-go/engine/access/state_stream" "github.com/onflow/flow-go/engine/common/rpc/convert" "github.com/onflow/flow-go/integration/testnet" "github.com/onflow/flow-go/integration/tests/access/common" @@ -115,7 +115,7 @@ func (s *RestStateStreamSuite) TestRestEventStreaming() { client, err := common.GetWSClient(s.ctx, url) require.NoError(t, err) - var receivedEventsResponse []*backend.EventsResponse + var receivedEventsResponse []*state_stream.EventsResponse go func() { time.Sleep(10 * time.Second) @@ -123,10 +123,10 @@ func (s *RestStateStreamSuite) TestRestEventStreaming() { client.Close() }() - eventChan := make(chan *backend.EventsResponse) + eventChan := make(chan *state_stream.EventsResponse) go func() { for { - resp := &backend.EventsResponse{} + resp := &state_stream.EventsResponse{} err := client.ReadJSON(resp) if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) { @@ -152,7 +152,7 @@ func (s *RestStateStreamSuite) TestRestEventStreaming() { // requireEvents is a helper function that encapsulates logic for comparing received events from rest state streaming and // events which received from grpc api -func (s *RestStateStreamSuite) requireEvents(receivedEventsResponse []*backend.EventsResponse) { +func (s *RestStateStreamSuite) requireEvents(receivedEventsResponse []*state_stream.EventsResponse) { // make sure there are received events require.GreaterOrEqual(s.T(), len(receivedEventsResponse), 1, "expect received events") diff --git a/module/executiondatasync/optimistic_sync/mock/core_factory.go b/module/executiondatasync/optimistic_sync/mock/core_factory.go new file mode 100644 index 00000000000..7a1c19343c6 --- /dev/null +++ b/module/executiondatasync/optimistic_sync/mock/core_factory.go @@ -0,0 +1,49 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" + + optimistic_sync "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" +) + +// CoreFactory is an autogenerated mock type for the CoreFactory type +type CoreFactory struct { + mock.Mock +} + +// NewCore provides a mock function with given fields: result +func (_m *CoreFactory) NewCore(result *flow.ExecutionResult) optimistic_sync.Core { + ret := _m.Called(result) + + if len(ret) == 0 { + panic("no return value specified for NewCore") + } + + var r0 optimistic_sync.Core + if rf, ok := ret.Get(0).(func(*flow.ExecutionResult) optimistic_sync.Core); ok { + r0 = rf(result) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(optimistic_sync.Core) + } + } + + return r0 +} + +// NewCoreFactory creates a new instance of CoreFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewCoreFactory(t interface { + mock.TestingT + Cleanup(func()) +}) *CoreFactory { + mock := &CoreFactory{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/module/executiondatasync/optimistic_sync/mock/pipeline.go b/module/executiondatasync/optimistic_sync/mock/pipeline.go new file mode 100644 index 00000000000..46e488b4faa --- /dev/null +++ b/module/executiondatasync/optimistic_sync/mock/pipeline.go @@ -0,0 +1,80 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + context "context" + + optimistic_sync "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" + mock "github.com/stretchr/testify/mock" +) + +// Pipeline is an autogenerated mock type for the Pipeline type +type Pipeline struct { + mock.Mock +} + +// Abandon provides a mock function with no fields +func (_m *Pipeline) Abandon() { + _m.Called() +} + +// GetState provides a mock function with no fields +func (_m *Pipeline) GetState() optimistic_sync.State { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetState") + } + + var r0 optimistic_sync.State + if rf, ok := ret.Get(0).(func() optimistic_sync.State); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(optimistic_sync.State) + } + + return r0 +} + +// OnParentStateUpdated provides a mock function with given fields: parentState +func (_m *Pipeline) OnParentStateUpdated(parentState optimistic_sync.State) { + _m.Called(parentState) +} + +// Run provides a mock function with given fields: ctx, core, parentState +func (_m *Pipeline) Run(ctx context.Context, core optimistic_sync.Core, parentState optimistic_sync.State) error { + ret := _m.Called(ctx, core, parentState) + + if len(ret) == 0 { + panic("no return value specified for Run") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, optimistic_sync.Core, optimistic_sync.State) error); ok { + r0 = rf(ctx, core, parentState) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetSealed provides a mock function with no fields +func (_m *Pipeline) SetSealed() { + _m.Called() +} + +// NewPipeline creates a new instance of Pipeline. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPipeline(t interface { + mock.TestingT + Cleanup(func()) +}) *Pipeline { + mock := &Pipeline{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/module/executiondatasync/optimistic_sync/mock/pipeline_factory.go b/module/executiondatasync/optimistic_sync/mock/pipeline_factory.go new file mode 100644 index 00000000000..1ceee4bcb1d --- /dev/null +++ b/module/executiondatasync/optimistic_sync/mock/pipeline_factory.go @@ -0,0 +1,49 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + flow "github.com/onflow/flow-go/model/flow" + mock "github.com/stretchr/testify/mock" + + optimistic_sync "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" +) + +// PipelineFactory is an autogenerated mock type for the PipelineFactory type +type PipelineFactory struct { + mock.Mock +} + +// NewPipeline provides a mock function with given fields: result, isSealed +func (_m *PipelineFactory) NewPipeline(result *flow.ExecutionResult, isSealed bool) optimistic_sync.Pipeline { + ret := _m.Called(result, isSealed) + + if len(ret) == 0 { + panic("no return value specified for NewPipeline") + } + + var r0 optimistic_sync.Pipeline + if rf, ok := ret.Get(0).(func(*flow.ExecutionResult, bool) optimistic_sync.Pipeline); ok { + r0 = rf(result, isSealed) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(optimistic_sync.Pipeline) + } + } + + return r0 +} + +// NewPipelineFactory creates a new instance of PipelineFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPipelineFactory(t interface { + mock.TestingT + Cleanup(func()) +}) *PipelineFactory { + mock := &PipelineFactory{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/module/executiondatasync/optimistic_sync/mock/pipeline_state_consumer.go b/module/executiondatasync/optimistic_sync/mock/pipeline_state_consumer.go new file mode 100644 index 00000000000..f8fec6bd17d --- /dev/null +++ b/module/executiondatasync/optimistic_sync/mock/pipeline_state_consumer.go @@ -0,0 +1,32 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + optimistic_sync "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" + mock "github.com/stretchr/testify/mock" +) + +// PipelineStateConsumer is an autogenerated mock type for the PipelineStateConsumer type +type PipelineStateConsumer struct { + mock.Mock +} + +// OnStateUpdated provides a mock function with given fields: newState +func (_m *PipelineStateConsumer) OnStateUpdated(newState optimistic_sync.State) { + _m.Called(newState) +} + +// NewPipelineStateConsumer creates a new instance of PipelineStateConsumer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPipelineStateConsumer(t interface { + mock.TestingT + Cleanup(func()) +}) *PipelineStateConsumer { + mock := &PipelineStateConsumer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/module/executiondatasync/optimistic_sync/mock/pipeline_state_provider.go b/module/executiondatasync/optimistic_sync/mock/pipeline_state_provider.go new file mode 100644 index 00000000000..b8833d72080 --- /dev/null +++ b/module/executiondatasync/optimistic_sync/mock/pipeline_state_provider.go @@ -0,0 +1,45 @@ +// Code generated by mockery. DO NOT EDIT. + +package mock + +import ( + optimistic_sync "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" + mock "github.com/stretchr/testify/mock" +) + +// PipelineStateProvider is an autogenerated mock type for the PipelineStateProvider type +type PipelineStateProvider struct { + mock.Mock +} + +// GetState provides a mock function with no fields +func (_m *PipelineStateProvider) GetState() optimistic_sync.State { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetState") + } + + var r0 optimistic_sync.State + if rf, ok := ret.Get(0).(func() optimistic_sync.State); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(optimistic_sync.State) + } + + return r0 +} + +// NewPipelineStateProvider creates a new instance of PipelineStateProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPipelineStateProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *PipelineStateProvider { + mock := &PipelineStateProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/module/state_synchronization/requester/execution_data_requester_test.go b/module/state_synchronization/requester/execution_data_requester_test.go index 3684679d900..c528711930e 100644 --- a/module/state_synchronization/requester/execution_data_requester_test.go +++ b/module/state_synchronization/requester/execution_data_requester_test.go @@ -18,7 +18,7 @@ import ( "github.com/onflow/flow-go/consensus/hotstuff/model" "github.com/onflow/flow-go/consensus/hotstuff/notifications/pubsub" - "github.com/onflow/flow-go/engine/access/subscription" + "github.com/onflow/flow-go/engine/access/subscription_old" "github.com/onflow/flow-go/model/flow" "github.com/onflow/flow-go/module" "github.com/onflow/flow-go/module/blobs" @@ -408,7 +408,7 @@ func (suite *ExecutionDataRequesterSuite) prepareRequesterTest(cfg *fetchTestRun suite.downloader = MockDownloader(cfg.executionDataEntries) suite.distributor = NewExecutionDataDistributor() - heroCache := herocache.NewBlockExecutionData(subscription.DefaultCacheSize, logger, metricsCollector) + heroCache := herocache.NewBlockExecutionData(subscription_old.DefaultCacheSize, logger, metricsCollector) edCache := cache.NewExecutionDataCache(suite.downloader, headers, seals, results, heroCache) followerDistributor := pubsub.NewFollowerDistributor()