runtime: prevent time.Timer.Reset(0) from deadlocking testing/synctest tests

In Go 1.23+, timer channels behave synchronously. When we have a timer
channel (i.e. !async && t.isChan) we would lock the
runtime.timer.sendLock mutex at the beginning of
runtime.timer.modify()'s execution.

Calling time.Timer.Reset(0) within a testing/synctest test,
unfortunately, causes it to hang indefinitely. This is because the
runtime.timer.sendLock mutex ends up being locked twice before it could
be unlocked:

- When calling time.Timer.Reset(), runtime.timer.modify() would lock the
  mutex per usual.
- Due to the 0 argument, runtime.timer.modify() would also try to
  execute the bubbled timer immediately rather than adding them to a
  heap. However, in doing so, it uses runtime.timer.unlockAndRun(),
  which also locks the same mutex.

This CL solves this issue by making sure that a locked
runtime.timer.sendLock mutex is unlocked first, whenever we try to
execute bubbled timer immediately in the stack.

Fixes #76052

Change-Id: I66429b9bf6971400de95dcf2d5dc9670c3135492
Reviewed-on: https://go-review.googlesource.com/c/go/+/716883
Reviewed-by: Damien Neil <dneil@google.com>
Auto-Submit: Nicholas Husin <nsh@golang.org>
Reviewed-by: Nicholas Husin <husin@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
This commit is contained in:
Nicholas S. Husin 2025-11-01 11:15:58 -04:00 committed by Gopher Robot
parent 99b724f454
commit 385dc33250
2 changed files with 29 additions and 0 deletions

View file

@ -5,6 +5,7 @@
package synctest_test
import (
"context"
"fmt"
"internal/synctest"
"internal/testenv"
@ -329,6 +330,31 @@ func TestAfterFuncRunsImmediately(t *testing.T) {
})
}
// TestTimerResetZeroDoNotHang verifies that using timer.Reset(0) does not
// cause the test to hang indefinitely. See https://go.dev/issue/76052.
func TestTimerResetZeroDoNotHang(t *testing.T) {
synctest.Run(func() {
timer := time.NewTimer(0)
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
return
case <-timer.C:
}
}
}()
synctest.Wait()
timer.Reset(0)
synctest.Wait()
cancel()
synctest.Wait()
})
}
func TestChannelFromOutsideBubble(t *testing.T) {
choutside := make(chan struct{})
for _, test := range []struct {

View file

@ -636,6 +636,9 @@ func (t *timer) modify(when, period int64, f func(arg any, seq uintptr, delay in
}
if t.state&timerHeaped == 0 && when <= bubble.now {
systemstack(func() {
if !async && t.isChan {
unlock(&t.sendLock)
}
t.unlockAndRun(bubble.now, bubble)
})
return pending