What version of Go are you using (go version)?
$ go version go version go1.23.6 linux/amd64
Does this issue reproduce with the latest release?
Yes (1.24.0)
What operating system and processor architecture are you using (go env)?
go env Output
$ go env GO111MODULE='' GOARCH='amd64' GOBIN='' GOCACHE='/home/dadanhrn/.cache/go-build' GOENV='/home/dadanhrn/.config/go/env' GOEXE='' GOEXPERIMENT='' GOFLAGS='' GOHOSTARCH='amd64' GOHOSTOS='linux' GOINSECURE='' GOMODCACHE='/home/dadanhrn/go/pkg/mod' GONOPROXY='' GONOSUMDB='' GOOS='linux' GOPATH='/home/dadanhrn/go' GOPRIVATE='' GOPROXY='direct' GOROOT='/usr/lib/golang' GOSUMDB='off' GOTMPDIR='' GOTOOLCHAIN='local' GOTOOLDIR='/usr/lib/golang/pkg/tool/linux_amd64' GOVCS='' GOVERSION='go1.23.6' GODEBUG='' GOTELEMETRY='local' GOTELEMETRYDIR='/home/dadanhrn/.config/go/telemetry' GCCGO='gccgo' GOAMD64='v1' AR='ar' CC='gcc' CXX='g++' CGO_ENABLED='1' GOMOD='/home/dadanhrn/Documents/Kuliah/trapeze-go_debug/go.mod' GOWORK='' CGO_CFLAGS='-O2 -g' CGO_CPPFLAGS='' CGO_CXXFLAGS='-O2 -g' CGO_FFLAGS='-O2 -g' CGO_LDFLAGS='-O2 -g' PKG_CONFIG='pkg-config' GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3041393783=/tmp/go-build -gno-record-gcc-switches' GOROOT/bin/go version: go version go1.23.6 linux/amd64 GOROOT/bin/go tool compile -V: compile version go1.23.6 uname -sr: Linux 6.13.4-200.fc41.x86_64 /lib64/libc.so.6: GNU C Library (GNU libc) stable release version 2.40. gdb --version: GNU gdb (Fedora Linux) 16.2-1.fc41
What did you do?
Example code: https://go.dev/play/p/j5I16KsARN_i
The HTTP request can be replaced with a query to an SQL database or just any operation that takes context.Context. Set the timeout long enough so the operation can finish before the context timeouts. Notice the defer cancel() statement.
I compiled the program with GOOS=js GOARCH=wasm go build -o main.wasm and run the wasm binary on Node.js v22.13.0 with the provided wasm_exec.js. I added several print statements around these lines to help with debugging https://pastebin.com/ZTUnDYwn
Here's the Node.js script to run the wasm binary https://pastebin.com/CDNMS5Au
What did you expect to see?
###### SCHEDULE TIMEOUT EVENT 8 2951
Get "https://www.google.com": dial tcp: lookup www.google.com on [::1]:53: write udp 127.0.0.1:8->[::1]:53: write: Connection reset by peer
###### CLEAR TIMEOUT EVENT 8
I suppose the connection reset error is expected due to this https://cs.opensource.google/go/go/+/master:src/net/http/roundtrip_js.go;l=49-57?q=jsFetchDisabled&ss=go%2Fgo
What did you see instead?
###### SCHEDULE TIMEOUT EVENT 8 2951
Get "https://www.google.com": dial tcp: lookup www.google.com on [::1]:53: write udp 127.0.0.1:8->[::1]:53: write: Connection reset by peer
###### TRIGGER TIMEOUT EVENT 8
/home/dadanhrn/Documents/Kuliah/debug_timer/wasm_exec.js:560
throw new Error("Go program has already exited");
^
Error: Go program has already exited
at globalThis.Go._resume (/home/dadanhrn/Documents/Kuliah/debug_timer/wasm_exec.js:560:11)
at Timeout._onTimeout (/home/dadanhrn/Documents/Kuliah/debug_timer/wasm_exec.js:286:14)
at listOnTimeout (node:internal/timers:594:17)
at process.processTimers (node:internal/timers:529:7)
Node.js v22.13.0
I suppose it makes sense that calling the cancel() function (through defer in this case) should also clear the timeout in the host Node.js runtime.
Comment From: gabyhelp
Related Issues
- net/http: roundtrip_js.go fails to handle fetch() Promise if RoundTrip context is canceled #57098
- cmd/compile: uncaught RuntimeError: memory access out of bounds #38093 (closed)
- runtime: wasm: fatal error: all goroutines are asleep - deadlock! #41310 (closed)
- misc/wasm: deadlock from http.Get in syscall/js.FuncOf #37136 (closed)
- net/http: Dial I/O Timeout even when request context is not canceled #36848
- net/http: js-wasm in nodejs HTTP requests fail #69106
- runtime: scheduler sometimes starves a runnable goroutine on wasm platforms #65178 (closed)
- runtime: wasm runtime crashes under certain/undefined conditions #38574 (closed)
- net/http: WASM http.Get fails to read response and deadlocks when called in event listener #52737 (closed)
- net/http: slight scheduling changes makes TestServerWriteTimeout/h2 flaky (primarily wasm) #65410 (closed)
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: dr2chase
@golang/wasm
Comment From: johanbrandhorst
Looks like maybe we're not cleaning up timers when the program exits? These are created by https://cs.opensource.google/go/go/+/master:src/runtime/lock_js.go;l=267;bpv=1;bpt=0?q=scheduleTimeoutEvent&ss=go%2Fgo.
Comment From: dadanhrn
Adding something like this in the scheduleTimeoutEvent implementation eliminates the crash, but it still leaves a dangling timeout which prevents the Node.js runtime from terminating immediately after the wasm call
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
if (this.exited) { // <== this
return
}
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try>
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
Comment From: Zxilly
maybe we can cancel all _scheduledTimeouts in the "runtime.wasmExit"?
Comment From: Zxilly
diff --git a/lib/wasm/wasm_exec.js b/lib/wasm/wasm_exec.js
--- a/lib/wasm/wasm_exec.js (revision 6fb7bdc96d0398fab313586fba6fdc89cc14c679)
+++ b/lib/wasm/wasm_exec.js (date 1739646571863)
@@ -243,6 +243,10 @@
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
+ for (const id of this._scheduledTimeouts) {
+ clearTimeout(id[1]);
+ }
+ delete this._scheduledTimeouts;
this.exit(code);
},
Comment From: dadanhrn
maybe we can cancel all
_scheduledTimeoutsin the "runtime.wasmExit"?
I confirm that this works. However,
- are there any specific cases where the wasm program exits without calling runtime.wasmExit? (Other than when the host Node.js runtime gets interrupted)
- I suppose this works as a garbage collection measure, which only suppresses the symptom rather than solving the root cause
Comment From: Zxilly
-
runtime.wasmExitwas called fromruntime.exit, I think this will always happen. -
I agree that it's just a workaround, but since wasm is single-threaded, we rely on js to do the swapping of contexts and scheduling. I think a meaningful fix should wait until
WASM Threadis implemented.
Comment From: gopherbot
Change https://go.dev/cl/685535 mentions this issue: wasm: clear remaining timeouts on exit