Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FABGW-21: Realtime implementation of ChaincodeEvents service #2604

Merged
merged 2 commits into from
Jun 4, 2021

Conversation

bestbeforetoday
Copy link
Member

@bestbeforetoday bestbeforetoday commented May 25, 2021

Initial implementation of Fabric Gateway's ChaincodeEvents service, allowing only listening for realtime chaincode events.

Also add block number into CommitStatus response.

@bestbeforetoday bestbeforetoday force-pushed the FABGW-21 branch 2 times, most recently from 43d18f1 to 7f951ea Compare May 26, 2021 12:13
@bestbeforetoday
Copy link
Member Author

/ci-run

@github-actions
Copy link

AZP build triggered!

@bestbeforetoday bestbeforetoday force-pushed the FABGW-21 branch 3 times, most recently from 1b323b6 to 16b05e6 Compare May 26, 2021 15:01
@bestbeforetoday
Copy link
Member Author

Refactored to break cyclic dependency and enable the use of Counterfeiter as the mocking framework again.

@bestbeforetoday
Copy link
Member Author

@manish-sethi Moving back to draft to continue changes to support chaincode event listening, at least until there is a suitable mechanism to grab block number along with transaction status and a satisfactory interim PR can be delivered.

@bestbeforetoday bestbeforetoday marked this pull request as draft May 27, 2021 14:43
@bestbeforetoday bestbeforetoday force-pushed the FABGW-21 branch 5 times, most recently from 358f48d to 8d64284 Compare May 28, 2021 14:24
@bestbeforetoday bestbeforetoday changed the title FABGW-21: Return block number in CommitStatusResponse FABGW-21: Realtime implementation of ChaincodeEvents service May 28, 2021
@manish-sethi
Copy link
Contributor

@manish-sethi Moving back to draft to continue changes to support chaincode event listening, at least until there is a suitable mechanism to grab block number along with transaction status and a satisfactory interim PR can be delivered.

@bestbeforetoday - The PR for block number is merged now.

@bestbeforetoday bestbeforetoday force-pushed the FABGW-21 branch 4 times, most recently from cd6dba4 to e4ee943 Compare June 2, 2021 13:17
@bestbeforetoday bestbeforetoday marked this pull request as ready for review June 2, 2021 13:57
Copy link
Contributor

@manish-sethi manish-sethi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bestbeforetoday - I feel that the code in this area is already becoming complex enough. Primarily because of how dependencies are factored out and a number of notifiers. I have listed a few suggestions to simplify things, see if they makes sense to you.

Comment on lines +277 to +280
request := &gp.ChaincodeEventsRequest{}
if err := proto.Unmarshal(signedRequest.Request, request); err != nil {
return status.Error(codes.InvalidArgument, "invalid chaincode events request")
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even if there is no error, this could still leave request with one or more empty field (e.g., signedRequest.Request could be an empty bytes). Better to explicitly check for this so it does not lead to nil-pointer panic in the downstream code. The downstream code may already be checking for this but it becomes hard to reason about at higher level so better to have independent check at the entry level.

I guess that this applies to existing function in this file as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will an empty string or empty slice cause a nil-pointer panic? Could you give me an example of the change you are requiring here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In downstream code this byte array is used to unmarshal and I am not certain that every place all the checks have been in place. So, it was just to fail early if this byte slice is nil - kind of basic input check.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can unmarshall a zero-length []byte fine. I don't understand the benefit of adding code bloat for a special check for a zero-length slice rather than letting a meaningful error be raised, such as the requested channel doesn't exist or the supplied identity is not valid. The client APIs don't send empty protobufs so it's not a scenario that can occur by accident

Comment on lines 825 to 840
peerAdapter := &commit.PeerAdapter{
Peer: peerInstance,
}
notifier := commit.NewNotifier(peerAdapter)
gatewayprotos.RegisterGatewayServer(
peerServer.Server(),
gateway.CreateServer(
&gateway.EndorserServerAdapter{Server: serverEndorser},
discoveryService,
&commit.Finder{
Query: peerAdapter,
Notifier: commit.NewNotifier(peerAdapter),
Notifier: notifier,
},
&commit.Eventer{
Notifier: notifier,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though not introduced in this PR but it got further flared - This pattern of linking the dependencies surfaces much of internal details of gateway module to this level. The only real dependency of gateway module is a few functions on the peerInstance. I feel that this will be clearer if you could define a limited interface for this and just pass peerInstance in gateway.CreateServer and construct PeerAdapter, notifier, Finder, and Eventer internally inside gateway module. This may also remove many interfaces in the gateway module.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with the narrower interface but was required to do it this way by another maintainer. The way the code is currently structured (at the request of another maintainer), we can't pass concrete implementations in to gateway.CreateServer since mocks are passed in there by unit tests.

I'll look at some changes back towards my original approach for the Finder and Eventer, and I propose we look at changes to other areas of the code in a different PR rather than shoe-horn a bunch of unrelated code refactor into this one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made some changes to reduce the leakage of internal abstractions outside of the gateway package, which I think is along the lines of what you are asking.

Note that even within the gateway packages there are deliberately different layers of abstraction. The core Gateway code is intended to be agnostic to whether it is an embedded or standalone implementation, and so interacts with the embedded implementation-specific elements using interfaces, which could be replaced standalone implementations.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can definitely do it in a separate PR. But I feel that there are way to many layers of abstraction for a simple enough functionality.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't pass concrete implementations in to gateway.CreateServer

contrary to this you were actually passing the peer instance to commit package. What my suggestion was that only external dependency of gateway package is to get an object on which it can call "RegisterForNotification" for a given channel and that should be easy enough to reflect in first-order via an interface.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What my suggestion was that only external dependency of gateway package is to get an object on which it can call "RegisterForNotification" for a given channel and that should be easy enough to reflect in first-order via an interface.

No.

RegisterForNotification() implies the peer embedded implementation, which is in the commit package. The commit package uses the NotificationSupplier interface to define CommitNotifications() which is the same as RegisterForNotification(), so it's already doing as you suggest.

The gateway server is not intended to be tied to either a peer embedded or standalone implementation, so it uses a CommitFinder interface to obtain transaction validation codes by some means that it doesn't need to know about, and which would be significantly different in the embedded and standalone implementations.

Comment on lines 27 to 29
type QueryProvider interface {
TransactionStatus(channelName string, transactionID string) (peer.TxValidationCode, error)
TransactionStatus(channelName string, transactionID string) (peer.TxValidationCode, uint64, error)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To highlight the comment that I made in start.go in context - This is how it's easy for someone to get lost in the maze of layers of finer-grained dependencies as opposed having a simplified straight-forward dependency. In this case, this interface, it's sole implementation, and it's sole consumer all three are in the same package. Which makes it unnecessarily round-about to understand the true linkages.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This describes an interface for a subset of capability provided by the PeerAdapter struct, which allows me to use a mock implementation in unit tests. Trying to make the PeerAdapter work purely with interfaces (that I could then mock) would be even more convoluted because of the way to Go type system works

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decoupling code within a package for the sake of using mocks for UTs does not sound a good idea to me. I am not sure I understand what prevents from writing UTs as long as external dependencies are mocked.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueryProvider decouples the PeerAdapter (i.e. the thin layer of code not easily untangled from the peer implementation, and so you could think of as being the external dependency) from the implementation in this package

Comment on lines 15 to 18
type blockListener interface {
receiveBlock(*ledger.CommitNotification)
close()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this interface and straight away use the two concrete types - to make it more explicit. The current code may have saved a few lines of code but lessen the readability.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This abstraction isn't there to save a few lines of code. It's to:

  1. Keep the blockListener focused on the task of receiving and dispatching events from the ledger
  2. Allow it to be extended with other listeners without requiring code changes
  3. Avoid it needing dependencies on listener implementations

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These objectives sounds good if they are between course level modules. Applying these to micro level of granularity has led to Fabric code at many places as hard to read IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to make that change

Comment on lines +20 to +21
type blockNotifier struct {
commitChannel <-chan *ledger.CommitNotification
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel that too many notifiers are making it hard to read code. Can we club the functionality in this into the main Notifier. That will make the main Notifier act as a central piece that glues together the lower level events received from ledger and the other two notifiers handle the event specific delivery.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a one-to-many relationship between Notifier and blockNotifier. Also Notifier needs to be able to throw away and recreate a blockNotifier. I'm not sure how you see merging Notifier and blockNotifier working

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking in the lines of launching a gorouting for each channel directly into the Notifier and the notifier just keeps track of the two event specific notifiers. In the current structure of the code it sounded like all three notifiers are kind of siblings when they are actually not. The other approach could be to have the both event specific notifiers only inside this blocknotifier - so this becomes full controller of channel specific notifier and hierarchy is clearer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to make that change

Initial implementation of Fabric Gateway's ChaincodeEvents service, allowing only listening for realtime chaincode events.

Also add block number into CommitStatus response.

Signed-off-by: Mark S. Lewis <[email protected]>
@bestbeforetoday
Copy link
Member Author

/ci-run

@github-actions
Copy link

github-actions bot commented Jun 3, 2021

AZP build triggered!

Previously node.serve() accessed a lot of implementation detail within the gateway package to create structs of the appropriate abstraction for the gateway internals. Refactored to move the translation between external structs and internal abstractions to the gateway.CreateServer() function, and moved the old implementation to an newServer() function that is used by unit tests to create a Gateway server that is agnostic to whether it is an embedded or standalone implementation.

Signed-off-by: Mark S. Lewis <[email protected]>
@bestbeforetoday
Copy link
Member Author

@manish-sethi Your outstanding comments seem to be primarily objections to code structure that was already there prior to this PR, and was there at the request of other maintainers. I have addressed as many of these structural issues as I think I reasonably can in this PR, which is delivering chaincode event listening, not a refactor of existing code

badSigningIdentity := network.OrdererUserSigner(network.Orderer("orderer"), "Admin")
badIdentity, err := badSigningIdentity.Serialize()
Describe("ChaincodeEvents", func() {
It("should respond with emitted chaincode events", func() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that you still plan to add security related tests, like the one in CommitStatus api ? However, that can always be added later.

@manish-sethi
Copy link
Contributor

@manish-sethi Your outstanding comments seem to be primarily objections to code structure that was already there prior to this PR, and was there at the request of other maintainers. I have addressed as many of these structural issues as I think I reasonably can in this PR, which is delivering chaincode event listening, not a refactor of existing code

Yes, As I mentioned in the previous comment, the refactoring can be taken up in a separate PR

@manish-sethi manish-sethi merged commit 1f89be9 into hyperledger:main Jun 4, 2021
@bestbeforetoday
Copy link
Member Author

@manish You were not keen on having the blockNotifier in addition to the other notifiers. This is really there because you imposed the limitation that there could only be one listener to ledger notifications, so the blockNotifier provides a mechanism to workaround that limitation and allow multiple listeners. One approach to simplifying this code might be to remove your restriction on only allowing a single listener for ledger notifications, in which case the blockNotifier and the logic there could be completely removed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants