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

Implemented sendAsync for js commands with a callback #321

Merged
merged 13 commits into from
Sep 18, 2017

Conversation

tiabc
Copy link
Contributor

@tiabc tiabc commented Sep 14, 2017

Geth js commands coming through jail with a callback will now be executed truly asynchronously blocking jail only when an actual interaction with VM is performed.

Technically, it registers a new handler jeth.sendAsync which executes functions with callbacks asynchronously.

Changes include:

  1. Send and SendAsync now use cell.VM instead of otto.Otto providing proper locking.
  2. Unmarshalling in ExecuionPolicy.ExecuteWithClient is now done into var result interface{} instead of var result json.RawMessage because test case 0 of TestJailWhisper failed providing byte codes instead of 5.0.
  3. Due to the asynchronous nature of web3 calls new weird timeouts in tests have been introduced. They may fail sometimes but I gave up trying to implement a more reliable and readable solution.

The only failing test is TestJailWhisper. Refer to #336 for explanations.

@tiabc tiabc changed the title Implemented first version (tests fail) Implemented sendAsync for js commands with a callback Sep 14, 2017
} else {
resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object()
resp = newErrorResponse(-32603, err.Error(), &req.ID)
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe move this -32603 constant to constant definition, similar to https://github.com/status-im/status-go/blob/develop/geth/rpc/call_raw.go#L13:

const (
	...
	errInvalidMessageCode = -32700 // from go-ethereum/rpc/errors.go
)

}

return ""
msgIDStr, err := msgID.ToString()
Copy link
Contributor

@divan divan Sep 14, 2017

Choose a reason for hiding this comment

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

There is also a handy (*otto.Value).String() method which returns empty string if there is an error.
https://github.com/status-im/status-go/blob/develop/vendor/github.com/robertkrimen/otto/value.go#L384

1. Encapsulated error code -32603 into newErrorResopnse
2. Shortened a conversion to string
# Conflicts:
#	geth/jail/execution_policy.go
#	geth/jail/jail.go
@tiabc tiabc removed the in progress label Sep 15, 2017
@tiabc tiabc mentioned this pull request Sep 17, 2017
@tiabc tiabc removed the in progress label Sep 17, 2017
@@ -32,26 +31,25 @@ func NewExecutionPolicy(
}

// Execute handles the execution of a RPC request and routes appropriately to either a local or remote ethereum node.
Copy link
Contributor

Choose a reason for hiding this comment

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

and routes appropriately to either a local or remote ethereum node should be removed, this function doesn't do routing anymore.

@adambabik
Copy link
Contributor

Due to the asynchronous nature of web3 calls new weird timeouts in tests have been introduced. They may fail sometimes but I gave up trying to implement a more reliable and readable solution.

I think the only way we can fix it is by removing jail cells or creating a new node instance in every tests instead of reusing it. I checked web3.js and some commands (like creating a contract) register filters and do not provide a way to cancel them.

}

client := ep.nodeManager.RPCClient()

return ep.executeWithClient(client, req, call)
return ep.executeWithClient(client, vm, req)
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, after moving routing into rpc package, there is no need to have execute with client function. Either inline its code here, or move client := ep.nodeManager.RPCClient() to that function.

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 make sense, do we still need ExecutionPolicy?

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 refactor this out or rename?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think ExecutionPolicy refactoring is out of scope. Added to #200.

cell := cellInt.(*Cell)
return func(call otto.FunctionCall) otto.Value {
go func() {
response := jail.Send(call, cell.VM)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have tests ensuring we are not loosing references anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We've never lost references. These were just race conditions. But we have one decent test anyway:

func (s *JailTestSuite) TestJailRPCAsyncSend() {

Copy link
Contributor

Choose a reason for hiding this comment

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

Alright

Copy link
Contributor

@influx6 influx6 left a comment

Choose a reason for hiding this comment

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

Looks aright

cell := cellInt.(*Cell)
return func(call otto.FunctionCall) otto.Value {
go func() {
response := jail.Send(call, cell.VM)
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't the same comments as on line :92 (in makeSendHandler - Send calls are guaranteed to be only invoked...) applies here? Should not it unlock and lock mutex as well here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope. Moreover, we'll get panic if we do this.
The reason is that we run this in a goroutine and just return otto.UndefinedValue() and this is fast. The cell will be quickly unlocked after these 2 operations and jail.Send execution in the goroutine will start afterwards.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. I'm not sure I completely understand workflow here, but fine for now.

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 an important piece, let's sort it out. I may also turn out to be wrong here and it will be fatal. I'll draw a diagram in a minute.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Something like that:
send and sendasync

Copy link
Contributor

Choose a reason for hiding this comment

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

Woah, this is a cool picture explaining a lot. Let's add it do the documentation doc.

Just one question with calls without callbacks: if a long running operation will be called and another independent call without callback will be executed, we are still fine because we use thread-safe otto (our vm.VM struct)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, these two will be just run in sequence.

func newErrorResponseOtto(vm *vm.VM, msg string, id interface{}) otto.Value {
// TODO(tiabc): Handle errors.
errResp, _ := json.Marshal(newErrorResponse(msg, id))
errRespVal, _ := vm.Run("(" + string(errResp) + ")")
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks a bit cryptic. Why do we call JS to form error?

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldnt we assign it into a variable in the script source to be runned?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@divan this is legacy. But it seems that's to get otto.Value out of an arbitrary object.
@influx6 nope, you can run it and get the result into errRespVal.

Copy link
Contributor

Choose a reason for hiding this comment

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

i see. Understood.

@@ -47,7 +47,7 @@ statusgo-ios-simulator-mainnet: xgo
@echo "iOS framework cross compilation done (mainnet)."

ci: mock
build/env.sh go test -timeout 10m -v ./geth/api/...
build/env.sh go test -timeout 40m -v ./geth/api/...
Copy link
Contributor

Choose a reason for hiding this comment

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

Didnt we already change this in another PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes but it's not merged yet.

@tiabc
Copy link
Contributor Author

tiabc commented Sep 17, 2017

I think the only way we can fix it is by removing jail cells or creating a new node instance in every tests instead of reusing it. I checked web3.js and some commands (like creating a contract) register filters and do not provide a way to cancel them.

Sorry, @adambabik, didn't get your comment. My comment was about waiting callbacks, are we on the same page?

Copy link
Contributor

@adambabik adambabik left a comment

Choose a reason for hiding this comment

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

Overall LGTM, but there are some nuances I don't get.

@@ -264,6 +264,9 @@ func (s *BackendTestSuite) TestContractDeployment() {
s.FailNow("test timed out")
}

// Wait until callback is fired and `responseValue` is set. Hacky but simple.
Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, so was it failing sometimes because of invalid responseValue? I have never noticed that...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes it was. It hasn't failed before just because callbacks were fired synchronously. Now that they're really asynchronous, we somehow need to sync assertions with callbacks invocations.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You can refer to this comment to see the whole picture: #321 (comment)

if err != nil {
return nil, err
func (ep *ExecutionPolicy) executeSendTransaction(vm *vm.VM, req common.RPCCall) (map[string]interface{}, error) {
// TODO(tiabc): Move it to the bottom where `result` and `hash` are set.
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 move it down in this PR? It seems like it can be just returned with all values in return statement.

@@ -66,23 +66,54 @@ func registerHandlers(jail *Jail, cell common.JailCell, chatID string) error {
return nil
}

// makeAsyncSendHandler returns jeth.sendAsync() handler.
func makeAsyncSendHandler(jail *Jail, cellInt common.JailCell) func(call otto.FunctionCall) otto.Value {
// FIXME(tiabc): Get rid of this.
Copy link
Contributor

Choose a reason for hiding this comment

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

Hm? Can we get rid of this in this PR? I guess we can just use cell *Cell?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes but it better be done during some refactoring as it would introduce a number of unrelated changes. Generally, I'd like to revisit how we use jail cell and its interface.

go func() {
response := jail.Send(call, cell.VM)

if fn := call.Argument(1); fn.Class() == "Function" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hope it's always a a case params, callback :)

More bulletproof solution could be: fn := call.Argument(call.NumberOfArguments - 1), provided there is a prop which returns a number of arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, send in web3 always has a single argument. sendAsync always has only 2 arguments. If it changes in web3 we'd better receive an error during tests rather than come across some non-obvious behavior.

cell := cellInt.(*Cell)
return func(call otto.FunctionCall) otto.Value {
go func() {
response := jail.Send(call, cell.VM)
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we shouldn't call this outside of a goroutine and return the result... It usually should be undefiend but who knows.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, we shouldn't. This invocation is actually the main point of this PR which introduced all the other changes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh ok. I get it now after giving it a little bit more thinking :)

// method of jail.Cell and the cell is locked during that call. In order to allow jail.Send
// to perform any operations on cell.VM and not hang, we need to unlock the mutex and return
// it to the previous state afterwards so that the caller didn't panic doing cell.Unlock().
cell.Unlock()
Copy link
Contributor

Choose a reason for hiding this comment

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

So we're bypassing the fact that cell might be blocked by other call? E.g. there is a web3 call which hangs and send command is sent at the same time and this code will allow to do that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, it's alright and there's even a test for that. The reason is that jail.Send now make calls through cell.VM - the very same VM that is locked upon the initial call. So we need to unlock it. Otherwise, it's locked 2 times during a single jail.Call and hangs.

@@ -119,6 +125,36 @@ func (s *JailTestSuite) TestFunctionCall() {
require.Equal(expectedResponse, response)
}

func (s *JailTestSuite) TestJailRPCAsyncSend() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we make a simple test which also checks if a callback is called? I am not sure what we're testing here as web3.eth.sendTransaction(data, function(){}) would be called without these changes anyway, just in sync way I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's checked everywhere in a lot of tests, this is why I decided not to include another test.
I'll write a comment what this test is about.

@tiabc tiabc merged commit 5f19c9c into develop Sep 18, 2017
@tiabc tiabc mentioned this pull request Sep 18, 2017
@adambabik adambabik deleted the feature/send-async-#184 branch December 27, 2019 10:02
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.

4 participants