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 <dneil@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Damien Neil 2025-06-05 13:55:35 -07:00 committed by Gopher Robot
parent ac1686752b
commit 049a5e6036
2 changed files with 12 additions and 5 deletions

View file

@ -488,7 +488,7 @@ func TestDeadlockRoot(t *testing.T) {
} }
func TestDeadlockChild(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() { synctest.Run(func() {
go func() { go func() {
select {} select {}
@ -497,7 +497,7 @@ func TestDeadlockChild(t *testing.T) {
} }
func TestDeadlockTicker(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() { synctest.Run(func() {
go func() { go func() {
for range time.Tick(1 * time.Second) { for range time.Tick(1 * time.Second) {

View file

@ -242,7 +242,13 @@ func synctestRun(f func()) {
raceacquireg(gp, gp.bubble.raceaddr()) raceacquireg(gp, gp.bubble.raceaddr())
} }
if total != 1 { 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 { if gp.timer != nil && gp.timer.isFake {
// Verify that we haven't marked this goroutine's sleep timer as fake. // Verify that we haven't marked this goroutine's sleep timer as fake.
@ -252,11 +258,12 @@ func synctestRun(f func()) {
} }
type synctestDeadlockError struct { type synctestDeadlockError struct {
reason string
bubble *synctestBubble bubble *synctestBubble
} }
func (synctestDeadlockError) Error() string { func (e synctestDeadlockError) Error() string {
return "deadlock: all goroutines in bubble are blocked" return e.reason
} }
func synctestidle_c(gp *g, _ unsafe.Pointer) bool { func synctestidle_c(gp *g, _ unsafe.Pointer) bool {