clock

package
v9.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

Documentation

Overview

Package clock provides an injectable source of time so components that stamp, pace, or schedule work can be tested deterministically.

The Clock interface covers the three ways services consume time: reading it (Now, Since), pacing against it (Sleep, which is context-aware and never strands a goroutine past cancellation), and ticking on it (NewTicker). NewClock returns the production implementation backed by the time package.

Components should accept a Clock rather than calling time.Now or time.Sleep directly. Scheduling (cron, distributed coordination) is out of scope; in a multi-process system the shared database's clock, not this one, is the arbiter of ordering.

Testing time-dependent logic

There is no fake Clock, because testing/synctest makes one unnecessary. The wall Clock delegates to the time package on every call and caches nothing, so inside a bubble it rides the bubble's fake clock: a component under test keeps its production Clock, and the test moves time with time.Sleep. TTL expiry, backoff pacing, and periodic sweeps run in nanoseconds of wall time. See synctest_test.go, which pins this contract.

The bubble replaces the two things a hand-rolled fake provided. Advancing is time.Sleep in the test goroutine, or nothing at all — when every goroutine is durably blocked, time jumps to the next deadline on its own. Waiting for a goroutine to reach its sleep or ticker before advancing is synctest.Wait, which needs no count of registered waiters and returns without moving time.

That auto-advance is worth respecting, because it will happily rescue a broken test. A blocking receive on the result of the code under test — <-done, <-ticked — durably blocks the bubble, so time skips to whatever deadline comes next and the receive succeeds no matter how wrong the interval was. Such a test passes with the cadence set to an hour. Assert timing the other way instead: sleep to just short of the deadline, synctest.Wait (which parks everything without moving the clock), and check that nothing has happened; then step across the deadline, Wait again, and check with a non-blocking receive or a counter that exactly one thing did. Pair it with a deferred cancel or Close so a failed assertion unwinds the goroutines under test and reports itself instead of tripping the bubble's deadlock panic.

Two limits are worth knowing. A bubble's clock always starts at midnight UTC 2000-01-01, so a test wanting a particular timestamp derives it from time.Now inside the bubble. And Example functions cannot open a bubble — synctest.Test needs a *testing.T — so an example that must not be interrupted by a tick sets an interval longer than the example instead.

Tests that genuinely cannot run in a bubble — those blocking on real network or container I/O, which never counts as durably blocked — should use real durations against the real clock, as the integration suites do.

Example

Example wires a component to the production clock. Tests need no double: inside a testing/synctest bubble this same Clock reads the bubble's fake time, so time.Sleep moves the token to expiry in nanoseconds of wall time.

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/primandproper/platform-go/v9/clock"
)

// expiringToken is the shape components take: it stamps and checks against an
// injected Clock rather than calling time.Now directly, which is what lets a
// test drive it under testing/synctest without waiting on real time.
type expiringToken struct {
	clock     clock.Clock
	expiresAt time.Time
}

func newExpiringToken(c clock.Clock, ttl time.Duration) *expiringToken {
	return &expiringToken{clock: c, expiresAt: c.Now().Add(ttl)}
}

func (t *expiringToken) expired() bool {
	return !t.clock.Now().Before(t.expiresAt)
}

// Example wires a component to the production clock. Tests need no double:
// inside a testing/synctest bubble this same Clock reads the bubble's fake
// time, so time.Sleep moves the token to expiry in nanoseconds of wall time.
func main() {
	c := clock.NewClock()

	tok := newExpiringToken(c, time.Hour)
	fmt.Println("expired at issue:", tok.expired())

	// Sleep is context-aware: a canceled context ends the pause immediately
	// rather than stranding the goroutine for the full duration.
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	fmt.Println("sleep:", c.Sleep(ctx, time.Hour))

}
Output:
expired at issue: false
sleep: context canceled

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func RegisterClock

func RegisterClock(i do.Injector)

RegisterClock registers the wall Clock with the injector.

Types

type Clock

type Clock interface {
	// Now returns the current time.
	Now() time.Time

	// Since returns the time elapsed since t, per this clock's Now.
	Since(t time.Time) time.Duration

	// Sleep pauses the calling goroutine for d or until ctx is done,
	// whichever comes first, returning ctx.Err in the latter case. A
	// non-positive d does not sleep but still reports a done context, so a
	// pacing loop's cancellation check cannot be skipped by a zero delay.
	Sleep(ctx context.Context, d time.Duration) error

	// NewTicker returns a Ticker that delivers ticks every d. Like
	// time.NewTicker it panics if d is not positive, and slow receivers see
	// coalesced (dropped) ticks rather than a backlog. Callers must Stop the
	// ticker to release its resources.
	NewTicker(d time.Duration) Ticker
}

Clock is an injectable source of time. Production and test code alike receive the wall clock from NewClock: inside a testing/synctest bubble it reads the bubble's fake time, so no test double is needed. The interface deliberately covers only reading time, sleeping against it, and ticking on it — anything more (timers with resets, cron scheduling) belongs to the caller.

func NewClock

func NewClock() Clock

NewClock returns the wall Clock backed by the time package.

type Ticker

type Ticker interface {
	// Chan returns the channel on which ticks are delivered.
	Chan() <-chan time.Time

	// Stop turns off the ticker. As with *time.Ticker, Stop does not close
	// the channel.
	Stop()
}

Ticker delivers periodic ticks on a channel. It is the injectable counterpart of *time.Ticker, narrowed to the two members loops actually use.

Directories

Path Synopsis
Package clockmock provides moq-generated mock implementations of the clock package's interfaces.
Package clockmock provides moq-generated mock implementations of the clock package's interfaces.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL