Documentation
¶
Overview ¶
Package clock abstracts the passage of time so higher layers can inject a deterministic time source in tests instead of coupling to the wall clock.
The package carries two independent capabilities behind two independent interfaces, and that separation is the load-bearing design decision:
- Clock READS time (Now, Since). It is frozen at two methods on purpose. It is reachable from the published pkg module — pkg/v1/data/cache.Config is a type alias whose Clock field has this type — so every method added to it would break every hand-written double downstream, at compile time, with no deprecation window.
- Waiter WAITS for time (After, NewTimer, NewTicker, Sleep). Code that only stamps records keeps asking for a Clock; code that schedules, retries, or times out asks for a Timed, the union of the two.
System is the production value: a Timed delegating to package time. ManualClock is the test double: a Timed whose time moves only when the test moves it, so a timeout, a retry backoff, or a ticker cadence can be asserted exactly, without sleeping and without a wall-clock tolerance.
Non-positive durations ¶
After, NewTimer and Sleep accept a non-positive delay and read it as "already elapsed": the value is delivered at once, Sleep returns at once. That is the stdlib contract, it is total, and it is the only sensible reading of a delay that has no future — a clamp needing no explanation (ADR 0031).
NewTicker and Ticker.Reset REFUSE a non-positive period and panic. A ticker has no meaningful zero period, and any positive value this package invented instead would be arbitrary — the other half of ADR 0031. The panic is raised HERE, by requirePositivePeriod, with a message naming this package and the offending duration, and it is raised identically by System and by ManualClock: time.NewTicker's own panic never reaches a caller, so the two implementations stay interchangeable at the boundary that matters most.
Relationship to testing/synctest ¶
The stdlib's testing/synctest replaces package time inside a bubble, so System already observes fake time there — the two mechanisms are complementary, not redundant. Which one to reach for is documented in CLAUDE.md §"clock vs testing/synctest", and both halves of that comparison are pinned by synctest_external_test.go rather than asserted in prose.
Package clock — hosts compile-time interface assertions — keeping them out of the production source so the runtime binary carries no diagnostic-only declarations.
Package clock — hosts ManualClock, the deterministic Timed whose instant only moves when a caller moves it.
Package clock — hosts manualTicker, the Ticker handle a ManualClock hands out for a repeating wait.
Package clock — hosts manualTimer, the Timer handle a ManualClock hands out for a one-shot wait.
Package clock — hosts manualWait, the record a ManualClock keeps for one armed wait, plus the three lock-free helpers that operate on it.
Package clock — hosts the production implementation of the time port: systemClock, a zero-sized value delegating every method to package time, and the System singleton every consumer defaults to.
Package clock — hosts the Ticker contract, the handle a Waiter returns for a repeating wake-up.
Package clock — hosts the Timer contract, the handle a Waiter returns for a one-shot wake-up.
Package clock — hosts the waiting half of the time port: the Waiter contract, plus the single guard both implementations funnel their non-positive-period refusal through.
Index ¶
- type Clock
- type ManualClock
- func (m *ManualClock) Advance(d time.Duration)
- func (m *ManualClock) After(d time.Duration) <-chan time.Time
- func (m *ManualClock) BlockUntil(n int)
- func (m *ManualClock) NewTicker(d time.Duration) Ticker
- func (m *ManualClock) NewTimer(d time.Duration) Timer
- func (m *ManualClock) Now() time.Time
- func (m *ManualClock) Pending() int
- func (m *ManualClock) Set(t time.Time)
- func (m *ManualClock) Since(t time.Time) time.Duration
- func (m *ManualClock) Sleep(d time.Duration)
- type Ticker
- type Timed
- type Timer
- type Waiter
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 instant.
Now() time.Time
// Since returns the elapsed duration between the given instant and Now.
Since(t time.Time) time.Duration
}
Clock produces timestamps and durations. Implementations must be safe for concurrent use by multiple goroutines.
This interface is DELIBERATELY not extended: see the package comment. New time capabilities land on Waiter and are consumed through Timed.
type ManualClock ¶ added in v0.1.27
type ManualClock struct {
// contains filtered or unexported fields
}
ManualClock is a Timed whose instant moves ONLY when the caller moves it, with ManualClock.Advance or ManualClock.Set. Every wait it hands out — After, NewTimer, NewTicker, Sleep — is due at an absolute instant on this clock, and fires exactly when the caller drives the clock past that instant. No wall-clock time passes, so a test asserts cadence and ordering instead of sleeping and hoping.
It is a concrete struct, not an interface, for the same reason recycler.Pool and snapshot.Value are: there is one implementation, and a single-impl interface would be over-abstraction. Always used behind the pointer returned by NewManualClock — the embedded mutex and cond must not be copied.
Safe for concurrent use: every field is guarded by mu, and each wait's channel is created once at registration and never reassigned.
Two hazards, both inherent to a clock that only a caller can move:
- ManualClock.Sleep blocks until ANOTHER goroutine advances the clock. A test that sleeps on its own goroutine with nobody to advance hangs until the test binary's timeout. Sleep on the code under test, advance from the test.
- Advance wakes a waiter but does not schedule it. When the assertion depends on the woken goroutine having run, synchronise on something the goroutine itself signals; ManualClock.BlockUntil closes the mirror race (advancing before the goroutine has registered its wait).
func NewManualClock ¶ added in v0.1.27
func NewManualClock(start time.Time) *ManualClock
NewManualClock returns a ManualClock reading start. Any instant is legal — before the Unix epoch, after 2038, in any location — which is the point: unlike testing/synctest's bubble clock, the origin is the caller's choice.
func (*ManualClock) Advance ¶ added in v0.1.27
func (m *ManualClock) Advance(d time.Duration)
Advance moves the clock forward by d, firing every wait that comes due, in deadline order. It panics on a negative d: rewinding is ManualClock.Set's job and would otherwise silently mean "un-fire the timers I already fired".
Each fired wait receives its own deadline, not the post-advance instant, so a receiver reads the moment it was scheduled for. A ticker delivers AT MOST ONE tick per Advance and re-arms past the new instant: the delivery channel holds one tick anyway, and the alternative — one send per elapsed period — makes Advance(time.Hour) on a nanosecond ticker an unbounded loop. To observe N ticks, call Advance(period) N times, draining between calls.
func (*ManualClock) After ¶ added in v0.1.27
func (m *ManualClock) After(d time.Duration) <-chan time.Time
After returns a channel delivering the fire instant once d has elapsed on this clock. A non-positive d delivers before After returns.
func (*ManualClock) BlockUntil ¶ added in v0.1.27
func (m *ManualClock) BlockUntil(n int)
BlockUntil blocks until at least n waits are armed. It closes the race a manual clock otherwise has with the code it drives: the test would advance before the goroutine under test had registered its timer, and the wake would be lost. It is the manual counterpart of synctest.Wait — coarser, because it counts registrations instead of detecting durable blocking, but usable in a plain parallel test.
It blocks forever if the count is never reached; bound the test with the binary's own timeout rather than an arbitrary one here.
func (*ManualClock) NewTicker ¶ added in v0.1.27
func (m *ManualClock) NewTicker(d time.Duration) Ticker
NewTicker returns a repeating Ticker of period d, panicking on a non-positive d with the same message System uses.
func (*ManualClock) NewTimer ¶ added in v0.1.27
func (m *ManualClock) NewTimer(d time.Duration) Timer
NewTimer returns a one-shot Timer due d from the clock's current instant. A non-positive d is already elapsed and fires before NewTimer returns.
func (*ManualClock) Now ¶ added in v0.1.27
func (m *ManualClock) Now() time.Time
Now returns the clock's current instant.
func (*ManualClock) Pending ¶ added in v0.1.27
func (m *ManualClock) Pending() int
Pending returns the number of armed waits. A fired one-shot timer is no longer pending; a stopped timer or ticker is not pending; a live ticker is.
func (*ManualClock) Set ¶ added in v0.1.27
func (m *ManualClock) Set(t time.Time)
Set moves the clock to t. Forward, it behaves exactly like Advance and fires every wait that comes due. Backward, it moves the instant and fires nothing: deadlines are absolute, so a rewound clock re-reaches them and the waits fire then. That models a wall-clock jump — an NTP step, a VM restore — which is something testing/synctest's monotonic bubble clock cannot express.
func (*ManualClock) Since ¶ added in v0.1.27
func (m *ManualClock) Since(t time.Time) time.Duration
Since returns the duration between t and the clock's current instant. It can be negative when t is in the clock's future, exactly like time.Since.
func (*ManualClock) Sleep ¶ added in v0.1.27
func (m *ManualClock) Sleep(d time.Duration)
Sleep blocks the calling goroutine until d has elapsed on this clock — which means until another goroutine advances it. A non-positive d returns at once.
type Ticker ¶ added in v0.1.27
type Ticker interface {
// C returns the channel on which each tick instant is delivered. The same
// channel is returned by every call; it is never closed.
C() <-chan time.Time
// Stop halts the Ticker. It does NOT close the channel, so a concurrent
// receiver never sees a spurious zero value, and after Stop returns no
// tick from before the call is received — the guarantee time.Ticker.Stop
// makes since Go 1.23, whose channel is synchronous.
Stop()
// Reset halts the Ticker and restarts it with period d: no tick from
// before the call is received after it, and the next arrives once d has
// elapsed. It PANICS on a non-positive d; see the package comment.
Reset(d time.Duration)
}
Ticker is a repeating wake-up scheduled on some clock. It mirrors the usable surface of *time.Ticker — including its drop-on-slow-receiver contract: the delivery channel holds at most one pending tick, and ticks that cannot be delivered are discarded rather than queued.
Implementations must be safe for concurrent use by multiple goroutines.
type Timed ¶ added in v0.1.27
Timed is a Clock that also drives waiting. It is the interface production code asks for when it needs both halves — a scheduler, a timeout, a retry backoff — and the one both System and ManualClock satisfy.
type Timer ¶ added in v0.1.27
type Timer interface {
// C returns the channel on which the fire instant is delivered. The same
// channel is returned by every call; it is never closed.
C() <-chan time.Time
// Stop prevents a not-yet-fired Timer from firing and reports whether the
// Timer was still armed. After Stop returns, no value from a previous
// arming is left pending on C.
Stop() bool
// Reset re-arms the Timer to fire d from the clock's current instant and
// reports whether it was still armed. After Reset returns, no value from a
// previous arming is left pending on C. A non-positive d fires at once.
Reset(d time.Duration) bool
}
Timer is a one-shot wake-up scheduled on some clock. It mirrors the usable surface of *time.Timer, with one unavoidable difference: the stdlib exposes the delivery channel as a FIELD (t.C), and no interface can require a field, so it is a method here.
Implementations must be safe for concurrent use by multiple goroutines.
type Waiter ¶ added in v0.1.27
type Waiter interface {
// After returns a channel that delivers the fire instant once d has
// elapsed. A non-positive d delivers at once.
After(d time.Duration) <-chan time.Time
// NewTimer returns a one-shot [Timer] due d from now. A non-positive d
// fires at once.
NewTimer(d time.Duration) Timer
// NewTicker returns a repeating [Ticker] of period d. It PANICS on a
// non-positive d; see the package comment.
NewTicker(d time.Duration) Ticker
// Sleep blocks the calling goroutine until d has elapsed on this clock.
// A non-positive d returns at once.
Sleep(d time.Duration)
}
Waiter suspends a caller until a duration has elapsed on some clock. It is the half of the time abstraction that Clock deliberately does not carry.
Implementations must be safe for concurrent use by multiple goroutines.