Skip to content

Commit

Permalink
make eventually helper and timecop time travel to improve chances for…
Browse files Browse the repository at this point in the history
… background goroutines
  • Loading branch information
adamluzsi committed Aug 5, 2024
1 parent a13be71 commit 87798da
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 8 deletions.
2 changes: 2 additions & 0 deletions assert/Eventually.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package assert

import (
"runtime"
"testing"
"time"

Expand Down Expand Up @@ -50,6 +51,7 @@ func (r Retry) Assert(tb testing.TB, blk func(t It)) {
isFailed := tb.Failed()
r.Strategy.While(func() bool {
tb.Helper()
runtime.Gosched()
lastRecorder = &doubles.RecorderTB{TB: tb}
ro := sandbox.Run(func() {
tb.Helper()
Expand Down
6 changes: 3 additions & 3 deletions assert/Waiter.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package assert

import (
"runtime"
"time"

"go.llib.dev/testcase/internal/rth"
)

// Waiter is a component that waits for a time, event, or opportunity.
Expand All @@ -19,8 +20,7 @@ type Waiter struct {
func (w Waiter) Wait() {
finishTime := time.Now().Add(w.WaitDuration)
for time.Now().Before(finishTime) {
runtime.Gosched()
time.Sleep(time.Nanosecond)
rth.Schedule(w.WaitDuration)
}
}

Expand Down
20 changes: 20 additions & 0 deletions clock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Will this replace dependency injection for time-related configurations?](#will-this-replace-dependency-injection-for-time-related-configurations)
- [Why not just use a global variable with `time.Now`?](#why-not-just-use-a-global-variable-with-timenow)
- [How does the clock package help with testing time-dependent goroutine synchronization?](#how-does-the-clock-package-help-with-testing-time-dependent-goroutine-synchronization)
- [Does the `clock` package have a monotonic view of time?](#does-the-clock-package-have-a-monotonic-view-of-time)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

Expand Down Expand Up @@ -167,3 +168,22 @@ func TestXXX(t *testing.T) {
}
```

### Does the `clock` package have a monotonic view of time?

Monotonic time means:
- Time is always increasing steadily, without jumping backwards.
- Every thread sees time moving forward at the same rate.
- All goroutines share a single, unified notion of time.

The `clock` package inherits its time view from the standard Go time API.
The Go standard library provides APIs to get the current time,
which includes both the "wall clock" and the "monotonic clock".
When the system clock synchronizes and changes, the wall clock time can move backward,
but for measurements, Go uses the monotonic time value for precise results.

The `clock` package also allows traveling backward in time.
In this sense, `clock` isn't strictly monotonic because you can set it to a specific point in time.
After time travel, time is monotonic again until you again make a travel back in time.

It’s open to interpretation whether the `clock` remains monotonic when time is frozen with `timecop`.

8 changes: 3 additions & 5 deletions clock/timecop/timecop.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package timecop

import (
"runtime"
"testing"
"time"

"go.llib.dev/testcase/clock/internal"
"go.llib.dev/testcase/internal/rth"
)

// Travel will initiate a time travel.
Expand All @@ -22,10 +22,8 @@ func Travel[D time.Duration | time.Time](tb testing.TB, d D, tos ...TravelOption
case time.Time:
travelByTime(tb, d, opt)
}
for i, n := 0, runtime.NumGoroutine(); i < n; i++ { // since goroutines don't have guarantee when they will be scheduled
runtime.Gosched() // we explicitly mark that we are okay with other goroutines to be scheduled
time.Sleep(time.Nanosecond) // and we also okay to be low piority and blocked for the sake of other goroutines.
}
const travelWaitTimeout = 3 * time.Second
rth.Schedule(travelWaitTimeout)
}

const BlazingFast = 100
Expand Down
24 changes: 24 additions & 0 deletions internal/rth/rth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package rth

import (
"runtime"
"time"
)

func Schedule(maxWait time.Duration) {
const WaitUnit = time.Nanosecond
var (
goroutNum = runtime.NumGoroutine()
startedAt = time.Now()
)
for i := 0; i < goroutNum; i++ { // since goroutines don't have guarantee when they will be scheduled
runtime.Gosched() // we explicitly mark that we are okay with other goroutines to be scheduled
elapsed := time.Since(startedAt)
if maxWait <= elapsed { // if max wait time is reached
return
}
if elapsed < maxWait { // if we withint the max wait time,
time.Sleep(WaitUnit) // then we could just yield CPU too with sleep
}
}
}
42 changes: 42 additions & 0 deletions internal/rth/rth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package rth_test

import (
"context"
"strconv"
"testing"
"time"

"go.llib.dev/testcase/assert"
"go.llib.dev/testcase/internal/rth"
"go.llib.dev/testcase/random"
)

func TestSchedule(t *testing.T) {
rnd := random.New(random.CryptoSeed{})
done := make(chan struct{})
defer close(done)

for i := 0; i < 1000; i++ {
// fire goroutines
go func() {
for {
select {
case <-done: // stop when the program is done
return
default: // do something that requires CPU time
strconv.Itoa(rnd.Int())
}
}
}()
}

var adjusted = func(d time.Duration, m float64) time.Duration {
return time.Duration(float64(d) * m)
}

for _, dur := range []time.Duration{time.Second, time.Millisecond} {
assert.Within(t, adjusted(dur, 1.2), func(ctx context.Context) {
rth.Schedule(dur)
})
}
}

0 comments on commit 87798da

Please sign in to comment.