diff --git a/agent/apphealth.go b/agent/apphealth.go index 0b7e87e57df68..1a5fd968835e6 100644 --- a/agent/apphealth.go +++ b/agent/apphealth.go @@ -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") diff --git a/agent/apphealth_test.go b/agent/apphealth_test.go index ff411433e3821..60647b6bf8064 100644 --- a/agent/apphealth_test.go +++ b/agent/apphealth_test.go @@ -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 { diff --git a/clock/README.md b/clock/README.md deleted file mode 100644 index 34f72444884a0..0000000000000 --- a/clock/README.md +++ /dev/null @@ -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(, 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(, 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. diff --git a/clock/clock.go b/clock/clock.go deleted file mode 100644 index ae550334844c2..0000000000000 --- a/clock/clock.go +++ /dev/null @@ -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 -} diff --git a/clock/example_test.go b/clock/example_test.go deleted file mode 100644 index de72312d7d036..0000000000000 --- a/clock/example_test.go +++ /dev/null @@ -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()) - } -} diff --git a/clock/mock.go b/clock/mock.go deleted file mode 100644 index 650d65a6b2128..0000000000000 --- a/clock/mock.go +++ /dev/null @@ -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 -} diff --git a/clock/mock_test.go b/clock/mock_test.go deleted file mode 100644 index 69aa683fded4a..0000000000000 --- a/clock/mock_test.go +++ /dev/null @@ -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") - } -} diff --git a/clock/real.go b/clock/real.go deleted file mode 100644 index 55800c87c58ba..0000000000000 --- a/clock/real.go +++ /dev/null @@ -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{} diff --git a/clock/ticker.go b/clock/ticker.go deleted file mode 100644 index 43700f31d4635..0000000000000 --- a/clock/ticker.go +++ /dev/null @@ -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() - } -} diff --git a/clock/timer.go b/clock/timer.go deleted file mode 100644 index b0cf0b33ac07d..0000000000000 --- a/clock/timer.go +++ /dev/null @@ -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 -} diff --git a/coderd/autobuild/notify/notifier.go b/coderd/autobuild/notify/notifier.go index d8226161507ef..ec7be11f81ada 100644 --- a/coderd/autobuild/notify/notifier.go +++ b/coderd/autobuild/notify/notifier.go @@ -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) diff --git a/coderd/autobuild/notify/notifier_test.go b/coderd/autobuild/notify/notifier_test.go index d53b06c1a2133..5cfdb33e1acd5 100644 --- a/coderd/autobuild/notify/notifier_test.go +++ b/coderd/autobuild/notify/notifier_test.go @@ -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 diff --git a/coderd/coderd.go b/coderd/coderd.go index 8bf7414fc4a14..5dd9b3f171654 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -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, diff --git a/coderd/database/pubsub/watchdog.go b/coderd/database/pubsub/watchdog.go index df54019bb49b2..b79c8ca777dd4 100644 --- a/coderd/database/pubsub/watchdog.go +++ b/coderd/database/pubsub/watchdog.go @@ -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, diff --git a/coderd/database/pubsub/watchdog_test.go b/coderd/database/pubsub/watchdog_test.go index 942f9eeb849c4..8a0550a35a15c 100644 --- a/coderd/database/pubsub/watchdog_test.go +++ b/coderd/database/pubsub/watchdog_test.go @@ -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() diff --git a/enterprise/tailnet/pgcoord.go b/enterprise/tailnet/pgcoord.go index ed2d3a7b7b5aa..1546f0ac3087b 100644 --- a/enterprise/tailnet/pgcoord.go +++ b/enterprise/tailnet/pgcoord.go @@ -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, diff --git a/enterprise/tailnet/pgcoord_internal_test.go b/enterprise/tailnet/pgcoord_internal_test.go index 5117131c05956..253487d28d196 100644 --- a/enterprise/tailnet/pgcoord_internal_test.go +++ b/enterprise/tailnet/pgcoord_internal_test.go @@ -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() diff --git a/enterprise/tailnet/pgcoord_test.go b/enterprise/tailnet/pgcoord_test.go index 6247680c68949..2232e3941eb0c 100644 --- a/enterprise/tailnet/pgcoord_test.go +++ b/enterprise/tailnet/pgcoord_test.go @@ -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") diff --git a/flake.nix b/flake.nix index 8f4b41356d434..7ec50f24341cc 100644 --- a/flake.nix +++ b/flake.nix @@ -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 ]; diff --git a/go.mod b/go.mod index 923736037cbab..94d45ff8da50f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 22db038ee3aca..ccc88db58d70e 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/tailnet/configmaps.go b/tailnet/configmaps.go index 5d609b90c4bd8..a6ef9f40028b1 100644 --- a/tailnet/configmaps.go +++ b/tailnet/configmaps.go @@ -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() { diff --git a/tailnet/configmaps_internal_test.go b/tailnet/configmaps_internal_test.go index 83b15387a9a43..718496244d870 100644 --- a/tailnet/configmaps_internal_test.go +++ b/tailnet/configmaps_internal_test.go @@ -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 diff --git a/tailnet/service.go b/tailnet/service.go index d5842151ecb26..f3c2ed22f6e76 100644 --- a/tailnet/service.go +++ b/tailnet/service.go @@ -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, diff --git a/tailnet/service_test.go b/tailnet/service_test.go index 0bbe9c20e6662..0b41d29eb1669 100644 --- a/tailnet/service_test.go +++ b/tailnet/service_test.go @@ -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