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

all goroutines are dead asleep - deadlock! wasm docs #426

Open
otaxhu opened this issue Nov 30, 2023 · 8 comments
Open

all goroutines are dead asleep - deadlock! wasm docs #426

otaxhu opened this issue Nov 30, 2023 · 8 comments
Labels

Comments

@otaxhu
Copy link

otaxhu commented Nov 30, 2023

I'm trying to do the next:

Client Code:

//go:build js && wasm

package main

import (
	"context"
	"log"
	"syscall/js"

	"nhooyr.io/websocket"
)

func main() {
	ctx := context.Background()
	window := js.Global()
	window.Set("goWS", js.FuncOf(func(_ js.Value, args []js.Value) any {
		conn, _, err := websocket.Dial(ctx, args[0].String(), nil)
		if err != nil {
			log.Fatal(err)
		}
		goWSObject := window.Get("goWS")
		goWSObject.Set("send", js.FuncOf(func(_ js.Value, args []js.Value) any {
			if err := conn.Write(ctx, websocket.MessageText, []byte(args[0].String())); err != nil {
				log.Fatal(err)
			}
			return nil
		}))
		goWSObject.Set("waitMessage", js.FuncOf(func(_ js.Value, _ []js.Value) any {
			_, bb, err := conn.Read(ctx)
			if err != nil {
				log.Fatal(err)
			}
			return bb
		}))
		return nil
	}))
	waitCh := make(chan struct{})
	<-waitCh
}

index.html:

<!doctype html>
<html lang="en">
<head>
	<meta charset="UTF-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<title>goWS</title>
	<script src="/assets/wasm_exec.js"></script>
	<script>
		const go = new Go();
		WebAssembly.instantiateStreaming(fetch("/assets/app.wasm"), go.importObject)
			.then(res => go.run(res.instance))
	</script>
</head>
<body>
</body>
</html>

Server code:

package main

import (
	"context"
	"net/http"

	"nhooyr.io/websocket"
)

func main() {
	ctx := context.Background()

	http.HandleFunc("/ws-chat", func(w http.ResponseWriter, r *http.Request) {
		conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
			InsecureSkipVerify: true,
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		_, bb, err := conn.Read(ctx)
		if err != nil {
			conn.Close(websocket.StatusInternalError, err.Error())
			return
		}
		wr, err := conn.Writer(ctx, websocket.MessageText)
		if err != nil {
			conn.Close(websocket.StatusInternalError, err.Error())
			return
		}
		wr.Write(bb)
		wr.Close()
		conn.CloseNow()
	})
	http.ListenAndServe(":8080", http.DefaultServeMux)
}

Also I'm going to attach my Makefile so you can see the commands when I'm compiling:

build_client:
	GOOS=js GOARCH=wasm go build -o dist/assets/app.wasm cmd/go-ws-client/main.go
	cp $(go env GOROOT)/misc/wasm/wasm_exec.js dist/assets/

build_server:
	go build -o dist/bin/app-server cmd/go-ws-server/main.go

Structure of dist folder:

dist/
	- assets/
		- app.wasm
		- wasm_exec.js
	- bin/
		- app-server
	- index.html

But i'm getting the following errors after calling window.goWS("ws://localhost:8080/ws-chat") from the firefox browser's console:

Console:

> window.goWS("ws://localhost:8080/ws-chat")
fatal error: all goroutines are asleep - deadlock! wasm_exec.js:22:14
<empty string>
wasm_exec.js:22:14
goroutine 1 [chan receive]: wasm_exec.js:22:14
main.main() wasm_exec.js:22:14
	/home/oscar/Escritorio/proyecto-react/cmd/go-ws-client/main.go:47 +0xc wasm_exec.js:22:14
<empty string> wasm_exec.js:22:14
goroutine 7 [select]: wasm_exec.js:22:14
nhooyr.io/websocket.dial({0x5aa80, 0x1430050}, {0x1466020, 0x1b}, 0x0) wasm_exec.js:22:14
	/home/oscar/go/pkg/mod/nhooyr.io/websocket@v1.8.10/ws_js.go:320 +0x1c wasm_exec.js:22:14
nhooyr.io/websocket.Dial({0x5aa80, 0x1430050}, {0x1466020, 0x1b}, 0x0) wasm_exec.js:22:14
	/home/oscar/go/pkg/mod/nhooyr.io/websocket@v1.8.10/ws_js.go:289 +0x2 wasm_exec.js:22:14
main.main.func1.1({{}, 0x0, 0x0}, {0x140e0f0, 0x1, 0x1}) wasm_exec.js:22:14
	/home/oscar/Escritorio/proyecto-react/cmd/go-ws-client/main.go:19 +0x4 wasm_exec.js:22:14
syscall/js.handleEvent() wasm_exec.js:22:14
	/usr/local/go/src/syscall/js/func.go:100 +0x26 wasm_exec.js:22:14
