-
Notifications
You must be signed in to change notification settings - Fork 3.4k
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
Add off-chain-data go client application #1269
Add off-chain-data go client application #1269
Conversation
45a2aac
to
09402bc
Compare
f7ede20
to
4e58c31
Compare
4e58c31
to
1d3f283
Compare
fc2ff58
to
c6d784a
Compare
307bb47
to
43f67fb
Compare
0b8ba91
to
f9285d5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First thing, it runs great for me so this looks nicely done. Thank you! Sorry for large number of comments. This is just polishing the material to make it a really clean sample.
I would be careful to try and keep all the code that listens for blocks, navigates to their transactions and passes the read/write sets to some kind of store close together. Perhaps all in listen.go
. This is the key learning point of the sample so it needs to be easy to access and navigate. The block parser should be separate (as you've done) so it is a reusable package on its own. The store implementation can also be a little saperate to avoid cluttering the key listener code. I might put it in a different file within the same package though, just as private (lowercase) content.
The Application section of off_chain_data/README.md
should be updated to include references to key parts of the Go implementation, similar to the existing TypeScript and Java entries.
To catch some issues, it might be worth running this command in the application-go
directory:
go run honnef.co/go/tools/cmd/staticcheck@latest -f stylish -checks all ./...
To avoid specific checks (like having package-level godoc), you can exclude specific rules. For example:
go run honnef.co/go/tools/cmd/staticcheck@latest -f stylish -checks 'all,-ST1000' ./...
d4a1546
to
d6ad358
Compare
Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Signed-off-by: Stanislav Jakuschevskij <[email protected]>
For the GetCreator() method return the identity.Identity interface was also implemented. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Created parser, contract and utils packages and extracted each piece of functionality into its own files. Removed "Get" prefix from methods and changed return values from interfaces to structs. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Removed Block and Transaction interfaces and unused statusCode function. Using the struct instead of the interfaces now. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Added block processor struct and the process method. Implemented getting valid transactions from the last processed index. Added data structures needed for the store. Decomposed the parser.Block.Transactions() method into readable chunks. Added transaction processor struct and process method. Unwrapping read write set data from the transaction, mapping to a new "write" data structure and passing down to the store. Store is an empty function and will be implemented next. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Persisting ledger writes to the file system into the store.log file in the application-go directory. The write values are converted from bytes to a string when the read write sets are unwrapped in the transaction processor. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Added caching util function with tests and applied in: - parser.Block.Transactions(), - parser.Payload.ChannelHeader(), - parser.Payload.SignatureHeader(), - parser.NamespaceReadWriteSet.ReadWriteSet(), - parser.EndorserTransaction.ReadWriteSets(), methods, as it was in the typescript sample. Corrected Println usage and added comments to util functions. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Created packages for the flat file store and the processor and moved functions, variables and constants from listener.go to those packages. Encapsulated everything not used outside the packages, introduced model.go files which later might be extracted into a model package and renamed parser/parsedBlock.go to parser/block.go. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Before when pressing 'ctrl+c' and stopping the go program non of the deferred functions in listen.go were called. A standard procedure for stopping goroutines with context was implemented which shuts down the program gracefully. Logs were added to notify the user that the shutdown was successful. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Every struct was put in its own file. Every method which is not used outside the parser package was given package scope. All interfaces were removed, they are implemented by the structs which are now used everywhere needed as return values. There is no clear benefit of using interfaces in this sample. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Before all transactions were processed and when the failure was simulated a message was printed and all the transactions still processed. Now the store returns an error when the failure is simulated which the listener expects so that it can gracefully shutdown the system and close the context. The context must be closed correctly or the checkpointer won't save the last processed transactionId to the file system. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Starting from the processor.Block.Process all methods now return errors if something goes wrong with unpacking of the blocks and reading the transactions. In each function where the error is being propagated back to client it is wrapped in a message with the function name. This makes it easier to track down the error and see the propagation chain. Finally the error is logged to the terminal and the go routine shuts down gracefully. The graceful shutdown executes all deferred functions which close the context, the checkpointer and the gateway. Before panics were used everywhere which was an issue because the unpacking of the blocks happened in a go routine. When a panic happens in a go routine only the deferred functions of the go routine are called but not those of the client which lead to unexpected behavior. The transact function is also executed in a go routine therefore the same typo of error handling was implemented there. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Signed-off-by: Stanislav Jakuschevskij <[email protected]>
- update Application section in README - remove param name in app.go - add error checks in processor/block.go - move vars from model to transact logic - move newAsset to transact - use ID for well-known initialisms - move randomelement, randomnint and differentelement to transact - remove AssertDefined - blockTxIdsJoinedByComma: use standard library to join elements - return nil, instead of []byte{} - remove go routine in listen.go - move cache to parser - inline processor in listen.go - move store to main package - move util to main package - fixed failing cache issue - fixed staticcheck issues - removed cache function, implemented caching in the structs and methods Signed-off-by: Stanislav Jakuschevskij <[email protected]>
- switch to ClientConnInterface - use command type alias for map of commands - add error return to command functions and handle in app.go - inline formatJSON function in getAllAssets.go - replace most panics with error returns - remove error wrapping in listen.go and further down the line - use strconv.ParseUint instead of ParseFloat - use WithCancelCause in transact.go to grab and propagate error from go routine - unmarshal and return []Asset in atb.GetAllAssets - switch to rand package - remove dependency to protobuf reflect package - switch to sync.OnceValues for caching parser - fixed typo in events sample connect.go Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Remove txError struct. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
The Javascript asset-transfer-basic chaincode stores AppraisedValue and Size as string types instead of number types. This leads to an issue when used with a Go client application where assets are unmarshaled into an Asset type where AppraisedValue and Size are of uint64 type. This change makes sure that AppraisedValue and Size are stringified as numbers. To prevent the pipeline from failing when the expected error occurs a sentinel error was created and checked against in the entry point. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Remove unused struct member and to-do comments. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
Add store interface and implement with offChainStore struct. Decompose storing into small chunks and keep state around storing writes and failure count. Move environment variables used for store setup into the setup phase of the listen function. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
752dcfc
to
fde0cd5
Compare
@bestbeforetoday one is still failing. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking really good now. Just a few very small niggles that would be nice to tidy up and I think it is ready to go.
return result, nil | ||
} | ||
|
||
func (*Block) unmarshalPayloadsFrom(envelopes []*common.Envelope) ([]*common.Payload, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is fine as-is. Just an option to have it a standalone function since it doesn't rely on any Block state. It is related to parsing of the block so I can see an argument for keeping it as a Block method to aid readability.
return result, nil | ||
} | ||
|
||
func (*endorserTransaction) extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads []*peer.ChaincodeActionPayload) ([]*peer.ChaincodeEndorsedAction, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An error is never returned so you don't need the error return value. The results are just passed to unmarshalProposalResponsePayloadsFrom
so you might find it easier to include this logic in that method. chaincodeActionPayload.GetAction().GetProposalResponsePayload()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great, done.
} | ||
|
||
func (*endorserTransaction) extractChaincodeEndorsedActionsFrom(chaincodeActionPayloads []*peer.ChaincodeActionPayload) ([]*peer.ChaincodeEndorsedAction, error) { | ||
result := []*peer.ChaincodeEndorsedAction{} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really a nitpick... I think the result slice you create here (or rather its underlying array) is going to get discarded as soon as you append to it in the loop since there isn't capacity to append more elements. You can save the work of creating storage that will be discarded by using a nil value, which still works as an empty slice:
var result []*peer.ChaincodeEndorsedAction
Or you can make a slice with capacity to hold len(chaincodeActionPayloads)
elements up-front.
Same comment for the other instances of creating an empty (zero capacity) slice that you will then append to in this file.
Like I say, it's a really minor detail. Change it only if you want to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the result slice you create here (or rather its underlying array) is going to get discarded as soon as you append to it in the loop since there isn't capacity to append more elements.
Had to reread a couple times but I think I understand now. Thanks for clarifying. I changed it.
ocs.clearLastWrites() | ||
|
||
if err := ocs.marshal(data.Writes); err != nil { | ||
return err | ||
} | ||
|
||
if err := ocs.persist(); err != nil { | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using object state that you then need to manage instead of just passing arguments between private methods looks like a red flag to me. There is much more potential for things to go wrong since you are mutating more state in your object, and state is left lying around longer than you need it. For example, ocs.writes
still contains data after you have finished persisting it and this method returns.
Also look out for error handling where you return only the error if it is non-nil; otherwise nil. Just return the error value directly. It is really easy to do this by accident.
Consider a simpler pattern, something like:
writes, err := ocs.marshal(data.Writes)
if err != nil {
return err
}
return ocs.persist(writes)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example,
ocs.writes
still contains data after you have finished persisting it and this method returns.
Actually, this was intended. But I was unsure about it. I thought it can make sense to keep the last write.
I believe I misunderstood this message then:
a store function (or store interface if you prefer to use a struct that maintains some state around the storing of writes, like the write and simulated failure count).
I removed it and used the simpler pattern. Thanks for pointing out! I am remembering more and more of the good practices thank to your reviews.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I meant the write count (and simulated failure count) but explained it poorly! The simulatedFailureCount
, transactionCount
and path
are state that makes sense to be there since they need to be maintained over over many calls to write().
if err := f.Close(); err != nil { | ||
return err | ||
} | ||
|
||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if err := f.Close(); err != nil { | |
return err | |
} | |
return nil | |
return f.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if err := app.run(); err != nil { | ||
return err | ||
} | ||
|
||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if err := app.run(); err != nil { | |
return err | |
} | |
return nil | |
return app.run() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
if err := context.Cause(ctx); err != nil { | ||
return err | ||
} | ||
|
||
return nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if err := context.Cause(ctx); err != nil { | |
return err | |
} | |
return nil | |
return context.Cause(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Use simplified return pattern. Change to var for slice declarations and simplified read-write-set unmarshal. Signed-off-by: Stanislav Jakuschevskij <[email protected]>
ae8c74b
to
97b89dc
Compare
Two things are left on my mind:
Once you approve I will squash everything to one commit. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent work. The code reads nicely to me and works perfectly.
The only things I noticed that should probably be added to make the block parser more reusable is:
- ToProto() method on the Block, Transaction and NamespaceReadWriteSet. These should return a *common.Block, *common.Payload and *rwset.TxReadWriteSet respectively. They allow anyone reusing the parser to extract any additional information they might need from the underlying protobuf messages.
- Transaction should have ValidationCode() and Creator() methods, since this information is commonly of interest and are either impossible or less easy to get from the associated *common.Payload protobuf message.
The sample works great as-is, and these are not good reasons to hold up the PR. They can be addressed in a future change.
Thank you for the contribution!
Off Chain Data Go Application
This PR contains an implementation of the off-chain data store sample in go using the block event listening capability of the Fabric Gateway client API. The issue reference is here in the Fabric Gateway SDK repository.
It follows the same procedure and uses mostly the same naming as the off-chain data typescript sample here. There are some differences in the go package structure compared to the typescript modules and I added a couple of tests to the parser which helped me better understand the unpacking of the block data structure. The Go implementation is more similar to the Java version. More about that below in the Differences section.
@bestbeforetoday gave me this useful link where I found the iterator_test.go to be helpful and this link where the "Endorser Transaction" comments were helpful.
Summary
app.go,
getAllAssets.go
,transact.go
andlisten.go
filesconnect.go
from the private data applicationcontract
packagegetAllAssets.go
andtransact.go
listen.go
Differences
toProto
orgetSignatureHeader
.parser
package.transaction
(which is private) is in theprocessor
package.store
package and because it resembles a bit a flat file db I called itflatFile.go
utils
package.Additional Information
I process the block events in a go routine and use context to stop the processing. Instead of panicking everywhere down the line where an error can occur I handle the errors by wrapping them, returning them, log them in the go routine, then return and shutdown gracefully. Panicking results in the context, gateway and checkpointer not getting closed.
Questions
processor.transaction
data structure.kvWrite
in the parser instead of having it intransaction.writes
.