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

Update Handler Interface & Improve Error Usage #5072

Closed
wants to merge 8 commits into from

Conversation

alexanderbez
Copy link
Contributor

@alexanderbez alexanderbez commented Sep 19, 2019

closes: #4844

/cc @ethanfrey @marbar3778


  • Targeted PR against correct branch (see CONTRIBUTING.md)

  • Linked to github-issue with discussion and accepted design OR link to spec that describes this work.

  • Wrote tests

  • Updated relevant documentation (docs/)

  • Added a relevant changelog entry to the Unreleased section in CHANGELOG.md

  • Re-reviewed Files changed in the github PR explorer


For Admin Use:

  • Added appropriate labels to PR (ex. wip, ready-for-review, docs)
  • Reviewers Assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

@alexanderbez alexanderbez added C:baseapp WIP T: API Breaking Breaking changes that impact APIs and the SDK only (not state machine). labels Sep 19, 2019
@alexanderbez alexanderbez mentioned this pull request Sep 19, 2019
4 tasks
@alexanderbez alexanderbez added the S:blocked Status: Blocked label Sep 20, 2019
@alexanderbez
Copy link
Contributor Author

Blocked by #5006. We should wait till that's finalized and merged as both modify BaseApp.

Copy link
Contributor

@ethanfrey ethanfrey left a comment

Choose a reason for hiding this comment

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

Nice start, and this does bring up some issues on the interfaces. Especially with GasHandling. After some reflection on the code base, I start to believe that GasUsed/GasWanted don't belong in Result after all. This would allow a proper *Result, err to be X, nil or nil, X as long as we pass the gas info some other way.

My reasoning is the following:

In fact, a Handler can return any info in these two Gas* fields and they will be ignored. So why expose it to them?

If we look at the end result in the abci interface:

	return abci.ResponseDeliverTx{
		Code:      uint32(result.Code),
		Codespace: string(result.Codespace),
		Data:      result.Data,
		Log:       result.Log,
		GasWanted: int64(result.GasWanted), // TODO: Should type accept unsigned ints?
		GasUsed:   int64(result.GasUsed),   // TODO: Should type accept unsigned ints?
		Events:    result.Events.ToABCIEvents(),
	}

https://github.com/cosmos/cosmos-sdk/blob/d5ec8427e2254fdf6e4ceec1f7ad9503b04646f7/baseapp/abci.go#L192-L201

I see this info coming from various sources:

  • Code/Codespace/Log represent info from error, as you are refactoring now
  • Data/Log/Events represent info from Result, set on successful execution
  • GasUsed/GasWanted is metering info which is handled in the runTx wrapper and not in a lower-level component. I would add a separate type for this.

My suggestion is:

  • Leave runMsg with (*Result, error) return value, and set Result to nil on error
  • Update runTx return value to (GasInfo, *Result, error) where Result and error are as runMsg (plus some other cases), but GasInfo is always set (from the defer function).
type GasInfo struct {
	GasWanted uint64
	GasUsed uint64
}

This actually would leave a super-minimal Result type, which is actually the only info a Handler can return on success:

type Result struct {
  Log string
  Data []byte
  Events Events
}

What do you think? I know it adds a type, but it can be a private type and is only used to communicate between runTx and CheckTx/DeliverTx, while further simplifying the Result interface to the essential. Since much more development work is expected to happen producing implementations of the Handler interface, than in the BaseApp wrapper around them, it seems like a nice tradeoff to me to make more simplicity for the Handler developers, which is actually the main goal of separating out the error return value, to make error handling simpler (and with better logging).