exit code: 2 wasm_exec.js:101:14
undefined
@nhooyr
Copy link
Contributor

nhooyr commented Nov 30, 2023

Unrelated to this library, see golang/go#41310

@nhooyr nhooyr closed this as completed Nov 30, 2023
@otaxhu
Copy link
Author

otaxhu commented Nov 30, 2023

I don't know if you are too busy af but I found the answer and I don't know if you have the time to create an example of a ping pong websocket client compiled in WASM.

So what I did was to require a callback, passing the message as an argument, the reason is because of async nature of (*Conn) Read() method, minimal reproduction following:

//go:build js && wasm

package main

import (
	"context"
	"syscall/js"

	"nhooyr.io/websocket"
)

func main() {
	ctx := context.Background()
	conn, _, err := websocket.Dial(ctx, "ws://localhost:8080/ping-pong", nil)
	if err != nil {
		panic(err)
	}
	wr, err := conn.Writer(ctx, websocket.MessageText)
	if err != nil {
		panic(err)
	}
	js.Global().Set("sendMessageGo", js.FuncOf(func(_ js.Value, args []js.Value) any {
		// args[0] is the string that want to sent to the server
		wr.Write([]byte(args[0].String()))
		wr.Close()
		// spawning a goroutine because of conn.Read() is async, and js functions cannot wait
		go func() {
			_, bb, err := conn.Read(ctx)
			if err != nil {
				panic(err)
			}
			// args[1] is the callback, has the shape (bytes) => {/* any operation with the bytes */}
			// passing to the callback the bytes in string format
			args[1].Invoke(string(bb))
		}()
		// the js functions must return inmediatly, cannot wait for async operations
		return nil
	}))
	waitCh := make(chan struct{})
	<-waitCh
}

@nhooyr
Copy link
Contributor

nhooyr commented Nov 30, 2023

Hmm weird. I have this test here which runs in WASM and requires no extra goroutines. https://github.com/nhooyr/websocket/blob/master/ws_js_test.go

I'm def a little busy to look into exactly what's going on in your example. I'll open this up again and look into it later.

@nhooyr nhooyr reopened this Nov 30, 2023
@nhooyr nhooyr changed the title Bug: all goroutines are dead asleep - deadlock! WebAssembly WS Client all goroutines are dead asleep - deadlock! wasm docs Nov 30, 2023
@otaxhu
Copy link
Author

otaxhu commented Nov 30, 2023

don't worry, I saw in the tests that you are not testing the (*Conn) .Read() method, it's only dialing to a ws server, no reading

@nhooyr
Copy link
Contributor

nhooyr commented Nov 30, 2023

No it is, see this line https://github.com/nhooyr/websocket/blob/e3a2d32f704fb06c439e56d2a85334de04b50d32/ws_js_test.go#L32

It writes a message and then confirms the same message is read back.

@otaxhu
Copy link
Author

otaxhu commented Nov 30, 2023

forget it, you are in fact reading in the wstest.Echo, but I think it has something to do when you are binding the Go function with the JS function

@otaxhu
Copy link
Author

otaxhu commented Dec 1, 2023

I tried this other example and also worked, with Promise object instead of callback for compatibility with async and await js keywords:

//go:build js && wasm

package main

import (
	"context"
	"syscall/js"

	"nhooyr.io/websocket"
)

func main() {
	ctx := context.Background()
	conn, _, err := websocket.Dial(ctx, "ws://localhost:8080/ws-chat", nil)
	if err != nil {
		panic(err)
	}

	js.Global().Set("sendMessageGo", js.FuncOf(func(_ js.Value, args []js.Value) any {
		wr, err := conn.Writer(ctx, websocket.MessageText)
		if err != nil {
			panic(err)
		}
		wr.Write([]byte(args[0].String()))
		wr.Close()
		// returning a Promise
		return js.Global().Get("Promise").New(js.FuncOf(func(_ js.Value, args []js.Value) any {

			// args[0] is resolve callback
			// args[1] is reject callback

			// spawining a goroutine because of conn.Read() blocking nature
			go func() {
				_, bb, err := conn.Read(ctx)
				if err != nil {
					// if there is an error, reject the promise, calling the reject callback
					args[1].Invoke(err.Error())
					return
				}
				// if there is no error then pass to resolve the bytes that comes from the server
				args[0].Invoke(string(bb))
			}()
			// also the constructor must return inmediatly, cannot wait for async operations
			return nil
		}))
	}))
	waitCh := make(chan struct{})
	<-waitCh
}

@nhooyr
Copy link
Contributor

nhooyr commented Mar 7, 2024

Ah yes I see, you probably can't block in a JS callback so you have to return a promise. That makes sense. We can document it for sure. See also https://www.reddit.com/r/WebAssembly/comments/nm69e8/blocking_calls_in_wasm/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants