chore: spin clock library out to coder/quartz repo (#13777)

Code that was in `/clock` has been moved to github.com/coder/quartz.  This PR refactors our use of the clock library to point to the external Quartz repo.
This commit is contained in:
Spike Curtis
2024-07-03 15:02:54 +04:00
committed by GitHub
parent a110d18275
commit e5268e4551
25 changed files with 61 additions and 1986 deletions
+3 -3
View File
@@ -10,9 +10,9 @@ import (
"golang.org/x/xerrors"
"cdr.dev/slog"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/quartz"
)
// PostWorkspaceAgentAppHealth updates the workspace app health.
@@ -23,7 +23,7 @@ type WorkspaceAppHealthReporter func(ctx context.Context)
// NewWorkspaceAppHealthReporter creates a WorkspaceAppHealthReporter that reports app health to coderd.
func NewWorkspaceAppHealthReporter(logger slog.Logger, apps []codersdk.WorkspaceApp, postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth) WorkspaceAppHealthReporter {
return NewAppHealthReporterWithClock(logger, apps, postWorkspaceAgentAppHealth, clock.NewReal())
return NewAppHealthReporterWithClock(logger, apps, postWorkspaceAgentAppHealth, quartz.NewReal())
}
// NewAppHealthReporterWithClock is only called directly by test code. Product code should call
@@ -32,7 +32,7 @@ func NewAppHealthReporterWithClock(
logger slog.Logger,
apps []codersdk.WorkspaceApp,
postWorkspaceAgentAppHealth PostWorkspaceAgentAppHealth,
clk clock.Clock,
clk quartz.Clock,
) WorkspaceAppHealthReporter {
logger = logger.Named("apphealth")
+5 -5
View File
@@ -17,11 +17,11 @@ import (
"github.com/coder/coder/v2/agent"
"github.com/coder/coder/v2/agent/agenttest"
"github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/codersdk/agentsdk"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestAppHealth_Healthy(t *testing.T) {
@@ -69,7 +69,7 @@ func TestAppHealth_Healthy(t *testing.T) {
httpapi.Write(r.Context(), w, http.StatusOK, nil)
}),
}
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
defer healthcheckTrap.Close()
reportTrap := mClock.Trap().TickerFunc("report")
@@ -137,7 +137,7 @@ func TestAppHealth_500(t *testing.T) {
}),
}
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
healthcheckTrap := mClock.Trap().TickerFunc("healthcheck")
defer healthcheckTrap.Close()
reportTrap := mClock.Trap().TickerFunc("report")
@@ -187,7 +187,7 @@ func TestAppHealth_Timeout(t *testing.T) {
<-r.Context().Done()
}),
}
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
start := mClock.Now()
// for this test, it's easier to think in the number of milliseconds elapsed
@@ -235,7 +235,7 @@ func setupAppReporter(
ctx context.Context, t *testing.T,
apps []codersdk.WorkspaceApp,
handlers []http.Handler,
clk clock.Clock,
clk quartz.Clock,
) (*agenttest.FakeAgentAPI, func()) {
closers := []func(){}
for _, app := range apps {
-635
View File
@@ -1,635 +0,0 @@
# Quartz
A Go time testing library for writing deterministic unit tests
_Note: Quartz is the name I'm targeting for the standalone open source project when we spin this
out._
Our high level goal is to write unit tests that
1. execute quickly
2. don't flake
3. are straightforward to write and understand
For tests to execute quickly without flakes, we want to focus on _determinism_: the test should run
the same each time, and it should be easy to force the system into a known state (no races) before
executing test assertions. `time.Sleep`, `runtime.Gosched()`, and
polling/[Eventually](https://pkg.go.dev/github.com/stretchr/testify/assert#Eventually) are all
symptoms of an inability to do this easily.
## Usage
### `Clock` interface
In your application code, maintain a reference to a `quartz.Clock` instance to start timers and
tickers, instead of the bare `time` standard library.
```go
import "github.com/coder/quartz"
type Component struct {
...
// for testing
clock quartz.Clock
}
```
Whenever you would call into `time` to start a timer or ticker, call `Component`'s `clock` instead.
In production, set this clock to `quartz.NewReal()` to create a clock that just transparently passes
through to the standard `time` library.
### Mocking
In your tests, you can use a `*Mock` to control the tickers and timers your code under test gets.
```go
import (
"testing"
"github.com/coder/quartz"
)
func TestComponent(t *testing.T) {
mClock := quartz.NewMock(t)
comp := &Component{
...
clock: mClock,
}
}
```
The `*Mock` clock starts at Jan 1, 2024, 00:00 UTC by default, but you can set any start time you'd like prior to your test.
```go
mClock := quartz.NewMock(t)
mClock.Set(time.Date(2021, 6, 18, 12, 0, 0, 0, time.UTC)) // June 18, 2021 @ 12pm UTC
```
#### Advancing the clock
Once you begin setting timers or tickers, you cannot change the time backward, only advance it
forward. You may continue to use `Set()`, but it is often easier and clearer to use `Advance()`.
For example, with a timer:
```go
fired := false
tmr := mClock.Afterfunc(time.Second, func() {
fired = true
})
mClock.Advance(time.Second)
```
When you call `Advance()` it immediately moves the clock forward the given amount, and triggers any
tickers or timers that are scheduled to happen at that time. Any triggered events happen on separate
goroutines, so _do not_ immediately assert the results:
```go
fired := false
tmr := mClock.Afterfunc(time.Second, func() {
fired = true
})
mClock.Advance(time.Second)
// RACE CONDITION, DO NOT DO THIS!
if !fired {
t.Fatal("didn't fire")
}
```
`Advance()` (and `Set()` for that matter) return an `AdvanceWaiter` object you can use to wait for
all triggered events to complete.
```go
fired := false
// set a test timeout so we don't wait the default `go test` timeout for a failure
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
tmr := mClock.Afterfunc(time.Second, func() {
fired = true
})
w := mClock.Advance(time.Second)
err := w.Wait(ctx)
if err != nil {
t.Fatal("AfterFunc f never completed")
}
if !fired {
t.Fatal("didn't fire")
}
```
The construction of waiting for the triggered events and failing the test if they don't complete is
very common, so there is a shorthand:
```go
w := mClock.Advance(time.Second)
err := w.Wait(ctx)
if err != nil {
t.Fatal("AfterFunc f never completed")
}
```
is equivalent to:
```go
w := mClock.Advance(time.Second)
w.MustWait(ctx)
```
or even more briefly:
```go
mClock.Advance(time.Second).MustWait(ctx)
```
### Advance only to the next event
One important restriction on advancing the clock is that you may only advance forward to the next
timer or ticker event and no further. The following will result in a test failure:
```go
func TestAdvanceTooFar(t *testing.T) {
ctx, cancel := context.WithTimeout(10*time.Second)
defer cancel()
mClock := quartz.NewMock(t)
var firedAt time.Time
mClock.AfterFunc(time.Second, func() {
firedAt := mClock.Now()
})
mClock.Advance(2*time.Second).MustWait(ctx)
}
```
This is a deliberate design decision to allow `Advance()` to immediately and synchronously move the
clock forward (even without calling `Wait()` on returned waiter). This helps meet Quartz's design
goals of writing deterministic and easy to understand unit tests. It also allows the clock to be
advanced, deterministically _during_ the execution of a tick or timer function, as explained in the
next sections on Traps.
Advancing multiple events can be accomplished via looping. E.g. if you have a 1-second ticker
```go
for i := 0; i < 10; i++ {
mClock.Advance(time.Second).MustWait(ctx)
}
```
will advance 10 ticks.
If you don't know or don't want to compute the time to the next event, you can use `AdvanceNext()`.
```go
d, w := mClock.AdvanceNext()
w.MustWait(ctx)
// d contains the duration we advanced
```
`d, ok := Peek()` returns the duration until the next event, if any (`ok` is `true`). You can use
this to advance a specific time, regardless of the tickers and timer events:
```go
desired := time.Minute // time to advance
for desired > 0 {
p, ok := mClock.Peek()
if !ok || p > desired {
mClock.Advance(desired).MustWait(ctx)
break
}
mClock.Advance(p).MustWait(ctx)
desired -= p
}
```
### Traps
A trap allows you to match specific calls into the library while mocking, block their return,
inspect their arguments, then release them to allow them to return. They help you write
deterministic unit tests even when the code under test executes asynchronously from the test.
You set your traps prior to executing code under test, and then wait for them to be triggered.
```go
func TestTrap(t *testing.T) {
ctx, cancel := context.WithTimeout(10*time.Second)
defer cancel()
mClock := quartz.NewMock(t)
trap := mClock.Trap().AfterFunc()
defer trap.Close() // stop trapping AfterFunc calls
count := 0
go mClock.AfterFunc(time.Hour, func(){
count++
})
call := trap.MustWait(ctx)
call.Release()
if call.Duration != time.Hour {
t.Fatal("wrong duration")
}
// Now that the async call to AfterFunc has occurred, we can advance the clock to trigger it
mClock.Advance(call.Duration).MustWait(ctx)
if count != 1 {
t.Fatal("wrong count")
}
}
```
In this test, the trap serves 2 purposes. Firstly, it allows us to capture and assert the duration
passed to the `AfterFunc` call. Secondly, it prevents a race between setting the timer and advancing
it. Since these things happen on different goroutines, if `Advance()` completes before
`AfterFunc()` is called, then the timer never pops in this test.
Any untrapped calls immediately complete using the current time, and calling `Close()` on a trap
causes the mock clock to stop trapping those calls.
You may also `Advance()` the clock between trapping a call and releasing it. The call uses the
current (mocked) time at the moment it is released.
```go
func TestTrap2(t *testing.T) {
ctx, cancel := context.WithTimeout(10*time.Second)
defer cancel()
mClock := quartz.NewMock(t)
trap := mClock.Trap().Now()
defer trap.Close() // stop trapping AfterFunc calls
var logs []string
done := make(chan struct{})
go func(clk quartz.Clock){
defer close(done)
start := clk.Now()
phase1()
p1end := clk.Now()
logs = append(fmt.Sprintf("Phase 1 took %s", p1end.Sub(start).String()))
phase2()
p2end := clk.Now()
logs = append(fmt.Sprintf("Phase 2 took %s", p2end.Sub(p1end).String()))
}(mClock)
// start
trap.MustWait(ctx).Release()
// phase 1
call := trap.MustWait(ctx)
mClock.Advance(3*time.Second).MustWait(ctx)
call.Release()
// phase 2
call = trap.MustWait(ctx)
mClock.Advance(5*time.Second).MustWait(ctx)
call.Release()
<-done
// Now logs contains []string{"Phase 1 took 3s", "Phase 2 took 5s"}
}
```
### Tags
When multiple goroutines in the code under test call into the Clock, you can use `tags` to
distinguish them in your traps.
```go
trap := mClock.Trap.Now("foo") // traps any calls that contain "foo"
defer trap.Close()
foo := make(chan time.Time)
go func(){
foo <- mClock.Now("foo", "bar")
}()
baz := make(chan time.Time)
go func(){
baz <- mClock.Now("baz")
}()
call := trap.MustWait(ctx)
mClock.Advance(time.Second).MustWait(ctx)
call.Release()
// call.Tags contains []string{"foo", "bar"}
gotFoo := <-foo // 1s after start
gotBaz := <-baz // ?? never trapped, so races with Advance()
```
Tags appear as an optional suffix on all `Clock` methods (type `...string`) and are ignored entirely
by the real clock. They also appear on all methods on returned timers and tickers.
## Recommended Patterns
### Options
We use the Option pattern to inject the mock clock for testing, keeping the call signature in
production clean. The option pattern is compatible with other optional fields as well.
```go
type Option func(*Thing)
// WithTestClock is used in tests to inject a mock Clock
func WithTestClock(clk quartz.Clock) Option {
return func(t *Thing) {
t.clock = clk
}
}
func NewThing(<required args>, opts ...Option) *Thing {
t := &Thing{
...
clock: quartz.NewReal()
}
for _, o := range opts {
o(t)
}
return t
}
```
In tests, this becomes
```go
func TestThing(t *testing.T) {
mClock := quartz.NewMock(t)
thing := NewThing(<required args>, WithTestClock(mClock))
...
}
```
### Tagging convention
Tag your `Clock` method calls as:
```go
func (c *Component) Method() {
now := c.clock.Now("Component", "Method")
}
```
or
```go
func (c *Component) Method() {
start := c.clock.Now("Component", "Method", "start")
...
end := c.clock.Now("Component", "Method", "end")
}
```
This makes it much less likely that code changes that introduce new components or methods will spoil
existing unit tests.
## Why another time testing library?
Writing good unit tests for components and functions that use the `time` package is difficult, even
though several open source libraries exist. In building Quartz, we took some inspiration from
- [github.com/benbjohnson/clock](https://github.com/benbjohnson/clock)
- Tailscale's [tstest.Clock](https://github.com/coder/tailscale/blob/main/tstest/clock.go)
- [github.com/aspenmesh/tock](https://github.com/aspenmesh/tock)
Quartz shares the high level design of a `Clock` interface that closely resembles the functions in
the `time` standard library, and a "real" clock passes thru to the standard library in production,
while a mock clock gives precise control in testing.
As mentioned in our introduction, our high level goal is to write unit tests that
1. execute quickly
2. don't flake
3. are straightforward to write and understand
For several reasons, this is a tall order when it comes to code that depends on time, and we found
the existing libraries insufficient for our goals.
### Preventing test flakes
The following example comes from the README from benbjohnson/clock:
```go
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)
```
The first race condition is fairly obvious: moving the clock forward 10 seconds may generate 10
ticks on the `ticker.C` channel, but there is no guarantee that `count++` executes before
`fmt.Println(count)`.
The second race condition is more subtle, but `runtime.Gosched()` is the tell. Since the ticker
is started on a separate goroutine, there is no guarantee that `mock.Ticker()` executes before
`mock.Add()`. `runtime.Gosched()` is an attempt to get this to happen, but it makes no hard
promises. On a busy system, especially when running tests in parallel, this can flake, advance the
time 10 seconds first, then start the ticker and never generate a tick.
Let's talk about how Quartz tackles these problems.
In our experience, an extremely common use case is creating a ticker then doing a 2-arm `select`
with ticks in one and context expiring in another, i.e.
```go
t := time.NewTicker(duration)
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
err := do()
if err != nil {
return err
}
}
}
```
In Quartz, we refactor this to be more compact and testing friendly:
```go
t := clock.TickerFunc(ctx, duration, do)
return t.Wait()
```
This affords the mock `Clock` the ability to explicitly know when processing of a tick is finished
because it's wrapped in the function passed to `TickerFunc` (`do()` in this example).
In Quartz, when you advance the clock, you are returned an object you can `Wait()` on to ensure all
ticks and timers triggered are finished. This solves the first race condition in the example.
(As an aside, we still support a traditional standard library-style `Ticker`. You may find it useful
if you want to keep your code as close as possible to the standard library, or if you need to use
the channel in a larger `select` block. In that case, you'll have to find some other mechanism to
sync tick processing to your test code.)
To prevent race conditions related to the starting of the ticker, Quartz allows you to set "traps"
for calls that access the clock.
```go
func TestTicker(t *testing.T) {
mClock := quartz.NewMock(t)
trap := mClock.Trap().TickerFunc()
defer trap.Close() // stop trapping at end
go runMyTicker(mClock) // async calls TickerFunc()
call := trap.Wait(context.Background()) // waits for a call and blocks its return
call.Release() // allow the TickerFunc() call to return
// optionally check the duration using call.Duration
// Move the clock forward 1 tick
mClock.Advance(time.Second).MustWait(context.Background())
// assert results of the tick
}
```
Trapping and then releasing the call to `TickerFunc()` ensures the ticker is started at a
deterministic time, so our calls to `Advance()` will have a predictable effect.
Take a look at `TestExampleTickerFunc` in `example_test.go` for a complete worked example.
### Complex time dependence
Another difficult issue to handle when unit testing is when some code under test makes multiple
calls that depend on the time, and you want to simulate some time passing between them.
A very basic example is measuring how long something took:
```go
var measurement time.Duration
go func(clock quartz.Clock) {
start := clock.Now()
doSomething()
measurement = clock.Since(start)
}(mClock)
// how to get measurement to be, say, 5 seconds?
```
The two calls into the clock happen asynchronously, so we need to be able to advance the clock after
the first call to `Now()` but before the call to `Since()`. Doing this with the libraries we
mentioned above means that you have to be able to mock out or otherwise block the completion of
`doSomething()`.
But, with the trap functionality we mentioned in the previous section, you can deterministically
control the time each call sees.
```go
trap := mClock.Trap().Since()
var measurement time.Duration
go func(clock quartz.Clock) {
start := clock.Now()
doSomething()
measurement = clock.Since(start)
}(mClock)
c := trap.Wait(ctx)
mClock.Advance(5*time.Second)
c.Release()
```
We wait until we trap the `clock.Since()` call, which implies that `clock.Now()` has completed, then
advance the mock clock 5 seconds. Finally, we release the `clock.Since()` call. Any changes to the
clock that happen _before_ we release the call will be included in the time used for the
`clock.Since()` call.
As a more involved example, consider an inactivity timeout: we want something to happen if there is
no activity recorded for some period, say 10 minutes in the following example:
```go
type InactivityTimer struct {
mu sync.Mutex
activity time.Time
clock quartz.Clock
}
func (i *InactivityTimer) Start() {
i.mu.Lock()
defer i.mu.Unlock()
next := i.clock.Until(i.activity.Add(10*time.Minute))
t := i.clock.AfterFunc(next, func() {
i.mu.Lock()
defer i.mu.Unlock()
next := i.clock.Until(i.activity.Add(10*time.Minute))
if next == 0 {
i.timeoutLocked()
return
}
t.Reset(next)
})
}
```
The actual contents of `timeoutLocked()` doesn't matter for this example, and assume there are other
functions that record the latest `activity`.
We found that some time testing libraries hold a lock on the mock clock while calling the function
passed to `AfterFunc`, resulting in a deadlock if you made clock calls from within.
Others allow this sort of thing, but don't have the flexibility to test edge cases. There is a
subtle bug in our `Start()` function. The timer may pop a little late, and/or some measurable real
time may elapse before `Until()` gets called inside the `AfterFunc`. If there hasn't been activity,
`next` might be negative.
To test this in Quartz, we'll use a trap. We only want to trap the inner `Until()` call, not the
initial one, so to make testing easier we can "tag" the call we want. Like this:
```go
func (i *InactivityTimer) Start() {
i.mu.Lock()
defer i.mu.Unlock()
next := i.clock.Until(i.activity.Add(10*time.Minute))
t := i.clock.AfterFunc(next, func() {
i.mu.Lock()
defer i.mu.Unlock()
next := i.clock.Until(i.activity.Add(10*time.Minute), "inner")
if next == 0 {
i.timeoutLocked()
return
}
t.Reset(next)
})
}
```
All Quartz `Clock` functions, and functions on returned timers and tickers support zero or more
string tags that allow traps to match on them.
```go
func TestInactivityTimer_Late(t *testing.T) {
// set a timeout on the test itself, so that if Wait functions get blocked, we don't have to
// wait for the default test timeout of 10 minutes.
ctx, cancel := context.WithTimeout(10*time.Second)
defer cancel()
mClock := quartz.NewMock(t)
trap := mClock.Trap.Until("inner")
defer trap.Close()
it := &InactivityTimer{
activity: mClock.Now(),
clock: mClock,
}
it.Start()
// Trigger the AfterFunc
w := mClock.Advance(10*time.Minute)
c := trap.Wait(ctx)
// Advance the clock a few ms to simulate a busy system
mClock.Advance(3*time.Millisecond)
c.Release() // Until() returns
w.MustWait(ctx) // Wait for the AfterFunc to wrap up
// Assert that the timeoutLocked() function was called
}
```
This test case will fail with our bugged implementation, since the triggered AfterFunc won't call
`timeoutLocked()` and instead will reset the timer with a negative number. The fix is easy, use
`next <= 0` as the comparison.
-43
View File
@@ -1,43 +0,0 @@
// Package clock is a library for testing time related code. It exports an interface Clock that
// mimics the standard library time package functions. In production, an implementation that calls
// thru to the standard library is used. In testing, a Mock clock is used to precisely control and
// intercept time functions.
package clock
import (
"context"
"time"
)
type Clock interface {
// NewTicker returns a new Ticker containing a channel that will send the current time on the
// channel after each tick. The period of the ticks is specified by the duration argument. The
// ticker will adjust the time interval or drop ticks to make up for slow receivers. The
// duration d must be greater than zero; if not, NewTicker will panic. Stop the ticker to
// release associated resources.
NewTicker(d time.Duration, tags ...string) *Ticker
// TickerFunc is a convenience function that calls f on the interval d until either the given
// context expires or f returns an error. Callers may call Wait() on the returned Waiter to
// wait until this happens and obtain the error. The duration d must be greater than zero; if
// not, TickerFunc will panic.
TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter
// NewTimer creates a new Timer that will send the current time on its channel after at least
// duration d.
NewTimer(d time.Duration, tags ...string) *Timer
// AfterFunc waits for the duration to elapse and then calls f in its own goroutine. It returns
// a Timer that can be used to cancel the call using its Stop method. The returned Timer's C
// field is not used and will be nil.
AfterFunc(d time.Duration, f func(), tags ...string) *Timer
// Now returns the current local time.
Now(tags ...string) time.Time
// Since returns the time elapsed since t. It is shorthand for Clock.Now().Sub(t).
Since(t time.Time, tags ...string) time.Duration
// Until returns the duration until t. It is shorthand for t.Sub(Clock.Now()).
Until(t time.Time, tags ...string) time.Duration
}
// Waiter can be waited on for an error.
type Waiter interface {
Wait(tags ...string) error
}
-149
View File
@@ -1,149 +0,0 @@
package clock_test
import (
"context"
"sync"
"testing"
"time"
"github.com/coder/coder/v2/clock"
)
type exampleTickCounter struct {
ctx context.Context
mu sync.Mutex
ticks int
clock clock.Clock
}
func (c *exampleTickCounter) Ticks() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.ticks
}
func (c *exampleTickCounter) count() {
_ = c.clock.TickerFunc(c.ctx, time.Hour, func() error {
c.mu.Lock()
defer c.mu.Unlock()
c.ticks++
return nil
}, "mytag")
}
func newExampleTickCounter(ctx context.Context, clk clock.Clock) *exampleTickCounter {
tc := &exampleTickCounter{ctx: ctx, clock: clk}
go tc.count()
return tc
}
// TestExampleTickerFunc demonstrates how to test the use of TickerFunc.
func TestExampleTickerFunc(t *testing.T) {
t.Parallel()
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mClock := clock.NewMock(t)
// Because the ticker is started on a goroutine, we can't immediately start
// advancing the clock, or we will race with the start of the ticker. If we
// win that race, the clock gets advanced _before_ the ticker starts, and
// our ticker will not get a tick.
//
// To handle this, we set a trap for the call to TickerFunc(), so that we
// can assert it has been called before advancing the clock.
trap := mClock.Trap().TickerFunc("mytag")
defer trap.Close()
tc := newExampleTickCounter(ctx, mClock)
// Here, we wait for our trap to be triggered.
call, err := trap.Wait(ctx)
if err != nil {
t.Fatal("ticker never started")
}
// it's good practice to release calls before any possible t.Fatal() calls
// so that we don't leave dangling goroutines waiting for the call to be
// released.
call.Release()
if call.Duration != time.Hour {
t.Fatal("unexpected duration")
}
if tks := tc.Ticks(); tks != 0 {
t.Fatalf("expected 0 got %d ticks", tks)
}
// Now that we know the ticker is started, we can advance the time.
mClock.Advance(time.Hour).MustWait(ctx)
if tks := tc.Ticks(); tks != 1 {
t.Fatalf("expected 1 got %d ticks", tks)
}
}
type exampleLatencyMeasurer struct {
mu sync.Mutex
lastLatency time.Duration
}
func newExampleLatencyMeasurer(ctx context.Context, clk clock.Clock) *exampleLatencyMeasurer {
m := &exampleLatencyMeasurer{}
clk.TickerFunc(ctx, 10*time.Second, func() error {
start := clk.Now()
// m.doSomething()
latency := clk.Since(start)
m.mu.Lock()
defer m.mu.Unlock()
m.lastLatency = latency
return nil
})
return m
}
func (m *exampleLatencyMeasurer) LastLatency() time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
return m.lastLatency
}
func TestExampleLatencyMeasurer(t *testing.T) {
t.Parallel()
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mClock := clock.NewMock(t)
trap := mClock.Trap().Since()
defer trap.Close()
lm := newExampleLatencyMeasurer(ctx, mClock)
w := mClock.Advance(10 * time.Second) // triggers first tick
c := trap.MustWait(ctx) // call to Since()
mClock.Advance(33 * time.Millisecond)
c.Release()
w.MustWait(ctx)
if l := lm.LastLatency(); l != 33*time.Millisecond {
t.Fatalf("expected 33ms got %s", l.String())
}
// Next tick is in 10s - 33ms, but if we don't want to calculate, we can use:
d, w2 := mClock.AdvanceNext()
c = trap.MustWait(ctx)
mClock.Advance(17 * time.Millisecond)
c.Release()
w2.MustWait(ctx)
expectedD := 10*time.Second - 33*time.Millisecond
if d != expectedD {
t.Fatalf("expected %s got %s", expectedD.String(), d.String())
}
if l := lm.LastLatency(); l != 17*time.Millisecond {
t.Fatalf("expected 17ms got %s", l.String())
}
}
-647
View File
@@ -1,647 +0,0 @@
package clock
import (
"context"
"fmt"
"slices"
"sync"
"testing"
"time"
"golang.org/x/xerrors"
)
// Mock is the testing implementation of Clock. It tracks a time that monotonically increases
// during a test, triggering any timers or tickers automatically.
type Mock struct {
tb testing.TB
mu sync.Mutex
// cur is the current time
cur time.Time
all []event
nextTime time.Time
nextEvents []event
traps []*Trap
}
type event interface {
next() time.Time
fire(t time.Time)
}
func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter {
if d <= 0 {
panic("TickerFunc called with negative or zero duration")
}
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionTickerFunc, tags, withDuration(d))
m.matchCallLocked(c)
defer close(c.complete)
t := &mockTickerFunc{
ctx: ctx,
d: d,
f: f,
nxt: m.cur.Add(d),
mock: m,
cond: sync.NewCond(&m.mu),
}
m.all = append(m.all, t)
m.recomputeNextLocked()
go t.waitForCtx()
return t
}
func (m *Mock) NewTicker(d time.Duration, tags ...string) *Ticker {
if d <= 0 {
panic("NewTicker called with negative or zero duration")
}
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionNewTicker, tags, withDuration(d))
m.matchCallLocked(c)
defer close(c.complete)
// 1 element buffer follows standard library implementation
ticks := make(chan time.Time, 1)
t := &Ticker{
C: ticks,
c: ticks,
d: d,
nxt: m.cur.Add(d),
mock: m,
}
m.addEventLocked(t)
return t
}
func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer {
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionNewTimer, tags, withDuration(d))
defer close(c.complete)
m.matchCallLocked(c)
ch := make(chan time.Time, 1)
t := &Timer{
C: ch,
c: ch,
nxt: m.cur.Add(d),
mock: m,
}
if d <= 0 {
// zero or negative duration timer means we should immediately fire
// it, rather than add it.
go t.fire(t.mock.cur)
return t
}
m.addEventLocked(t)
return t
}
func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer {
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionAfterFunc, tags, withDuration(d))
defer close(c.complete)
m.matchCallLocked(c)
t := &Timer{
nxt: m.cur.Add(d),
fn: f,
mock: m,
}
if d <= 0 {
// zero or negative duration timer means we should immediately fire
// it, rather than add it.
go t.fire(t.mock.cur)
return t
}
m.addEventLocked(t)
return t
}
func (m *Mock) Now(tags ...string) time.Time {
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionNow, tags)
defer close(c.complete)
m.matchCallLocked(c)
return m.cur
}
func (m *Mock) Since(t time.Time, tags ...string) time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionSince, tags, withTime(t))
defer close(c.complete)
m.matchCallLocked(c)
return m.cur.Sub(t)
}
func (m *Mock) Until(t time.Time, tags ...string) time.Duration {
m.mu.Lock()
defer m.mu.Unlock()
c := newCall(clockFunctionUntil, tags, withTime(t))
defer close(c.complete)
m.matchCallLocked(c)
return t.Sub(m.cur)
}
func (m *Mock) addEventLocked(e event) {
m.all = append(m.all, e)
m.recomputeNextLocked()
}
func (m *Mock) recomputeNextLocked() {
var best time.Time
var events []event
for _, e := range m.all {
if best.IsZero() || e.next().Before(best) {
best = e.next()
events = []event{e}
continue
}
if e.next().Equal(best) {
events = append(events, e)
continue
}
}
m.nextTime = best
m.nextEvents = events
}
func (m *Mock) removeTimer(t *Timer) {
m.mu.Lock()
defer m.mu.Unlock()
m.removeTimerLocked(t)
}
func (m *Mock) removeTimerLocked(t *Timer) {
t.stopped = true
m.removeEventLocked(t)
}
func (m *Mock) removeEventLocked(e event) {
defer m.recomputeNextLocked()
for i := range m.all {
if m.all[i] == e {
m.all = append(m.all[:i], m.all[i+1:]...)
return
}
}
}
func (m *Mock) matchCallLocked(c *Call) {
var traps []*Trap
for _, t := range m.traps {
if t.matches(c) {
traps = append(traps, t)
}
}
if len(traps) == 0 {
return
}
c.releases.Add(len(traps))
m.mu.Unlock()
for _, t := range traps {
go t.catch(c)
}
c.releases.Wait()
m.mu.Lock()
}
// AdvanceWaiter is returned from Advance and Set calls and allows you to wait for ticks and timers
// to complete. In the case of functions passed to AfterFunc or TickerFunc, it waits for the
// functions to return. For other ticks & timers, it just waits for the tick to be delivered to
// the channel.
//
// If multiple timers or tickers trigger simultaneously, they are all run on separate
// go routines.
type AdvanceWaiter struct {
tb testing.TB
ch chan struct{}
}
// Wait for all timers and ticks to complete, or until context expires.
func (w AdvanceWaiter) Wait(ctx context.Context) error {
select {
case <-w.ch:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// MustWait waits for all timers and ticks to complete, and fails the test immediately if the
// context completes first. MustWait must be called from the goroutine running the test or
// benchmark, similar to `t.FailNow()`.
func (w AdvanceWaiter) MustWait(ctx context.Context) {
w.tb.Helper()
select {
case <-w.ch:
return
case <-ctx.Done():
w.tb.Fatalf("context expired while waiting for clock to advance: %s", ctx.Err())
}
}
// Done returns a channel that is closed when all timers and ticks complete.
func (w AdvanceWaiter) Done() <-chan struct{} {
return w.ch
}
// Advance moves the clock forward by d, triggering any timers or tickers. The returned value can
// be used to wait for all timers and ticks to complete. Advance sets the clock forward before
// returning, and can only advance up to the next timer or tick event. It will fail the test if you
// attempt to advance beyond.
//
// If you need to advance exactly to the next event, and don't know or don't wish to calculate it,
// consider AdvanceNext().
func (m *Mock) Advance(d time.Duration) AdvanceWaiter {
m.tb.Helper()
w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})}
m.mu.Lock()
fin := m.cur.Add(d)
// nextTime.IsZero implies no events scheduled.
if m.nextTime.IsZero() || fin.Before(m.nextTime) {
m.cur = fin
m.mu.Unlock()
close(w.ch)
return w
}
if fin.After(m.nextTime) {
m.tb.Errorf(fmt.Sprintf("cannot advance %s which is beyond next timer/ticker event in %s",
d.String(), m.nextTime.Sub(m.cur)))
m.mu.Unlock()
close(w.ch)
return w
}
m.cur = m.nextTime
go m.advanceLocked(w)
return w
}
func (m *Mock) advanceLocked(w AdvanceWaiter) {
defer close(w.ch)
wg := sync.WaitGroup{}
for i := range m.nextEvents {
e := m.nextEvents[i]
t := m.cur
wg.Add(1)
go func() {
e.fire(t)
wg.Done()
}()
}
// release the lock and let the events resolve. This allows them to call back into the
// Mock to query the time or set new timers. Each event should remove or reschedule
// itself from nextEvents.
m.mu.Unlock()
wg.Wait()
}
// Set the time to t. If the time is after the current mocked time, then this is equivalent to
// Advance() with the difference. You may only Set the time earlier than the current time before
// starting tickers and timers (e.g. at the start of your test case).
func (m *Mock) Set(t time.Time) AdvanceWaiter {
m.tb.Helper()
w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})}
m.mu.Lock()
if t.Before(m.cur) {
defer close(w.ch)
defer m.mu.Unlock()
// past
if !m.nextTime.IsZero() {
m.tb.Error("Set mock clock to the past after timers/tickers started")
}
m.cur = t
return w
}
// future
// nextTime.IsZero implies no events scheduled.
if m.nextTime.IsZero() || t.Before(m.nextTime) {
defer close(w.ch)
defer m.mu.Unlock()
m.cur = t
return w
}
if t.After(m.nextTime) {
defer close(w.ch)
defer m.mu.Unlock()
m.tb.Errorf("cannot Set time to %s which is beyond next timer/ticker event at %s",
t.String(), m.nextTime)
return w
}
m.cur = m.nextTime
go m.advanceLocked(w)
return w
}
// AdvanceNext advances the clock to the next timer or tick event. It fails the test if there are
// none scheduled. It returns the duration the clock was advanced and a waiter that can be used to
// wait for the timer/tick event(s) to finish.
func (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter) {
m.mu.Lock()
m.tb.Helper()
w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})}
if m.nextTime.IsZero() {
defer close(w.ch)
defer m.mu.Unlock()
m.tb.Error("cannot AdvanceNext because there are no timers or tickers running")
}
d := m.nextTime.Sub(m.cur)
m.cur = m.nextTime
go m.advanceLocked(w)
return d, w
}
// Peek returns the duration until the next ticker or timer event and the value
// true, or, if there are no running tickers or timers, it returns zero and
// false.
func (m *Mock) Peek() (d time.Duration, ok bool) {
m.mu.Lock()
defer m.mu.Unlock()
if m.nextTime.IsZero() {
return 0, false
}
return m.nextTime.Sub(m.cur), true
}
// Trapper allows the creation of Traps
type Trapper struct {
// mock is the underlying Mock. This is a thin wrapper around Mock so that
// we can have our interface look like mClock.Trap().NewTimer("foo")
mock *Mock
}
func (t Trapper) NewTimer(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionNewTimer, tags)
}
func (t Trapper) AfterFunc(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionAfterFunc, tags)
}
func (t Trapper) TimerStop(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionTimerStop, tags)
}
func (t Trapper) TimerReset(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionTimerReset, tags)
}
func (t Trapper) TickerFunc(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionTickerFunc, tags)
}
func (t Trapper) TickerFuncWait(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionTickerFuncWait, tags)
}
func (t Trapper) NewTicker(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionNewTicker, tags)
}
func (t Trapper) TickerStop(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionTickerStop, tags)
}
func (t Trapper) TickerReset(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionTickerReset, tags)
}
func (t Trapper) Now(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionNow, tags)
}
func (t Trapper) Since(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionSince, tags)
}
func (t Trapper) Until(tags ...string) *Trap {
return t.mock.newTrap(clockFunctionUntil, tags)
}
func (m *Mock) Trap() Trapper {
return Trapper{m}
}
func (m *Mock) newTrap(fn clockFunction, tags []string) *Trap {
m.mu.Lock()
defer m.mu.Unlock()
tr := &Trap{
fn: fn,
tags: tags,
mock: m,
calls: make(chan *Call),
done: make(chan struct{}),
}
m.traps = append(m.traps, tr)
return tr
}
// NewMock creates a new Mock with the time set to midnight UTC on Jan 1, 2024.
// You may re-set the time earlier than this, but only before timers or tickers
// are created.
func NewMock(tb testing.TB) *Mock {
cur, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z")
if err != nil {
panic(err)
}
return &Mock{
tb: tb,
cur: cur,
}
}
var _ Clock = &Mock{}
type mockTickerFunc struct {
ctx context.Context
d time.Duration
f func() error
nxt time.Time
mock *Mock
// cond is a condition Locked on the main Mock.mu
cond *sync.Cond
// done is true when the ticker exits
done bool
// err holds the error when the ticker exits
err error
}
func (m *mockTickerFunc) next() time.Time {
return m.nxt
}
func (m *mockTickerFunc) fire(_ time.Time) {
m.mock.mu.Lock()
defer m.mock.mu.Unlock()
if m.done {
return
}
m.nxt = m.nxt.Add(m.d)
m.mock.recomputeNextLocked()
m.mock.mu.Unlock()
err := m.f()
m.mock.mu.Lock()
if err != nil {
m.exitLocked(err)
}
}
func (m *mockTickerFunc) exitLocked(err error) {
if m.done {
return
}
m.done = true
m.err = err
m.mock.removeEventLocked(m)
m.cond.Broadcast()
}
func (m *mockTickerFunc) waitForCtx() {
<-m.ctx.Done()
m.mock.mu.Lock()
defer m.mock.mu.Unlock()
m.exitLocked(m.ctx.Err())
}
func (m *mockTickerFunc) Wait(tags ...string) error {
m.mock.mu.Lock()
defer m.mock.mu.Unlock()
c := newCall(clockFunctionTickerFuncWait, tags)
m.mock.matchCallLocked(c)
defer close(c.complete)
for !m.done {
m.cond.Wait()
}
return m.err
}
var _ Waiter = &mockTickerFunc{}
type clockFunction int
const (
clockFunctionNewTimer clockFunction = iota
clockFunctionAfterFunc
clockFunctionTimerStop
clockFunctionTimerReset
clockFunctionTickerFunc
clockFunctionTickerFuncWait
clockFunctionNewTicker
clockFunctionTickerReset
clockFunctionTickerStop
clockFunctionNow
clockFunctionSince
clockFunctionUntil
)
type callArg func(c *Call)
type Call struct {
Time time.Time
Duration time.Duration
Tags []string
fn clockFunction
releases sync.WaitGroup
complete chan struct{}
}
func (c *Call) Release() {
c.releases.Done()
<-c.complete
}
func withTime(t time.Time) callArg {
return func(c *Call) {
c.Time = t
}
}
func withDuration(d time.Duration) callArg {
return func(c *Call) {
c.Duration = d
}
}
func newCall(fn clockFunction, tags []string, args ...callArg) *Call {
c := &Call{
fn: fn,
Tags: tags,
complete: make(chan struct{}),
}
for _, a := range args {
a(c)
}
return c
}
type Trap struct {
fn clockFunction
tags []string
mock *Mock
calls chan *Call
done chan struct{}
}
func (t *Trap) catch(c *Call) {
select {
case t.calls <- c:
case <-t.done:
c.Release()
}
}
func (t *Trap) matches(c *Call) bool {
if t.fn != c.fn {
return false
}
for _, tag := range t.tags {
if !slices.Contains(c.Tags, tag) {
return false
}
}
return true
}
func (t *Trap) Close() {
t.mock.mu.Lock()
defer t.mock.mu.Unlock()
for i, tr := range t.mock.traps {
if t == tr {
t.mock.traps = append(t.mock.traps[:i], t.mock.traps[i+1:]...)
}
}
close(t.done)
}
var ErrTrapClosed = xerrors.New("trap closed")
func (t *Trap) Wait(ctx context.Context) (*Call, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-t.done:
return nil, ErrTrapClosed
case c := <-t.calls:
return c, nil
}
}
// MustWait calls Wait() and then if there is an error, immediately fails the
// test via tb.Fatalf()
func (t *Trap) MustWait(ctx context.Context) *Call {
t.mock.tb.Helper()
c, err := t.Wait(ctx)
if err != nil {
t.mock.tb.Fatalf("context expired while waiting for trap: %s", err.Error())
}
return c
}
-216
View File
@@ -1,216 +0,0 @@
package clock_test
import (
"context"
"testing"
"time"
"github.com/coder/coder/v2/clock"
)
func TestTimer_NegativeDuration(t *testing.T) {
t.Parallel()
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mClock := clock.NewMock(t)
start := mClock.Now()
trap := mClock.Trap().NewTimer()
defer trap.Close()
timers := make(chan *clock.Timer, 1)
go func() {
timers <- mClock.NewTimer(-time.Second)
}()
c := trap.MustWait(ctx)
c.Release()
// trap returns the actual passed value
if c.Duration != -time.Second {
t.Fatalf("expected -time.Second, got: %v", c.Duration)
}
tmr := <-timers
select {
case <-ctx.Done():
t.Fatal("timeout waiting for timer")
case tme := <-tmr.C:
// the tick is the current time, not the past
if !tme.Equal(start) {
t.Fatalf("expected time %v, got %v", start, tme)
}
}
if tmr.Stop() {
t.Fatal("timer still running")
}
}
func TestAfterFunc_NegativeDuration(t *testing.T) {
t.Parallel()
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mClock := clock.NewMock(t)
trap := mClock.Trap().AfterFunc()
defer trap.Close()
timers := make(chan *clock.Timer, 1)
done := make(chan struct{})
go func() {
timers <- mClock.AfterFunc(-time.Second, func() {
close(done)
})
}()
c := trap.MustWait(ctx)
c.Release()
// trap returns the actual passed value
if c.Duration != -time.Second {
t.Fatalf("expected -time.Second, got: %v", c.Duration)
}
tmr := <-timers
select {
case <-ctx.Done():
t.Fatal("timeout waiting for timer")
case <-done:
// OK!
}
if tmr.Stop() {
t.Fatal("timer still running")
}
}
func TestNewTicker(t *testing.T) {
t.Parallel()
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mClock := clock.NewMock(t)
start := mClock.Now()
trapNT := mClock.Trap().NewTicker("new")
defer trapNT.Close()
trapStop := mClock.Trap().TickerStop("stop")
defer trapStop.Close()
trapReset := mClock.Trap().TickerReset("reset")
defer trapReset.Close()
tickers := make(chan *clock.Ticker, 1)
go func() {
tickers <- mClock.NewTicker(time.Hour, "new")
}()
c := trapNT.MustWait(ctx)
c.Release()
if c.Duration != time.Hour {
t.Fatalf("expected time.Hour, got: %v", c.Duration)
}
tkr := <-tickers
for i := 0; i < 3; i++ {
mClock.Advance(time.Hour).MustWait(ctx)
}
// should get first tick, rest dropped
tTime := start.Add(time.Hour)
select {
case <-ctx.Done():
t.Fatal("timeout waiting for ticker")
case tick := <-tkr.C:
if !tick.Equal(tTime) {
t.Fatalf("expected time %v, got %v", tTime, tick)
}
}
go tkr.Reset(time.Minute, "reset")
c = trapReset.MustWait(ctx)
mClock.Advance(time.Second).MustWait(ctx)
c.Release()
if c.Duration != time.Minute {
t.Fatalf("expected time.Minute, got: %v", c.Duration)
}
mClock.Advance(time.Minute).MustWait(ctx)
// tick should show present time, ensuring the 2 hour ticks got dropped when
// we didn't read from the channel.
tTime = mClock.Now()
select {
case <-ctx.Done():
t.Fatal("timeout waiting for ticker")
case tick := <-tkr.C:
if !tick.Equal(tTime) {
t.Fatalf("expected time %v, got %v", tTime, tick)
}
}
go tkr.Stop("stop")
trapStop.MustWait(ctx).Release()
mClock.Advance(time.Hour).MustWait(ctx)
select {
case <-tkr.C:
t.Fatal("ticker still running")
default:
// OK
}
// Resetting after stop
go tkr.Reset(time.Minute, "reset")
trapReset.MustWait(ctx).Release()
mClock.Advance(time.Minute).MustWait(ctx)
tTime = mClock.Now()
select {
case <-ctx.Done():
t.Fatal("timeout waiting for ticker")
case tick := <-tkr.C:
if !tick.Equal(tTime) {
t.Fatalf("expected time %v, got %v", tTime, tick)
}
}
}
func TestPeek(t *testing.T) {
t.Parallel()
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mClock := clock.NewMock(t)
d, ok := mClock.Peek()
if d != 0 {
t.Fatal("expected Peek() to return 0")
}
if ok {
t.Fatal("expected Peek() to return false")
}
tmr := mClock.NewTimer(time.Second)
d, ok = mClock.Peek()
if d != time.Second {
t.Fatal("expected Peek() to return 1s")
}
if !ok {
t.Fatal("expected Peek() to return true")
}
mClock.Advance(999 * time.Millisecond).MustWait(ctx)
d, ok = mClock.Peek()
if d != time.Millisecond {
t.Fatal("expected Peek() to return 1ms")
}
if !ok {
t.Fatal("expected Peek() to return true")
}
stopped := tmr.Stop()
if !stopped {
t.Fatal("expected Stop() to return true")
}
d, ok = mClock.Peek()
if d != 0 {
t.Fatal("expected Peek() to return 0")
}
if ok {
t.Fatal("expected Peek() to return false")
}
}
-80
View File
@@ -1,80 +0,0 @@
package clock
import (
"context"
"time"
)
type realClock struct{}
func NewReal() Clock {
return realClock{}
}
func (realClock) NewTicker(d time.Duration, _ ...string) *Ticker {
tkr := time.NewTicker(d)
return &Ticker{ticker: tkr, C: tkr.C}
}
func (realClock) TickerFunc(ctx context.Context, d time.Duration, f func() error, _ ...string) Waiter {
ct := &realContextTicker{
ctx: ctx,
tkr: time.NewTicker(d),
f: f,
err: make(chan error, 1),
}
go ct.run()
return ct
}
type realContextTicker struct {
ctx context.Context
tkr *time.Ticker
f func() error
err chan error
}
func (t *realContextTicker) Wait(_ ...string) error {
return <-t.err
}
func (t *realContextTicker) run() {
defer t.tkr.Stop()
for {
select {
case <-t.ctx.Done():
t.err <- t.ctx.Err()
return
case <-t.tkr.C:
err := t.f()
if err != nil {
t.err <- err
return
}
}
}
}
func (realClock) NewTimer(d time.Duration, _ ...string) *Timer {
rt := time.NewTimer(d)
return &Timer{C: rt.C, timer: rt}
}
func (realClock) AfterFunc(d time.Duration, f func(), _ ...string) *Timer {
rt := time.AfterFunc(d, f)
return &Timer{C: rt.C, timer: rt}
}
func (realClock) Now(_ ...string) time.Time {
return time.Now()
}
func (realClock) Since(t time.Time, _ ...string) time.Duration {
return time.Since(t)
}
func (realClock) Until(t time.Time, _ ...string) time.Duration {
return time.Until(t)
}
var _ Clock = realClock{}
-75
View File
@@ -1,75 +0,0 @@
package clock
import "time"
// A Ticker holds a channel that delivers “ticks” of a clock at intervals.
type Ticker struct {
C <-chan time.Time
//nolint: revive
c chan time.Time
ticker *time.Ticker // realtime impl, if set
d time.Duration // period, if set
nxt time.Time // next tick time
mock *Mock // mock clock, if set
stopped bool // true if the ticker is not running
}
func (t *Ticker) fire(tt time.Time) {
t.mock.mu.Lock()
defer t.mock.mu.Unlock()
if t.stopped {
return
}
for !t.nxt.After(t.mock.cur) {
t.nxt = t.nxt.Add(t.d)
}
t.mock.recomputeNextLocked()
select {
case t.c <- tt:
default:
}
}
func (t *Ticker) next() time.Time {
return t.nxt
}
// Stop turns off a ticker. After Stop, no more ticks will be sent. Stop does
// not close the channel, to prevent a concurrent goroutine reading from the
// channel from seeing an erroneous "tick".
func (t *Ticker) Stop(tags ...string) {
if t.ticker != nil {
t.ticker.Stop()
return
}
t.mock.mu.Lock()
defer t.mock.mu.Unlock()
c := newCall(clockFunctionTickerStop, tags)
t.mock.matchCallLocked(c)
defer close(c.complete)
t.mock.removeEventLocked(t)
t.stopped = true
}
// Reset stops a ticker and resets its period to the specified duration. The
// next tick will arrive after the new period elapses. The duration d must be
// greater than zero; if not, Reset will panic.
func (t *Ticker) Reset(d time.Duration, tags ...string) {
if t.ticker != nil {
t.ticker.Reset(d)
return
}
t.mock.mu.Lock()
defer t.mock.mu.Unlock()
c := newCall(clockFunctionTickerReset, tags, withDuration(d))
t.mock.matchCallLocked(c)
defer close(c.complete)
t.nxt = t.mock.cur.Add(d)
t.d = d
if t.stopped {
t.stopped = false
t.mock.addEventLocked(t)
} else {
t.mock.recomputeNextLocked()
}
}
-81
View File
@@ -1,81 +0,0 @@
package clock
import "time"
// The Timer type represents a single event. When the Timer expires, the current time will be sent
// on C, unless the Timer was created by AfterFunc. A Timer must be created with NewTimer or
// AfterFunc.
type Timer struct {
C <-chan time.Time
//nolint: revive
c chan time.Time
timer *time.Timer // realtime impl, if set
nxt time.Time // next tick time
mock *Mock // mock clock, if set
fn func() // AfterFunc function, if set
stopped bool // True if stopped, false if running
}
func (t *Timer) fire(tt time.Time) {
t.mock.removeTimer(t)
if t.fn != nil {
t.fn()
} else {
t.c <- tt
}
}
func (t *Timer) next() time.Time {
return t.nxt
}
// Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the
// timer has already expired or been stopped. Stop does not close the channel, to prevent a read
// from the channel succeeding incorrectly.
//
// See https://pkg.go.dev/time#Timer.Stop for more information.
func (t *Timer) Stop(tags ...string) bool {
if t.timer != nil {
return t.timer.Stop()
}
t.mock.mu.Lock()
defer t.mock.mu.Unlock()
c := newCall(clockFunctionTimerStop, tags)
t.mock.matchCallLocked(c)
defer close(c.complete)
result := !t.stopped
t.mock.removeTimerLocked(t)
return result
}
// Reset changes the timer to expire after duration d. It returns true if the timer had been active,
// false if the timer had expired or been stopped.
//
// See https://pkg.go.dev/time#Timer.Reset for more information.
func (t *Timer) Reset(d time.Duration, tags ...string) bool {
if t.timer != nil {
return t.timer.Reset(d)
}
t.mock.mu.Lock()
defer t.mock.mu.Unlock()
c := newCall(clockFunctionTimerReset, tags, withDuration(d))
t.mock.matchCallLocked(c)
defer close(c.complete)
result := !t.stopped
select {
case <-t.c:
default:
}
if d <= 0 {
// zero or negative duration timer means we should immediately re-fire
// it, rather than remove and re-add it.
t.stopped = false
go t.fire(t.mock.cur)
return result
}
t.mock.removeTimerLocked(t)
t.stopped = false
t.nxt = t.mock.cur.Add(d)
t.mock.addEventLocked(t)
return result
}
+4 -4
View File
@@ -6,7 +6,7 @@ import (
"sync"
"time"
"github.com/coder/coder/v2/clock"
"github.com/coder/quartz"
)
// Notifier triggers callbacks at given intervals until some event happens. The
@@ -26,7 +26,7 @@ type Notifier struct {
countdown []time.Duration
// for testing
clock clock.Clock
clock quartz.Clock
}
// Condition is a function that gets executed periodically, and receives the
@@ -43,7 +43,7 @@ type Condition func(now time.Time) (deadline time.Time, callback func())
type Option func(*Notifier)
// WithTestClock is used in tests to inject a mock Clock
func WithTestClock(clk clock.Clock) Option {
func WithTestClock(clk quartz.Clock) Option {
return func(n *Notifier) {
n.clock = clk
}
@@ -67,7 +67,7 @@ func New(cond Condition, interval time.Duration, countdown []time.Duration, opts
countdown: ct,
condition: cond,
notifiedAt: make(map[time.Duration]bool),
clock: clock.NewReal(),
clock: quartz.NewReal(),
}
for _, opt := range opts {
opt(n)
+2 -2
View File
@@ -7,9 +7,9 @@ import (
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/coderd/autobuild/notify"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestNotifier(t *testing.T) {
@@ -87,7 +87,7 @@ func TestNotifier(t *testing.T) {
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
mClock.Set(now).MustWait(ctx)
numConditions := 0
numCalls := 0
+2 -2
View File
@@ -39,7 +39,6 @@ import (
"cdr.dev/slog"
agentproto "github.com/coder/coder/v2/agent/proto"
"github.com/coder/coder/v2/buildinfo"
"github.com/coder/coder/v2/clock"
_ "github.com/coder/coder/v2/coderd/apidoc" // Used for swagger docs.
"github.com/coder/coder/v2/coderd/appearance"
"github.com/coder/coder/v2/coderd/audit"
@@ -76,6 +75,7 @@ import (
"github.com/coder/coder/v2/provisionersdk"
"github.com/coder/coder/v2/site"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/quartz"
"github.com/coder/serpent"
)
@@ -573,7 +573,7 @@ func New(options *Options) *API {
options.PrometheusRegistry.MustRegister(stn)
}
api.NetworkTelemetryBatcher = tailnet.NewNetworkTelemetryBatcher(
clock.NewReal(),
quartz.NewReal(),
api.Options.NetworkTelemetryBatchFrequency,
api.Options.NetworkTelemetryBatchMaxSize,
api.handleNetworkTelemetry,
+4 -4
View File
@@ -8,7 +8,7 @@ import (
"time"
"cdr.dev/slog"
"github.com/coder/coder/v2/clock"
"github.com/coder/quartz"
)
const (
@@ -31,15 +31,15 @@ type Watchdog struct {
timeout chan struct{}
// for testing
clock clock.Clock
clock quartz.Clock
}
func NewWatchdog(ctx context.Context, logger slog.Logger, ps Pubsub) *Watchdog {
return NewWatchdogWithClock(ctx, logger, ps, clock.NewReal())
return NewWatchdogWithClock(ctx, logger, ps, quartz.NewReal())
}
// NewWatchdogWithClock returns a watchdog with the given clock. Product code should always call NewWatchDog.
func NewWatchdogWithClock(ctx context.Context, logger slog.Logger, ps Pubsub, c clock.Clock) *Watchdog {
func NewWatchdogWithClock(ctx context.Context, logger slog.Logger, ps Pubsub, c quartz.Clock) *Watchdog {
ctx, cancel := context.WithCancel(ctx)
w := &Watchdog{
ctx: ctx,
+3 -3
View File
@@ -8,15 +8,15 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/coderd/database/pubsub"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestWatchdog_NoTimeout(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
fPS := newFakePubsub()
@@ -74,7 +74,7 @@ func TestWatchdog_NoTimeout(t *testing.T) {
func TestWatchdog_Timeout(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
fPS := newFakePubsub()
+8 -8
View File
@@ -15,7 +15,6 @@ import (
gProto "google.golang.org/protobuf/proto"
"cdr.dev/slog"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/pubsub"
@@ -23,6 +22,7 @@ import (
"github.com/coder/coder/v2/coderd/rbac/policy"
agpl "github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
)
const (
@@ -116,16 +116,16 @@ var pgCoordSubject = rbac.Subject{
// NewPGCoord creates a high-availability coordinator that stores state in the PostgreSQL database and
// receives notifications of updates via the pubsub.
func NewPGCoord(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store) (agpl.Coordinator, error) {
return newPGCoordInternal(ctx, logger, ps, store, clock.NewReal())
return newPGCoordInternal(ctx, logger, ps, store, quartz.NewReal())
}
// NewTestPGCoord is only used in testing to pass a clock.Clock in.
func NewTestPGCoord(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk clock.Clock) (agpl.Coordinator, error) {
func NewTestPGCoord(ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk quartz.Clock) (agpl.Coordinator, error) {
return newPGCoordInternal(ctx, logger, ps, store, clk)
}
func newPGCoordInternal(
ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk clock.Clock,
ctx context.Context, logger slog.Logger, ps pubsub.Pubsub, store database.Store, clk quartz.Clock,
) (
*pgCoord, error,
) {
@@ -823,7 +823,7 @@ func newQuerier(ctx context.Context,
closeConnections chan *connIO,
numWorkers int,
firstHeartbeat chan struct{},
clk clock.Clock,
clk quartz.Clock,
) *querier {
updates := make(chan hbUpdate)
q := &querier{
@@ -1469,12 +1469,12 @@ type heartbeats struct {
lock sync.RWMutex
coordinators map[uuid.UUID]time.Time
timer *clock.Timer
timer *quartz.Timer
wg sync.WaitGroup
// for testing
clock clock.Clock
clock quartz.Clock
}
func newHeartbeats(
@@ -1482,7 +1482,7 @@ func newHeartbeats(
ps pubsub.Pubsub, store database.Store,
self uuid.UUID, update chan<- hbUpdate,
firstHeartbeat chan<- struct{},
clk clock.Clock,
clk quartz.Clock,
) *heartbeats {
h := &heartbeats{
ctx: ctx,
+5 -6
View File
@@ -10,8 +10,6 @@ import (
"testing"
"time"
"github.com/coder/coder/v2/clock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -21,6 +19,7 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/quartz"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbmock"
@@ -51,7 +50,7 @@ func TestHeartbeats_Cleanup(t *testing.T) {
mStore.EXPECT().CleanTailnetLostPeers(gomock.Any()).Times(2).Return(nil)
mStore.EXPECT().CleanTailnetTunnels(gomock.Any()).Times(2).Return(nil)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
trap := mClock.Trap().TickerFunc("heartbeats", "cleanupLoop")
defer trap.Close()
@@ -79,7 +78,7 @@ func TestHeartbeats_recvBeat_resetSkew(t *testing.T) {
ctx := testutil.Context(t, testutil.WaitShort)
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
trap := mClock.Trap().Until("heartbeats", "resetExpiryTimerWithLock")
defer trap.Close()
@@ -130,7 +129,7 @@ func TestHeartbeats_LostCoordinator_MarkLost(t *testing.T) {
ctrl := gomock.NewController(t)
mStore := dbmock.NewMockStore(ctrl)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
@@ -388,7 +387,7 @@ func TestPGCoordinatorUnhealthy(t *testing.T) {
ctrl := gomock.NewController(t)
mStore := dbmock.NewMockStore(ctrl)
ps := pubsub.NewInMemory()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
tfTrap := mClock.Trap().TickerFunc("heartbeats", "sendBeats")
defer tfTrap.Close()
+2 -3
View File
@@ -10,8 +10,6 @@ import (
"testing"
"time"
"github.com/coder/coder/v2/clock"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -33,6 +31,7 @@ import (
"github.com/coder/coder/v2/tailnet/proto"
agpltest "github.com/coder/coder/v2/tailnet/test"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestMain(m *testing.M) {
@@ -339,7 +338,7 @@ func TestPGCoordinatorSingle_MissedHeartbeats(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
afTrap := mClock.Trap().AfterFunc("heartbeats", "recvBeat")
defer afTrap.Close()
rstTrap := mClock.Trap().TimerReset("heartbeats", "resetExpiryTimerWithLock")
+1 -1
View File
@@ -97,7 +97,7 @@
name = "coder-${osArch}";
# Updated with ./scripts/update-flake.sh`.
# This should be updated whenever go.mod changes!
vendorHash = "sha256-5R2FelgM9NRYlse309NukEVh25pvusO2FXZx1VuWGoo=";
vendorHash = "sha256-0pLwV4zpu+3LEwGxuGgcrr5iHP8bDNYuHOSCsyDsv/g=";
proxyVendor = true;
src = ./.;
nativeBuildInputs = with pkgs; [ getopt openssl zstd ];
+1
View File
@@ -87,6 +87,7 @@ require (
github.com/cli/safeexec v1.0.1
github.com/coder/flog v1.1.0
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0
github.com/coder/quartz v0.1.0
github.com/coder/retry v1.5.1
github.com/coder/terraform-provider-coder v0.23.0
github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0
+2
View File
@@ -207,6 +207,8 @@ github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136 h1:0RgB61LcNs
github.com/coder/go-scim/pkg/v2 v2.0.0-20230221055123-1d63c1222136/go.mod h1:VkD1P761nykiq75dz+4iFqIQIZka189tx1BQLOp0Skc=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs=
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc=
github.com/coder/quartz v0.1.0 h1:cLL+0g5l7xTf6ordRnUMMiZtRE8Sq5LxpghS63vEXrQ=
github.com/coder/quartz v0.1.0/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA=
github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc=
github.com/coder/retry v1.5.1/go.mod h1:blHMk9vs6LkoRT9ZHyuZo360cufXEhrxqvEzeMtRGoY=
github.com/coder/serpent v0.7.0 h1:zGpD2GlF3lKIVkMjNGKbkip88qzd5r/TRcc30X/SrT0=
+5 -5
View File
@@ -24,8 +24,8 @@ import (
"tailscale.com/wgengine/wgcfg/nmcfg"
"cdr.dev/slog"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
)
const lostTimeout = 15 * time.Minute
@@ -70,7 +70,7 @@ type configMaps struct {
blockEndpoints bool
// for testing
clock clock.Clock
clock quartz.Clock
}
func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg.NodeID, nodeKey key.NodePrivate, discoKey key.DiscoPublic) *configMaps {
@@ -116,7 +116,7 @@ func newConfigMaps(logger slog.Logger, engine engineConfigurable, nodeID tailcfg
}},
},
peers: make(map[uuid.UUID]*peerLifecycle),
clock: clock.NewReal(),
clock: quartz.NewReal(),
}
go c.configLoop()
return c
@@ -657,9 +657,9 @@ type peerLifecycle struct {
node *tailcfg.Node
lost bool
lastHandshake time.Time
lostTimer *clock.Timer
lostTimer *quartz.Timer
readyForHandshake bool
readyForHandshakeTimer *clock.Timer
readyForHandshakeTimer *quartz.Timer
}
func (l *peerLifecycle) resetLostTimer() {
+8 -8
View File
@@ -21,9 +21,9 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestConfigMaps_setAddresses_different(t *testing.T) {
@@ -195,7 +195,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_neverConfigures(t *testing.
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
uut.clock = mClock
p1ID := uuid.UUID{1}
@@ -239,7 +239,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_outOfOrder(t *testing.T) {
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
uut.clock = mClock
p1ID := uuid.UUID{1}
@@ -310,7 +310,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake(t *testing.T) {
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
uut.clock = mClock
p1ID := uuid.UUID{1}
@@ -381,7 +381,7 @@ func TestConfigMaps_updatePeers_new_waitForHandshake_timeout(t *testing.T) {
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
uut.clock = mClock
p1ID := uuid.UUID{1}
@@ -566,7 +566,7 @@ func TestConfigMaps_updatePeers_lost(t *testing.T) {
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
start := mClock.Now()
uut.clock = mClock
@@ -651,7 +651,7 @@ func TestConfigMaps_updatePeers_lost_and_found(t *testing.T) {
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
start := mClock.Now()
uut.clock = mClock
@@ -736,7 +736,7 @@ func TestConfigMaps_setAllPeersLost(t *testing.T) {
discoKey := key.NewDisco()
uut := newConfigMaps(logger, fEng, nodeID, nodePrivateKey, discoKey.Public())
defer uut.close()
mClock := clock.NewMock(t)
mClock := quartz.NewMock(t)
start := mClock.Now()
uut.clock = mClock
+4 -4
View File
@@ -17,8 +17,8 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/v2/apiversion"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/quartz"
)
type streamIDContextKey struct{}
@@ -240,7 +240,7 @@ func (c communicator) loopResp() {
}
type NetworkTelemetryBatcher struct {
clock clock.Clock
clock quartz.Clock
frequency time.Duration
maxSize int
batchFn func(batch []*proto.TelemetryEvent)
@@ -248,11 +248,11 @@ type NetworkTelemetryBatcher struct {
mu sync.Mutex
closed chan struct{}
done chan struct{}
ticker *clock.Ticker
ticker *quartz.Ticker
pending []*proto.TelemetryEvent
}
func NewNetworkTelemetryBatcher(clk clock.Clock, frequency time.Duration, maxSize int, batchFn func(batch []*proto.TelemetryEvent)) *NetworkTelemetryBatcher {
func NewNetworkTelemetryBatcher(clk quartz.Clock, frequency time.Duration, maxSize int, batchFn func(batch []*proto.TelemetryEvent)) *NetworkTelemetryBatcher {
b := &NetworkTelemetryBatcher{
clock: clk,
frequency: frequency,
+2 -2
View File
@@ -15,11 +15,11 @@ import (
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/clock"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/tailnet/proto"
"github.com/coder/coder/v2/tailnet/tailnettest"
"github.com/coder/coder/v2/testutil"
"github.com/coder/quartz"
)
func TestClientService_ServeClient_V2(t *testing.T) {
@@ -182,7 +182,7 @@ func TestNetworkTelemetryBatcher(t *testing.T) {
var (
events = make(chan []*proto.TelemetryEvent, 64)
mClock = clock.NewMock(t)
mClock = quartz.NewMock(t)
b = tailnet.NewNetworkTelemetryBatcher(mClock, time.Millisecond, 3, func(batch []*proto.TelemetryEvent) {
assert.LessOrEqual(t, len(batch), 3)
events <- batch