Documentation
¶
Overview ¶
Package clock abstracts the passage of time so that code depending on it can be tested without waiting.
A DNS engine is full of deadlines: cache entries expire, upstream queries time out, health checks back off, statistics roll over. Testing any of that against the real clock means either sleeping — which makes a suite slow and flaky — or leaving the behaviour untested. Neither is acceptable in a library whose correctness at the boundary is the whole point, so every component in GatewayDNS takes a Clock and the tests hand it a Fake.
Choosing an implementation ¶
System is the real clock and is what production code uses. NewFake returns a clock whose time only moves when a test moves it, and whose timers fire deterministically as it does — no goroutine scheduling, no sleeping, no tolerance windows.
Components accept a Clock and treat nil as System, so a caller that does not care never has to mention time:
c := clock.OrSystem(cfg.Clock)
Monotonic time ¶
[System.Now] returns a time.Time carrying Go's monotonic reading, so durations computed from it are immune to wall-clock jumps. That matters more than it sounds: an NTP correction during an outage must not make every cache entry expire at once, and must not make them all live forever either. Code measuring an interval should subtract two Now values, or compare against a deadline built with Add, rather than reaching for the wall clock.
Fake deliberately has no monotonic reading. Its time is whatever the test says it is, including running backwards, which is how clock-jump handling gets tested at all.
Concurrency ¶
Every implementation in this package is safe for concurrent use, including Fake: a test may advance the clock from one goroutine while the code under test waits on a timer in another.
Index ¶
- type Clock
- type Fake
- func (f *Fake) Advance(d time.Duration)
- func (f *Fake) BlockUntil(n int)
- func (f *Fake) BlockUntilContext(ctx context.Context, n int) error
- func (f *Fake) Deadlines() []time.Time
- func (f *Fake) NewTicker(d time.Duration) Ticker
- func (f *Fake) NewTimer(d time.Duration) Timer
- func (f *Fake) Now() time.Time
- func (f *Fake) Set(t time.Time)
- func (f *Fake) Since(t time.Time) time.Duration
- func (f *Fake) Sleep(ctx context.Context, d time.Duration) error
- func (f *Fake) Until(t time.Time) time.Duration
- func (f *Fake) Waiters() int
- type Ticker
- type Timer
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// Since returns the time elapsed since t. Implementations must compute it
// against their own notion of now, never against [time.Now].
Since(t time.Time) time.Duration
// Until returns the duration remaining until t, negative if t has passed.
Until(t time.Time) time.Duration
// NewTimer returns a timer that fires once after d.
NewTimer(d time.Duration) Timer
// NewTicker returns a ticker that fires every d. It panics if d <= 0, the
// same as [time.NewTicker].
NewTicker(d time.Duration) Ticker
// Sleep blocks for d or until ctx is done, whichever comes first. It
// returns ctx.Err() if the context ended first and nil otherwise.
//
// Sleep takes a context because a DNS server must be able to shut down
// promptly, and a bare sleep is the most common reason it cannot.
Sleep(ctx context.Context, d time.Duration) error
}
Clock reports the current time and creates timers.
The interface is deliberately small. Every method corresponds to something the engine genuinely does, because each one is a method a Fake has to implement correctly and a caller has to reason about.
func OrSystem ¶
OrSystem returns c, or System when c is nil.
It exists so that every component can accept an optional Clock without repeating the nil check, and so that "no clock configured" can never mean "no clock at all".
Example ¶
ExampleOrSystem shows the nil-Clock convention every component follows, so that "no clock configured" never has to mean "no clock at all".
package main
import (
"fmt"
"time"
"github.com/daboss2003/dns/clock"
)
// exampleEpoch is the instant the examples start from. A recognisable, non-zero
// wall time keeps the expected output readable and avoids the year-1 underflow
// [clock.NewFake] warns about.
var exampleEpoch = time.Date(2024, time.March, 1, 12, 0, 0, 0, time.UTC)
func main() {
type Config struct {
// Clock is optional; production leaves it nil.
Clock clock.Clock
}
var production Config
fmt.Println("unset is the real clock:", clock.OrSystem(production.Clock) == clock.System())
underTest := Config{Clock: clock.NewFake(exampleEpoch)}
fmt.Println("set is handed back: ", clock.OrSystem(underTest.Clock) == underTest.Clock)
}
Output: unset is the real clock: true set is handed back: true
type Fake ¶
type Fake struct {
// contains filtered or unexported fields
}
Fake is a Clock whose time moves only when a test moves it.
Timers created from a Fake fire during Fake.Advance, at their exact deadline, in deadline order. There is no sleeping, no scheduler dependence and no tolerance window, so a test that asserts "the entry expired after exactly its TTL" means exactly that.
The usual shape of a test is:
c := clock.NewFake(time.Unix(0, 0)) go worker(c) // creates a timer somewhere inside c.BlockUntil(1) // wait for it to exist before moving time c.Advance(30 * time.Second)
Fake.BlockUntil is what removes the race between the code under test creating its timer and the test advancing past it. Without it, Advance can run first and the timer never fires, which is the single most common way a fake-clock test becomes flaky.
A Fake is safe for concurrent use.
Example ¶
ExampleFake shows the pattern every time-dependent test in GatewayDNS uses: start the code under test, wait for its timer to exist, then move the clock.
package main
import (
"fmt"
"time"
"github.com/daboss2003/dns/clock"
)
// exampleEpoch is the instant the examples start from. A recognisable, non-zero
// wall time keeps the expected output readable and avoids the year-1 underflow
// [clock.NewFake] warns about.
var exampleEpoch = time.Date(2024, time.March, 1, 12, 0, 0, 0, time.UTC)
func main() {
c := clock.NewFake(exampleEpoch)
// Stand in for a component that arms a timeout on its own goroutine.
expired := make(chan time.Time, 1)
go func() {
t := c.NewTimer(30 * time.Second)
defer t.Stop()
expired <- <-t.C()
}()
// BlockUntil is what removes the race. Without it, Advance can run before
// the goroutine has created its timer, in which case the timer is armed
// *after* the clock has already gone past its deadline and it never fires —
// the single most common way a fake-clock test becomes flaky. BlockUntil
// waits for the timer to be registered, so the ordering is no longer a
// matter of which goroutine the scheduler picks.
c.BlockUntil(1)
c.Advance(30 * time.Second)
fmt.Println("expired at:", (<-expired).Format(time.RFC3339))
fmt.Println("clock now: ", c.Now().Format(time.RFC3339))
}
Output: expired at: 2024-03-01T12:00:30Z clock now: 2024-03-01T12:00:30Z
func NewFake ¶
NewFake returns a Fake reading t.
Prefer a recognisable, non-zero instant. A zero time.Time is year 1, and a duration subtracted from it silently underflows into the distant future, which makes a broken expiry test look like a passing one.
func (*Fake) Advance ¶
Advance moves the clock forward by d, firing every timer and ticker whose deadline it passes.
Each waiter fires at its own deadline rather than at the destination, so a timer set for 1s and one set for 2s observe 1s and 2s respectively when the clock advances by 5s — not 5s each. Code that timestamps its own work during a callback therefore records the time it would have run at.
Advance panics on a negative duration; use Fake.Set to move time backwards.
Example ¶
ExampleFake_Advance shows that each waiter observes its own deadline rather than the instant the clock was moved to. Code that timestamps its work when a timer fires therefore records the time it would really have run at.
package main
import (
"fmt"
"time"
"github.com/daboss2003/dns/clock"
)
// exampleEpoch is the instant the examples start from. A recognisable, non-zero
// wall time keeps the expected output readable and avoids the year-1 underflow
// [clock.NewFake] warns about.
var exampleEpoch = time.Date(2024, time.March, 1, 12, 0, 0, 0, time.UTC)
func main() {
c := clock.NewFake(exampleEpoch)
short := c.NewTimer(1 * time.Second)
long := c.NewTimer(2 * time.Second)
unreached := c.NewTimer(1 * time.Hour)
defer unreached.Stop()
// One jump of five seconds passes both deadlines.
c.Advance(5 * time.Second)
fmt.Println("1s timer saw:", (<-short.C()).Sub(exampleEpoch))
fmt.Println("2s timer saw:", (<-long.C()).Sub(exampleEpoch))
fmt.Println("clock now: ", c.Now().Sub(exampleEpoch))
fmt.Println("still armed: ", c.Waiters())
}
Output: 1s timer saw: 1s 2s timer saw: 2s clock now: 5s still armed: 1
func (*Fake) BlockUntil ¶
BlockUntil waits until at least n timers or tickers are pending.
Use it before Fake.Advance whenever the code under test creates its timer on another goroutine. Without it the test races the code it is testing, and loses intermittently.
BlockUntil waits indefinitely; use Fake.BlockUntilContext to bound it. A test that hangs here is reporting something real — the timer it expected was never created.
func (*Fake) BlockUntilContext ¶
BlockUntilContext waits until at least n timers or tickers are pending, or ctx is done. It returns ctx.Err() in the latter case.
func (*Fake) Deadlines ¶
Deadlines returns the pending deadlines in ascending order. It exists to make a failing timing test explain itself: asserting "nothing fired" is far less useful than reporting what was actually scheduled.
func (*Fake) Set ¶
Set moves the clock to t.
Moving forward fires timers exactly as Fake.Advance does. Moving backwards fires nothing and leaves every deadline where it was, which is the point: it models a wall-clock correction, and is how code that must survive one gets tested. Note that System is immune to this by construction, because its durations come from Go's monotonic reading.
Example ¶
ExampleFake_Set shows a backwards jump: the wall clock is corrected by NTP while a timer is armed.
Nothing fires and no deadline moves, because a deadline is an absolute instant. The clock has to climb all the way back before the timer is due. This is exactly what clock.System is immune to — its durations come from Go's monotonic reading — and reproducing the hazard is the reason a clock.Fake deliberately has no monotonic reading of its own.
package main
import (
"fmt"
"time"
"github.com/daboss2003/dns/clock"
)
// exampleEpoch is the instant the examples start from. A recognisable, non-zero
// wall time keeps the expected output readable and avoids the year-1 underflow
// [clock.NewFake] warns about.
var exampleEpoch = time.Date(2024, time.March, 1, 12, 0, 0, 0, time.UTC)
func main() {
c := clock.NewFake(exampleEpoch)
t := c.NewTimer(1 * time.Minute)
defer t.Stop()
// NTP steps the machine back an hour.
c.Set(exampleEpoch.Add(-time.Hour))
fmt.Println("now: ", c.Now().Format(time.RFC3339))
fmt.Println("pending: ", c.Waiters())
fmt.Println("deadline:", c.Deadlines()[0].Format(time.RFC3339))
select {
case v := <-t.C():
fmt.Println("fired at:", v.Format(time.RFC3339))
default:
fmt.Println("fired: no")
}
// An hour and a minute of real elapsed time later, the deadline is finally
// reached.
c.Advance(time.Hour + time.Minute)
fmt.Println("fired at:", (<-t.C()).Format(time.RFC3339))
}
Output: now: 2024-03-01T11:00:00Z pending: 1 deadline: 2024-03-01T12:01:00Z fired: no fired at: 2024-03-01T12:01:00Z
type Ticker ¶
type Ticker interface {
// C returns the channel on which ticks are delivered.
C() <-chan time.Time
// Stop halts the ticker. It does not close C.
Stop()
// Reset changes the tick interval to d. It panics if d <= 0.
Reset(d time.Duration)
}
Ticker delivers ticks at intervals, mirroring time.Ticker.
type Timer ¶
type Timer interface {
// C returns the channel on which the time is delivered.
C() <-chan time.Time
// Stop prevents the timer from firing, reporting whether it had not already
// fired or been stopped. As with [time.Timer.Stop], it does not close C.
Stop() bool
// Reset changes the timer to expire after d, reporting whether the timer
// was active. The same caveats as [time.Timer.Reset] apply: a timer should
// be stopped and drained before it is reset.
Reset(d time.Duration) bool
}
Timer is a one-shot timer, mirroring time.Timer.