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
_scheduledTimeouts
in 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.wasmExit
was 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 Thread
is implemented.
Comment From: gopherbot
Change https://go.dev/cl/685535 mentions this issue: wasm: clear remaining timeouts on exit