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

syscall/js: deadlock occurred with http.Get #25902

Closed
tenntenn opened this issue Jun 15, 2018 · 15 comments
Closed

syscall/js: deadlock occurred with http.Get #25902

tenntenn opened this issue Jun 15, 2018 · 15 comments
Labels
arch-wasm WebAssembly issues FrozenDueToAge
Milestone

Comments

@tenntenn
Copy link
Contributor

What version of Go are you using (go version)?

go version devel +4eb1c84752 Fri Jun 15 02:09:44 2018 +0000 darwin/amd64

Does this issue reproduce with the latest release?

It has occurred with 4eb1c84.

What operating system and processor architecture are you using (go env)?

GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/tenntenn/Library/Caches/go-build"
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/tenntenn/Documents/gopath"
GORACE=""
GOROOT="/usr/local/gotip"
GOTMPDIR=""
GOTOOLDIR="/usr/local/gotip/pkg/tool/darwin_amd64"
GCCGO="gccgo"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/mq/m9_twy8j1vl9ppr6j5xqtfq80000gn/T/go-build130694127=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

I built this code withGOOS=js GOARCH=wasm.

package main

import (
	"fmt"
	"net/http"
	"syscall/js"
)

func main() {
	var cb js.Callback
	cb = js.NewCallback(func(args []js.Value) {
		fmt.Println("Clicked")
		resp, err := http.Get("/")
		fmt.Println(resp, err)
	})
	js.Global.Get("document").Call("getElementById", "btn").Call("addEventListener", "click", cb)
	select {}
}

And I ran it on Chrome using misc/wasm/wasm_exec.js and misc/wasm/wasm_exec.html.
In addition, I added a button to wasm_exec.html.

It occurred deadlock.

fatal error: all goroutines are asleep - deadlock!
wasm_exec.js:39 
wasm_exec.js:39 goroutine 1 [select (no cases)]:
wasm_exec.js:39 main.main()
wasm_exec.js:39 	/path/to/dir/main.go:17 +0x7
wasm_exec.js:39 
wasm_exec.js:39 goroutine 5 [select]:
wasm_exec.js:39 net/http.(*Transport).RoundTrip(0x312b20, 0xc0d8000, 0x0, 0x0, 0x0)
wasm_exec.js:39 	/usr/local/gotip/src/net/http/roundtrip_js.go:123 +0x40
wasm_exec.js:39 net/http.send(0xc0d8000, 0xaa5a0, 0x312b20, 0x0, 0x0, 0x0, 0xc00c058, 0xf8, 0xc028ba0, 0x1)
wasm_exec.js:39 	/usr/local/gotip/src/net/http/client.go:252 +0x16
wasm_exec.js:39 net/http.(*Client).send(0x31ebc0, 0xc0d8000, 0x0, 0x0, 0x0, 0xc00c058, 0x0, 0x1, 0x6bfa0)
wasm_exec.js:39 	/usr/local/gotip/src/net/http/client.go:176 +0x10
wasm_exec.js:39 net/http.(*Client).Do(0x31ebc0, 0xc0d8000, 0x7468c, 0x1, 0x0)
wasm_exec.js:39 	/usr/local/gotip/src/net/http/client.go:615 +0x16
wasm_exec.js:39 net/http.(*Client).Get(0x31ebc0, 0x7468c, 0x1, 0x8, 0x0, 0x0)
wasm_exec.js:39 	/usr/local/gotip/src/net/http/client.go:396 +0x5
wasm_exec.js:39 net/http.Get(0x7468c, 0x1, 0x1, 0x8, 0x0)
wasm_exec.js:39 	/usr/local/gotip/src/net/http/client.go:370 +0x2
wasm_exec.js:39 main.main.func1(0xc01e280, 0x1, 0x1)
wasm_exec.js:39 	/path/to/dir/main.go:13 +0x3
wasm_exec.js:39 syscall/js.callbackLoop()
wasm_exec.js:39 	/usr/local/gotip/src/syscall/js/callback.go:139 +0x6
wasm_exec.js:39 created by syscall/js.NewCallback.func1
wasm_exec.js:39 	/usr/local/gotip/src/syscall/js/callback.go:65 +0x2

I tried to remove select{} but it immediately exit and it didn't kick the callback.

Uncaught Error: bad callback: Go program has already exited
    at _values (wasm_exec.js:305)
    at HTMLButtonElement.eval (eval at syscall/js.valueCall (wasm_exec.js:222), <anonymous>:5:4)

What did you expect to see?

Any errors do not occur with a click event and http.Get.

What did you see instead?

It occurred an error.

@agnivade
Copy link
Contributor

I got some more error at the bottom while trying to reproduce this -

(index):24 Uncaught (in promise) RangeError: WebAssembly Instantiation: Out of memory: wasm memory
async function (async)
run @ (index):22
onclick @ (index):29
wasm_loader.js:305 Uncaught (in promise) Error: bad callback: Go program has already exited
    at _values (wasm_loader.js:305)
    at eval (eval at syscall/js.valueCall (wasm_loader.js:222), <anonymous>:5:4)

/cc @neelance , @johanbrandhorst

@agnivade agnivade added NeedsFix The path to resolution is known, but the work has not been done. arch-wasm WebAssembly issues labels Jun 15, 2018
@agnivade agnivade added this to the Go1.11 milestone Jun 15, 2018
@johanbrandhorst
Copy link
Member

This is a nonsensical example, no? Add a waitgroup or something to monitor the progress of the callback before exiting.

@johanbrandhorst
Copy link
Member

I've investigated this a bit more, and indeed, I think there is a problem. Modifying the code like so still exhibits the same problem:

package main

import (
	"fmt"
	"net/http"
	"syscall/js"
)

func main() {
	var cb js.Callback
	done := make(chan struct{})
	cb = js.NewCallback(func(args []js.Value) {
		fmt.Println("Clicked")
		resp, err := http.Get("/")
		fmt.Println(resp, err)
		close(done)
	})
	js.Global.Get("document").Call("getElementById", "btn").Call("addEventListener", "click", cb)
	<-done
}

The problem is that, indeed, all goroutines are asleep (while waiting for Fetch I/O). I'm not sure how to solve this so will defer to @neelance.

@neelance
Copy link
Member

The problem is that you are blocking the callback loop. New calls to callbacks (like the HTTP response) can not be processed while some other callback handler (like the click handler) have not returned. You need to explicitly start a new goroutine:

package main

import (
	"fmt"
	"net/http"
	"syscall/js"
)

func main() {
	var cb js.Callback
	done := make(chan struct{})
	cb = js.NewCallback(func(args []js.Value) {
		go func() {
			fmt.Println("Clicked")
			resp, err := http.Get("/")
			fmt.Println(resp, err)
			close(done)
		}()
	})
	js.Global.Get("document").Call("getElementById", "btn").Call("addEventListener", "click", cb)
	<-done
}

@agnivade
Copy link
Contributor

Ah I see. Makes sense now. It was not immediately apparent that the callback loop is being blocked.

I should have read the documentation first - https://tip.golang.org/pkg/syscall/js/?GOOS=js&GOARCH=wasm#NewCallback

This execution happens asynchronously on a special goroutine that handles all callbacks and preserves the order in which the callbacks got called. As a consequence, if one callback blocks this goroutine, other callbacks will not be processed. A blocking callback should therefore explicitly start a new goroutine.

@neelance - Feel free to close the bug if you don't think there is anything else to be done.

@agnivade agnivade removed the NeedsFix The path to resolution is known, but the work has not been done. label Jun 15, 2018
@neelance
Copy link
Member

Can't close the bug since it is not assigned to me.

@tenntenn
Copy link
Contributor Author

Thank you for comments.
I understand about js.Callback behavior by your explanation.
But I think it is not good that users of a package should mind whether the package is using js.Callback or not.

@agnivade
Copy link
Contributor

I understand. Unfortunately this is working as intended and documented explicitly. So I don't think much can be done except educate the users.

Maybe in future, this behavior can change. But until then, it is unlikely we will be able to do anything.

@neelance
Copy link
Member

The only idea that I have to improve this is to rename NewCallback to NewOrderedCallback and then have NewCallback create a new goroutine implicitly. Is this something we should do?

@agnivade
Copy link
Contributor

Kinda goes against the set of callbacks we have - NewCallback for any generic callback and NewEventCallback for events. NewOrderedCallback introduces a different category of a callback than what we have. That's my thinking as an end user.

@hajimehoshi
Copy link
Member

I was wondering how the order of callbacks matters.

@neelance
Copy link
Member

There are several JS APIs that depend on it. Usually JS devs (me included when writing JS) don't spend much thought on this since it is "simply" a property of the execution model, so generally assumed (JS devs also don't think about issues due to preemption, because there is none). See https://go-review.googlesource.com/c/go/+/114197/6/src/syscall/js/callback.go#52 and https://go-review.googlesource.com/c/go/+/114197/8/src/syscall/js/callback.go#57.

@mattn
Copy link
Member

mattn commented Jun 17, 2018

I probably not understand what is problem. Can we add function that wait all callbacks?

var (
	callbacks = map[Callback]chan struct{}{}
	mu sync.Mutex
)

func NewCallback(f func(args []Value)) Callback {
	// do something...
	return func(args []Value) {
		ch := make(chan struct{})
		mu.Lock()
		callbacks[f] = ch
		mu.Unlock()
		go func() {
			f(args)

			close(ch)
			mu.Lock()
			delete(callbacks, ch)
			mu.Unlock()
		}()
	}
}

func Wait() {
	for {
		// wait all callbacks
	}
}

@neelance
Copy link
Member

@mattn Now I don't get what you are saying...

@mattn
Copy link
Member

mattn commented Jun 18, 2018

@neelance I mean that I wonder that Go really can't solve this. For example, I don't make sure, is it possible to make channel/goroutine in Go instead of user code?

@golang golang locked and limited conversation to collaborators Jun 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
arch-wasm WebAssembly issues FrozenDueToAge
Projects
None yet
Development

No branches or pull requests

7 participants