From 049a5e603634dfdbc73b4255c7e72eabc3922d5b Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 5 Jun 2025 13:55:35 -0700 Subject: [PATCH] runtime: return a different bubble deadlock error when main goroutine is done The synctest.Test function waits for all goroutines in a bubble to exit before returning. If there is ever a point when all goroutines in a bubble are durably blocked, it panics and reports a deadlock. Panic with a different message depending on whether the bubble's main goroutine has returned or not. The main goroutine returning stops the bubble clock, so knowing whether it is running or not is useful debugging information. The new panic messages are: deadlock: all goroutines in bubble are blocked deadlock: main bubble goroutine has exited but blocked goroutines remain Change-Id: I94a69e79121c272d9c86f412c1c9c7de57ef27ef Reviewed-on: https://go-review.googlesource.com/c/go/+/679375 Auto-Submit: Damien Neil Reviewed-by: Michael Pratt LUCI-TryBot-Result: Go LUCI --- src/internal/synctest/synctest_test.go | 4 ++-- src/runtime/synctest.go | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/internal/synctest/synctest_test.go b/src/internal/synctest/synctest_test.go index c2f84be736..fe6eb63702 100644 --- a/src/internal/synctest/synctest_test.go +++ b/src/internal/synctest/synctest_test.go @@ -488,7 +488,7 @@ func TestDeadlockRoot(t *testing.T) { } func TestDeadlockChild(t *testing.T) { - defer wantPanic(t, "deadlock: all goroutines in bubble are blocked") + defer wantPanic(t, "deadlock: main bubble goroutine has exited but blocked goroutines remain") synctest.Run(func() { go func() { select {} @@ -497,7 +497,7 @@ func TestDeadlockChild(t *testing.T) { } func TestDeadlockTicker(t *testing.T) { - defer wantPanic(t, "deadlock: all goroutines in bubble are blocked") + defer wantPanic(t, "deadlock: main bubble goroutine has exited but blocked goroutines remain") synctest.Run(func() { go func() { for range time.Tick(1 * time.Second) { diff --git a/src/runtime/synctest.go b/src/runtime/synctest.go index c837c792a5..08a0e5d444 100644 --- a/src/runtime/synctest.go +++ b/src/runtime/synctest.go @@ -242,7 +242,13 @@ func synctestRun(f func()) { raceacquireg(gp, gp.bubble.raceaddr()) } if total != 1 { - panic(synctestDeadlockError{bubble}) + var reason string + if bubble.done { + reason = "deadlock: main bubble goroutine has exited but blocked goroutines remain" + } else { + reason = "deadlock: all goroutines in bubble are blocked" + } + panic(synctestDeadlockError{reason: reason, bubble: bubble}) } if gp.timer != nil && gp.timer.isFake { // Verify that we haven't marked this goroutine's sleep timer as fake. @@ -252,11 +258,12 @@ func synctestRun(f func()) { } type synctestDeadlockError struct { + reason string bubble *synctestBubble } -func (synctestDeadlockError) Error() string { - return "deadlock: all goroutines in bubble are blocked" +func (e synctestDeadlockError) Error() string { + return e.reason } func synctestidle_c(gp *g, _ unsafe.Pointer) bool { -- 2.50.0