@@ -17,7 +17,7 @@ type Msg interface {

// ValidateBasic does a simple validation check that
// doesn't require access to any other information.
ValidateBasic() Error
Copy link
Contributor

Choose a reason for hiding this comment

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

Error is a superset of error, so all existing code will work.
But they will have to update their function signatures, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe so

type Handler func(ctx Context, msg Msg) Result
// A reference to a Result type must be returned upon success and an error upon
// failure.
type Handler func(ctx Context, msg Msg) (*Result, error)
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I would actually wonder if we want to give a grace period for people to upgrade their 3rd party modules.
Otherwise this PR will leave the entire SDK in a broken state for a while, no?

Like call the new Handler MsgHandler and then have a wrapper function that converts Handler -> MsgHandler

Otherwise, there is quite some work to clean up code before this can go into master.
With the incremental approach, you can split that into several prs (even if all done before a release)

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems more plausible approach as well.

@@ -126,7 +126,7 @@ func ABCIError(codespace string, code uint32, log string) error {

// Error represents a root error.
//
// Weave framework is using root error to categorize issues. Each instance
// The SDK uses root errors (see above) to categorize issues. Each instance
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, missed this... as you see much of this code was copied, with minor adaptations (like adding codespaces)

@@ -186,7 +195,7 @@ type Error interface {
}

// NewError - create an error.
func NewError(codespace CodespaceType, code CodeType, format string, args ...interface{}) Error {
func NewErrorz(codespace CodespaceType, code CodeType, format string, args ...interface{}) Error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why add the z?
If intentional, I guess the godoc should be updated also

Copy link
Contributor

Choose a reason for hiding this comment

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

This seems more like a typo?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is intentional; I wanted the go compiler that resulted in this to help updating code due to new APIs. This PR is still a DRAFT; ignore.

@@ -522,7 +523,7 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk

// only run the tx if there is block gas remaining
if mode == runTxModeDeliver && ctx.BlockGasMeter().IsOutOfGas() {
return sdk.ErrOutOfGas("no block gas left to run tx").Result()
return nil, sdkerrors.Wrap(sdkerrors.ErrOutOfGas, "no block gas left to run tx")
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, looks good to me. I hope this format looks good for you also.

}

// only update state if all messages pass
if result.IsOK() {
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be if err == nil

Copy link
Contributor

Choose a reason for hiding this comment

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

actually can be combined with the above statment...
if err == nil && mode != runTxDeliver { ...Write() }
return result, err

log := fmt.Sprintf("recovered: %v\nstack:\n%v", r, string(debug.Stack()))
result = sdk.ErrInternal(log).Result()
err = sdkerrors.Wrap(sdkerrors.ErrPanic, log)
Copy link
Contributor

Choose a reason for hiding this comment

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

nice use of ErrPanic

@@ -610,79 +615,68 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk
// MultiStore in case message processing fails. At this point, the MultiStore
// is doubly cached-wrapped.
runMsgCtx, msCache := app.cacheTxContext(ctx, txBytes)
result = app.runMsgs(runMsgCtx, msgs, mode)
result, err = app.runMsgs(runMsgCtx, msgs, mode)
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, so the reason we need result even if there is an error is to capture the gas consumed, right?

Even if the data, events, etc of result are meaningless in error, the gas usage still needs to be communicated, as that is handled above? (Trying to understand the flow)

// skip actual execution for CheckTx mode
if mode != runTxModeCheck {
msgResult = handler(ctx, msg)
err = sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized message type: %s,", msgRoute)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not return nil, err, which would be closest to the old behavior?

if err := validateBasicTxMsgs(msgs); err != nil {
return err.Result()
return nil, err
Copy link
Contributor

Choose a reason for hiding this comment

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

This will panic... since the first defer block above sets result.GasWanted = gasWanted, we can never safely return nil here.

I didn't realize this case, and it seems this function must either always return a valid Result (in which case make it Result, not *Result so at least the empty value is valid), or the logic on 548-549 needs to change (I am not sure how that has repercussions on the rest of the stack)

@tac0turtle tac0turtle removed the S:blocked Status: Blocked label Oct 16, 2019
@alexanderbez alexanderbez deleted the bez/4844-handler-error-refactor branch December 17, 2019 19:08
@alexanderbez alexanderbez mentioned this pull request Dec 17, 2019
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C:baseapp T: API Breaking Breaking changes that impact APIs and the SDK only (not state machine).
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve Handler interface
4 participants