-
Notifications
You must be signed in to change notification settings - Fork 249
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
Conversation
geth/jail/execution_policy.go
Outdated
} else { | ||
resp = newErrorResponse(call.Otto, -32603, err.Error(), &req.ID).Object() | ||
resp = newErrorResponse(-32603, err.Error(), &req.ID) |
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.
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
)
geth/jail/execution_policy.go
Outdated
} | ||
|
||
return "" | ||
msgIDStr, err := msgID.ToString() |
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.
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
@@ -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. |
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.
and routes appropriately to either a local or remote ethereum node
should be removed, this function doesn't do routing anymore.
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) |
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.
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.
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 guess that make sense, do we still need ExecutionPolicy
?
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.
Can we refactor this out or rename?
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 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) |
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.
Do we have tests ensuring we are not loosing references anymore?
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.
We've never lost references. These were just race conditions. But we have one decent test anyway:
status-go/geth/jail/jail_test.go
Line 128 in 6800cf8
func (s *JailTestSuite) TestJailRPCAsyncSend() { |
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.
Alright
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.
Looks aright
cell := cellInt.(*Cell) | ||
return func(call otto.FunctionCall) otto.Value { | ||
go func() { | ||
response := jail.Send(call, cell.VM) |
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.
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?
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.
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.
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.
Ok. I'm not sure I completely understand workflow here, but fine for now.
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 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.
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.
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.
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)?
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.
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) + ")") |
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 looks a bit cryptic. Why do we call JS to form 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.
Shouldnt we assign it into a variable in the script source to be runned?
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.
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 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/... |
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.
Didnt we already change this in another PR?
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.
Yes but it's not merged yet.
Sorry, @adambabik, didn't get your comment. My comment was about waiting callbacks, are we on the same page? |
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.
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. |
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.
Oh, so was it failing sometimes because of invalid responseValue
? I have never noticed that...
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.
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.
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.
You can refer to this comment to see the whole picture: #321 (comment)
geth/jail/execution_policy.go
Outdated
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. |
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.
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. |
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.
Hm? Can we get rid of this in this PR? I guess we can just use cell *Cell
?
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.
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" { |
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.
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.
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.
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) |
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 wonder if we shouldn't call this outside of a goroutine and return the result... It usually should be undefiend
but who knows.
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.
No, we shouldn't. This invocation is actually the main point of this PR which introduced all the other changes.
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.
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() |
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.
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?
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.
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.
geth/jail/jail_test.go
Outdated
@@ -119,6 +125,36 @@ func (s *JailTestSuite) TestFunctionCall() { | |||
require.Equal(expectedResponse, response) | |||
} | |||
|
|||
func (s *JailTestSuite) TestJailRPCAsyncSend() { |
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.
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.
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'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.
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:
Send
andSendAsync
now usecell.VM
instead ofotto.Otto
providing proper locking.ExecuionPolicy.ExecuteWithClient
is now done intovar result interface{}
instead ofvar result json.RawMessage
because test case 0 ofTestJailWhisper
failed providing byte codes instead of5.0
.The only failing test is
TestJailWhisper
. Refer to #336 for explanations.