]> Cypherpunks repositories - gostls13.git/commitdiff
runtime: prevent time.Timer.Reset(0) from deadlocking testing/synctest tests
authorNicholas S. Husin <nsh@golang.org>
Sat, 1 Nov 2025 15:15:58 +0000 (11:15 -0400)
committerGopher Robot <gobot@golang.org>
Sat, 1 Nov 2025 15:37:04 +0000 (08:37 -0700)
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>

src/internal/synctest/synctest_test.go
src/runtime/time.go

index 73a0a1c453e3d5053eb2a633145030ba86074995..d5ac1e5c1f80dbe9f9663aa54c51c6bd13fe30fc 100644 (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 {
index e9d1f0b6c9a10f90bccb6bf8575239255f82a7be..81a4c6b79f1e66b96a479d366f33fb1a465d094a 100644 (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