This code works as I expect it
import (
"fmt"
"time"
"github.com/benbjohnson/clock"
)
func main() {
mockClock := clock.NewMock()
timer := mockClock.Timer(time.Duration(2) * time.Second)
go func() {
<-timer.C
fmt.Println("Done")
}()
mockClock.Add(time.Duration(10) * time.Second)
time.Sleep(1)
}
It prints "Done" as I expect.
Whereas this function does not
import (
"fmt"
"time"
"github.com/benbjohnson/clock"
)
func main() {
mockClock := clock.NewMock()
go func() {
timer := mockClock.Timer(time.Duration(2) * time.Second)
<-timer.C
fmt.Println("Done")
}()
mockClock.Add(time.Duration(10) * time.Second)
time.Sleep(1)
}
The only difference here is I'm declaring the timer outside the goroutine vs. inside it. The mockClock
Timer()
method has a pointer receiver and returns a pointer. I can't explain why the first one works and the second doesn't.
CodePudding user response:
The package benbjohnson/clock
provides mock time facilities. In particular their documentation states:
Timers and Tickers are also controlled by this same mock clock. They will only execute when the clock is moved forward
So when you call mockClock.Add
, it will sequentially execute the timers/tickers. The library also adds sequential 1 millisecond sleeps to artificially yield to other goroutines.
When the timer/ticker is declared outside the goroutine, i.e. before calling mockClock.Add
, by the time mockClock.Add
gets called the mock time does have something to execute. The library's internal sleeps are enough for the child goroutine to receive on the ticker and print "done", before the program exits.
When the ticker is declared inside the goroutine, by the time mockClock.Add
gets called, the mock time has no tickers to execute and Add
essentially does nothing. The internal sleeps do give a chance to the child goroutine to run, but receiving on the ticker now just blocks; main then resumes and exits.
You can also have a look at the ticker example that you can see in the repository's README:
mock := clock.NewMock()
count := 0
// Kick off a timer to increment every 1 mock second.
go func() {
ticker := mock.Ticker(1 * time.Second)
for {
<-ticker.C
count
}
}()
runtime.Gosched()
// Move the clock forward 10 seconds.
mock.Add(10 * time.Second)
// This prints 10.
fmt.Println(count)
This uses runtime.Gosched()
to yield to the child goroutine before calling mock.Add
. The sequence of this program is basically:
clock.NewMock()
count := 0
- spawn child goroutine
runtime.Gosched()
, yielding to the child goroutineticker := mock.Ticker(1 * time.Second)
- block on
<-ticker.C
(the mock clock hasn't moved forward yet) - resume main
mock.Add
, which moves the clock forward and yields to the child goroutine againfor
loop with<-ticker.C
- print 10
- exit
By the same logic, if you add a runtime.Gosched()
to your second snippet, it will work as expected, just like the repository's example. Playground: https://go.dev/play/p/ZitEdtx9GdL
However, do not rely on runtime.Gosched()
in production code, possibly not even in test code, unless you're very sure about what you are doing.
Finally, please remember that time.Sleep(1)
sleeps for one nanosecond.