uber-go / mock

GoMock is a mocking framework for the Go programming language.
Apache License 2.0
1.81k stars 103 forks source link

invocation from timer triggered go routine will report data race #173

Open zdevwu opened 2 months ago

zdevwu commented 2 months ago

There is a strange behaviour for the Call when triggered in go routines, a data race will be detected from timer triggered go routine but not in channel triggered go routine, see the bellow test cases:

func TestChannelTriggeredGoRoutineInvocation(t *testing.T) {
    ctrl := gomock.NewController(t)
    m := mock.NewMockMath(ctrl)

    called := make(chan struct{})
    releaseChannel := make(chan struct{})
    go func() {
        <-releaseChannel
        go func() {
            m.Sum(1, 1)
            called <- struct{}{}
        }()
    }()

    m.EXPECT().
        Sum(gomock.Any(), gomock.Any()).
        Return(2)
    close(releaseChannel)
    <-called
}

func TestTimerTriggeredGoRoutineInvocation(t *testing.T) {
    t.Skip("this test should not fail but it does")
    ctrl := gomock.NewController(t)
    m := mock.NewMockMath(ctrl)

    called := make(chan struct{})
    go func() {
        <-time.After(time.Millisecond * 300)
        go func() {
            m.Sum(1, 1)
            called <- struct{}{}
        }()
    }()

    m.EXPECT().
        Sum(gomock.Any(), gomock.Any()).
        Return(2)
    <-called
}
==================
WARNING: DATA RACE
Read at 0x00c000138160 by goroutine 18:
  go.uber.org/mock/gomock.(*Call).call()
      /home/zdevwu/projects/mock/gomock/call.go:432 +0x7ba
  go.uber.org/mock/gomock.(*Controller).Call.func1()
      /home/zdevwu/projects/mock/gomock/controller.go:224 +0x767
  go.uber.org/mock/gomock.(*Controller).Call()
      /home/zdevwu/projects/mock/gomock/controller.go:229 +0xc6
  go.uber.org/mock/sample/concurrent/mock.(*MockMath).Sum()
      /home/zdevwu/projects/mock/sample/concurrent/mock/concurrent_mock.go:44 +0x176
  go.uber.org/mock/sample/concurrent.TestTimerTriggeredGoRoutineInvocation.func1.1()
      /home/zdevwu/projects/mock/sample/concurrent/concurrent_test.go:119 +0x3b

Previous write at 0x00c000138160 by goroutine 16:
  go.uber.org/mock/gomock.(*Call).addAction()
      /home/zdevwu/projects/mock/gomock/call.go:497 +0xe8c
  go.uber.org/mock/gomock.(*Call).Return()
      /home/zdevwu/projects/mock/gomock/call.go:220 +0x3e9
  go.uber.org/mock/sample/concurrent.TestTimerTriggeredGoRoutineInvocation()
      /home/zdevwu/projects/mock/sample/concurrent/concurrent_test.go:126 +0x2d8
  testing.tRunner()
      /snap/go/10585/src/testing/testing.go:1689 +0x21e
  testing.(*T).Run.gowrap1()
      /snap/go/10585/src/testing/testing.go:1742 +0x44

Goroutine 18 (running) created at:
  go.uber.org/mock/sample/concurrent.TestTimerTriggeredGoRoutineInvocation.func1()
      /home/zdevwu/projects/mock/sample/concurrent/concurrent_test.go:118 +0x104

Goroutine 16 (running) created at:
  testing.(*T).Run()
      /snap/go/10585/src/testing/testing.go:1742 +0x825
  testing.runTests.func1()
      /snap/go/10585/src/testing/testing.go:2161 +0x85
  testing.tRunner()
      /snap/go/10585/src/testing/testing.go:1689 +0x21e
  testing.runTests()
      /snap/go/10585/src/testing/testing.go:2159 +0x8be
  testing.(*M).Run()
      /snap/go/10585/src/testing/testing.go:2027 +0xf17
  main.main()
      _testmain.go:55 +0x2bd
==================
==================
WARNING: DATA RACE
Read at 0x00c0001161c8 by goroutine 18:
  go.uber.org/mock/gomock.(*Controller).Call()
      /home/zdevwu/projects/mock/gomock/controller.go:232 +0xeb
  go.uber.org/mock/sample/concurrent/mock.(*MockMath).Sum()
      /home/zdevwu/projects/mock/sample/concurrent/mock/concurrent_mock.go:44 +0x176
  go.uber.org/mock/sample/concurrent.TestTimerTriggeredGoRoutineInvocation.func1.1()
      /home/zdevwu/projects/mock/sample/concurrent/concurrent_test.go:119 +0x3b

Previous write at 0x00c0001161c8 by goroutine 16:
  go.uber.org/mock/gomock.(*Call).addAction()
      /home/zdevwu/projects/mock/gomock/call.go:497 +0xe30
  go.uber.org/mock/gomock.(*Call).Return()
      /home/zdevwu/projects/mock/gomock/call.go:220 +0x3e9
  go.uber.org/mock/sample/concurrent.TestTimerTriggeredGoRoutineInvocation()
      /home/zdevwu/projects/mock/sample/concurrent/concurrent_test.go:126 +0x2d8
  testing.tRunner()
      /snap/go/10585/src/testing/testing.go:1689 +0x21e
  testing.(*T).Run.gowrap1()
      /snap/go/10585/src/testing/testing.go:1742 +0x44

Goroutine 18 (running) created at:
  go.uber.org/mock/sample/concurrent.TestTimerTriggeredGoRoutineInvocation.func1()
      /home/zdevwu/projects/mock/sample/concurrent/concurrent_test.go:118 +0x104

Goroutine 16 (running) created at:
  testing.(*T).Run()
      /snap/go/10585/src/testing/testing.go:1742 +0x825
  testing.runTests.func1()
      /snap/go/10585/src/testing/testing.go:2161 +0x85
  testing.tRunner()
      /snap/go/10585/src/testing/testing.go:1689 +0x21e
  testing.runTests()
      /snap/go/10585/src/testing/testing.go:2159 +0x8be
  testing.(*M).Run()
      /snap/go/10585/src/testing/testing.go:2027 +0xf17
  main.main()
      _testmain.go:55 +0x2bd
==================
    concurrent_test.go:128: race should be detected
    testing.go:1398: race detected during execution of test
--- FAIL: TestTimerTriggeredInvocation (0.31s)

The fork to demo the issue here:

https://github.com/zdevwu/mock/commit/0a962b4727acd05a4a8b0568c1f083f98473a378

Triage Notes for the Maintainers

r-hang commented 2 months ago

Hey @zdevwu, are you expecting the mock generated methods to be concurrency safe? If so, they're not expected to be.

With respect to the two cases you gave above, the first case does not race as he first goroutine only gets unblocked after the mock expectation is declared which serializes the the Sum expectation setting and the mock call to Sum.

zdevwu commented 2 months ago

Thanks for getting back @r-hang, can you elaborate a bit on the second case please? Although not explicitly sending the signal by closing the releaseChannel to continue to launch the go routine, the timer in second case makes sure the mock happens before the nested go routine that is calling Sum start running, both cases the mock happens before the call, but race is only detected in the second case with timer released go routine.