clock

package module
v1.2.1 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2026 License: MIT Imports: 5 Imported by: 8

README

clock

CI CodeQL Documentation Go Reference Release Go License

clock is a small, production-oriented clock foundation for Go 1.27.0 and later. It keeps time.Time and time.Duration as public values, separates wall time from elapsed time, and provides deterministic timers, tickers, sleeps, and callbacks without changing the process-wide clock.

The module is a stable v1 public library. It owns explicit process-local time capabilities and deterministic test clocks; it does not own calendars, scheduling, distributed ordering, or a process-global clock.

Use the standard time package directly when no dependency seam is needed. Use testing/synctest when a complete test can live inside one fake-time bubble. Use this module when business timestamps, explicit wall jumps, package contracts, or selectively controlled time require dependency injection.

Install

go get github.com/faustbrian/go-clock@v1

The module has no runtime dependencies.

Five-minute quickstarts

Current-main variants are compiler-checked in example_test.go. The installed-v1 manual-clock quick start below is separately verified in a clean external module pinned to v1.0.0; it handles every construction and wait error and releases the clock explicitly.

System clock

Depend on only the capability an operation needs:

func stamp(clock interface{ Now() time.Time }) time.Time {
    return clock.Now()
}

createdAt := stamp(clock.System{})

System.Now returns time.Now() unchanged, including its location and process-local monotonic reading. System.Sleep owns and releases its timer when the context is canceled.

Fixed clock
fixed := manual.NewFixed(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC))
fmt.Println(fixed.Now().Format(time.RFC3339))
// 2026-01-02T03:04:05Z
Manual clock
start := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
manualClock, err := manual.New(start)
if err != nil {
    panic(err)
}
timer, err := manualClock.NewTimer(time.Minute)
if err != nil {
    panic(err)
}

waiter, err := manualClock.Advance(time.Minute)
if err != nil {
    panic(err)
}
if _, err := waiter.Wait(context.Background()); err != nil {
    panic(err)
}
fmt.Println((<-timer.C()).Format(time.RFC3339))
if err := manualClock.Shutdown(); err != nil {
    panic(err)
}
// 2026-01-02T03:05:05Z

Events fire by deadline and then registration order. A ticker has a one-value buffer and drops backpressured ticks. Always stop resources that remain active, and release the manual clock when its owner is done. The versioned quick start uses Shutdown because it is available across the complete v1 line; Close is the preferred additive name on current main, and Shutdown remains its deprecated exact delegation.

testing/synctest
clocktest.SystemBubble(t, func(t *testing.T, system clock.System) {
    started := system.Now()
    require.NoError(t, system.Sleep(t.Context(), time.Hour))
    require.Equal(t, time.Hour, system.Since(started))
})

The helper delegates fake time and goroutine quiescence to the standard library. It does not install another scheduler.

Capability map

Need Interface
Business timestamp Clock
Monotonic elapsed measurement ElapsedClock
Cancelable bounded delay Sleeper
Owned one-shot event TimerFactory and Timer
Owned periodic event TickerFactory and Ticker
Owned callback CallbackClock and Callback

FullClock is a convenience only. Libraries should accept the narrowest row that meets their contract.

Package map

All packages are released together from the root module and use root v<version> tags.

Import path Package Role
github.com/faustbrian/go-clock clock Public capabilities, standard-library implementation, and bounded observations
github.com/faustbrian/go-clock/manual manual Public fixed and explicitly advanced deterministic clocks
github.com/faustbrian/go-clock/clocktest clocktest Test-support bridge to testing/synctest

Construction, defaults, and validation

clock.System{} is ready without construction and delegates to the standard library. manual.NewFixed returns an immutable fixed wall clock. manual.New(start, options...) strips the start value's process-local monotonic reading and applies explicit resource limits before returning a concurrency-safe clock. Its defaults permit 65,536 scheduled objects, 65,536 outstanding advancement waiters, and 1,000,000 triggered events per advance; manual.WithLimits replaces those limits and rejects zero or negative values.

clock.Observe rejects nil clock and observer interfaces, a nil ObserverFunc, and invalid tags before creating its wrapper. Other non-nil dynamic values remain caller-owned collaborators. WithTags copies at most 16 non-empty-key tags with keys and values no longer than 64 bytes. A later WithTags option replaces an earlier one, and nil options are ignored. Constructors read no environment or filesystem configuration.

Semantics at a glance

  • Advance never accepts negative elapsed movement; use Jump for wall-clock rollback or forward correction.
  • Mark, SinceMark, and Measure use manual monotonic progress and are not affected by Jump.
  • Callbacks never run while an internal lock is held. They may create, stop, or reset work. A callback waiting for future work must issue and wait on a nested Advance; same-instant work wakes the active coordinator automatically.
  • Callback panics are recovered by the manual clock and counted without keeping the payload. The system clock retains standard time.AfterFunc panic policy.
  • Active objects and work per advancement are bounded. Invalid durations, overflow, closure, and exhausted budgets return documented errors.
  • Observers receive bounded lifecycle metadata, never callback values, panic payloads, contexts, or timestamps.
  • Sleep observations distinguish completed, deadline, canceled, and other failed outcomes while returning the exact base error.

Documentation

Use the versioned Golib ecosystem catalog and its Foundations family guidance to compare this module with related foundations and composition packages.

Development

Run make cohesion for the repository's design-language contract and make check for its package gates. See CONTRIBUTING.md for focused and release verification.

Scope

This module does not implement calendars, date-only values, timezone data, interval algebra, cron, scheduling, distributed ordering, or a timestamp oracle. calendar, temporal, scheduler, and lease own those concerns.

License

MIT. See LICENSE.

Documentation

Overview

Package clock provides narrow time capabilities backed by the standard library and deterministic implementations for tests. Accept the narrowest capability interface a consumer needs; System's zero value is the normal production implementation, while manual supplies explicit test-time control. Observe adds synchronous hooks without installing an exporter or registry.

The package does not own calendars, cron scheduling, distributed ordering, or process-global time. All implementations use Go time values; no cgo or external service is required.

Wall-clock timestamps and monotonic elapsed time are deliberately distinct. A time.Time returned by System.Now retains the process-local monotonic reading supplied by time.Now. Serialization removes that reading, so persisted values must never be used as a substitute for monotonic elapsed measurement.

Index

Examples

Constants

View Source
const (
	// MaxObservationTags bounds labels attached to one observation.
	MaxObservationTags = 16
	// MaxObservationTagBytes bounds each tag key and value.
	MaxObservationTagBytes = 64
)

Variables

View Source
var (
	// ErrInvalidDuration reports a duration that is not valid for an operation.
	ErrInvalidDuration = errors.New("clock: invalid duration")
	// ErrInvalidCallback reports a nil callback function.
	ErrInvalidCallback = errors.New("clock: invalid callback")
	// ErrOverflow reports unrepresentable duration arithmetic or exhaustion of
	// the manual clock's event registration sequence.
	ErrOverflow = errors.New("clock: duration overflow")
)
View Source
var (
	// ErrInvalidClock reports a nil clock passed to Observe.
	ErrInvalidClock = errors.New("clock: invalid clock")
	// ErrInvalidObserver reports a nil observer passed to Observe.
	ErrInvalidObserver = errors.New("clock: invalid observer")
	// ErrObservationTags reports tags outside the documented bounds.
	ErrObservationTags = errors.New("clock: invalid observation tags")
)

Functions

This section is empty.

Types

type Callback

type Callback interface {
	// Stop reports whether it prevented a scheduled invocation from starting.
	// It does not wait for an invocation already running.
	Stop() bool
	// Reset reports prior scheduling activity and schedules another invocation.
	// It does not wait for a running invocation, so calls can overlap.
	Reset(time.Duration) (bool, error)
}

Callback is an owned timer callback.

Stop reports whether it prevented the callback from starting. It does not wait for an already-started callback to finish.

type CallbackClock

type CallbackClock interface {
	// AfterFunc schedules a non-nil function and transfers callback ownership.
	// A nil function returns nil and ErrInvalidCallback; non-positive durations
	// are immediately due. Panic and execution policy depend on the backend.
	AfterFunc(time.Duration, func()) (Callback, error)
}

CallbackClock creates owned timer callbacks.

type Clock

type Clock interface {
	// Now returns the implementation's current wall timestamp.
	Now() time.Time
}

Clock obtains the current wall-clock time.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-clock/manual"
)

func main() {
	start := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC)
	manualClock, err := manual.New(start)
	if err != nil {
		panic(err)
	}
	timer, err := manualClock.NewTimer(time.Minute)
	if err != nil {
		panic(err)
	}
	waiter, err := manualClock.Advance(time.Minute)
	if err != nil {
		panic(err)
	}
	if _, err := waiter.Wait(context.Background()); err != nil {
		panic(err)
	}
	fmt.Println((<-timer.C()).Format(time.RFC3339))
	if err := manualClock.Close(); err != nil {
		panic(err)
	}
}
Output:
2026-01-02T03:05:05Z

type ElapsedClock

type ElapsedClock interface {
	// Since measures from start using the implementation's timestamp semantics.
	// Manual clocks cannot recover elapsed progress from a wall timestamp.
	Since(time.Time) time.Duration
	// Measure captures elapsed progress and returns a reusable measurement.
	// Fixed clocks always report zero; manual wall jumps do not affect it.
	Measure() func() time.Duration
}

ElapsedClock measures implementation-local elapsed time. Since accepts a time.Time with time.Time.Sub semantics, including wall subtraction when no compatible monotonic readings exist. Measure returns a closure tied to the implementation's monotonic source and is the safe choice across explicit manual wall-clock jumps.

type FullClock

FullClock is a convenience composition for consumers that genuinely need every capability. Consumers should normally accept a narrower interface.

func Observe

func Observe(base FullClock, observer Observer, options ...ObserveOption) (FullClock, error)

Observe decorates a FullClock with bounded synchronous lifecycle hooks. It starts no goroutine and owns no exporter or global registry. The caller still owns the base clock and its release; the wrapper does not expose base-specific methods such as manual.Clock.Close.

Nil interfaces and a nil ObserverFunc are rejected; other typed-nil dynamic values are not detected. Nil options are ignored. Clock validation failures return nil with ErrInvalidClock, ErrInvalidObserver or the option's error. Concurrent use requires a concurrency-safe base and observer.

Now, Since and Measure delegate without notifications. Timer/ticker channel receives are not observed. Sleep reports after return without changing its error; callbacks report after return or during panic unwinding and re-panic the original value. Callback observations retain the initial duration and measurement origin even after Reset; resets have their own observations.

Example
package main

import (
	"fmt"
	"time"

	clock "github.com/faustbrian/go-clock"
)

func main() {
	observed, err := clock.Observe(clock.System{}, clock.ObserverFunc(func(observation clock.Observation) {
		fmt.Println(observation.Kind, observation.Outcome)
	}))
	if err != nil {
		panic(err)
	}
	timer, err := observed.NewTimer(time.Hour)
	if err != nil {
		panic(err)
	}
	timer.Stop()
}
Output:
timer created
timer stopped

type Kind

type Kind string

Kind identifies an observed time resource.

const (
	// KindSleep identifies a context-aware sleep.
	KindSleep Kind = "sleep"
	// KindTimer identifies a one-shot channel timer.
	KindTimer Kind = "timer"
	// KindTicker identifies a periodic channel ticker.
	KindTicker Kind = "ticker"
	// KindCallback identifies an AfterFunc callback.
	KindCallback Kind = "callback"
)

type Observation

type Observation struct {
	// Kind identifies the operation or resource being observed.
	Kind Kind
	// Outcome identifies the transition, completion or rejection.
	Outcome Outcome
	// Requested is the duration supplied to creation, sleep or reset.
	// Stop observations have no requested duration and use zero.
	Requested time.Duration
	// Elapsed measures sleep or callback progress from the original call.
	// Resource creation, stop and reset observations leave it zero.
	Elapsed time.Duration
	// Tags is a fresh map per observation, or nil when no tags are configured.
	// The observer may retain or mutate it without affecting other observations.
	Tags map[string]string
}

Observation contains bounded lifecycle metadata. Clock-generated metadata excludes callback functions, panic payloads, timestamps and contexts. Tag contents are caller-supplied and are not redacted; never attach secrets.

type ObserveOption

type ObserveOption func(*observeConfig) error

ObserveOption configures an observed clock.

func WithTags

func WithTags(tags map[string]string) ObserveOption

WithTags validates and copies tags when Observe applies the option, then supplies a separate copy to each observation. Keys must be non-empty; at most MaxObservationTags entries and MaxObservationTagBytes bytes per key/value are accepted. Nil tags are valid. A later WithTags replaces an earlier one. Do not mutate the input concurrently with construction or put secrets in tags.

type Observer

type Observer interface {
	// Observe consumes one observation and may retain its independently owned tags.
	Observe(Observation)
}

Observer consumes lifecycle metadata synchronously. Implementations must return promptly and synchronize their own state: concurrent clock operations and callbacks may call Observe concurrently. Calls happen outside wrapper locks; reentrant observed operations produce nested notifications. Panics are isolated, but blocking an observer blocks the corresponding clock operation.

type ObserverFunc

type ObserverFunc func(Observation)

ObserverFunc adapts a function to Observer.

func (ObserverFunc) Observe

func (function ObserverFunc) Observe(observation Observation)

Observe calls function with observation synchronously. A nil function panics when called directly; the clock decorator recovers observer panics.

type Outcome

type Outcome string

Outcome identifies an observed lifecycle transition.

const (
	// OutcomeCreated reports successful resource creation.
	OutcomeCreated Outcome = "created"
	// OutcomeCompleted reports successful synchronous completion.
	OutcomeCompleted Outcome = "completed"
	// OutcomeCanceled reports context cancellation.
	OutcomeCanceled Outcome = "canceled"
	// OutcomeDeadline reports context deadline expiration.
	OutcomeDeadline Outcome = "deadline"
	// OutcomeFailed reports another synchronous operation failure.
	OutcomeFailed Outcome = "failed"
	// OutcomeStopped reports a successful active-to-stopped transition.
	OutcomeStopped Outcome = "stopped"
	// OutcomeInactive reports an operation on an inactive resource.
	OutcomeInactive Outcome = "inactive"
	// OutcomeReset reports successful rescheduling.
	OutcomeReset Outcome = "reset"
	// OutcomeFired reports normal callback return.
	OutcomeFired Outcome = "fired"
	// OutcomePanicked reports a callback panic without its payload.
	OutcomePanicked Outcome = "panicked"
	// OutcomeRejected reports validation or resource rejection.
	OutcomeRejected Outcome = "rejected"
)

type Sleeper

type Sleeper interface {
	// Sleep accepts a non-nil context and returns nil on completion or an error
	// on cancellation or resource failure. Non-positive durations need no wait.
	// Context cancellation does not stop other resources owned by the clock.
	Sleep(context.Context, time.Duration) error
}

Sleeper waits for a duration or until its context is canceled.

type System

type System struct{}

System delegates time operations to the Go standard library.

Its zero value is ready for concurrent use and owns no background resources.

Example
package main

import (
	"fmt"

	clock "github.com/faustbrian/go-clock"
)

func main() {
	var timestamps clock.Clock = clock.System{}
	_ = timestamps.Now()
	fmt.Println("system clock ready")
}
Output:
system clock ready

func (System) AfterFunc

func (System) AfterFunc(d time.Duration, fn func()) (Callback, error)

AfterFunc returns an owned wrapper around time.AfterFunc. A nil fn returns nil and ErrInvalidCallback. The function runs in a standard-library goroutine; its panic is not recovered. Stop/Reset do not wait for a running function, and resetting a started callback can schedule an overlapping invocation.

func (System) Measure

func (System) Measure() func() time.Duration

Measure captures the current standard-library monotonic reading and returns a closure that reports elapsed time from it.

func (System) NewTicker

func (System) NewTicker(d time.Duration) (Ticker, error)

NewTicker returns an owned wrapper around time.NewTicker. A non-positive period returns nil and ErrInvalidDuration instead of panicking.

func (System) NewTimer

func (System) NewTimer(d time.Duration) (Timer, error)

NewTimer returns an owned wrapper around time.NewTimer and a nil error. Non-positive durations are immediately due. Stop/Reset follow time.Timer.

func (System) Now

func (System) Now() time.Time

Now returns time.Now without changing its location or monotonic reading.

func (System) Since

func (System) Since(start time.Time) time.Duration

Since returns time.Since(start). When start contains a monotonic reading, the standard library uses it instead of rollback-prone wall time.

func (System) Sleep

func (System) Sleep(ctx context.Context, d time.Duration) error

Sleep waits for d or context cancellation. Non-positive durations complete immediately unless the context is already canceled. Its timer is always stopped on cancellation so the resource can be released promptly. A nil context panics. If completion and cancellation race, either may win.

type Ticker

type Ticker interface {
	// C returns tick timestamps; slow receivers may miss ticks.
	C() <-chan time.Time
	// Stop idempotently prevents future ticks without closing C.
	Stop()
	// Reset starts a new period from the current instant. Non-positive periods
	// return ErrInvalidDuration without changing the existing schedule.
	Reset(time.Duration) error
}

Ticker is an owned periodic time event.

The owner must call Stop. Ticks may be dropped for a slow receiver, matching time.Ticker. Reset returns ErrInvalidDuration for a non-positive duration instead of exposing the standard library's panic across an interface seam.

type TickerFactory

type TickerFactory interface {
	// NewTicker requires a positive period and transfers ownership on success.
	// Invalid periods return nil and ErrInvalidDuration.
	NewTicker(time.Duration) (Ticker, error)
}

TickerFactory creates owned periodic tickers.

type Timer

type Timer interface {
	// C returns the caller-owned receive side; callers must not close it.
	C() <-chan time.Time
	// Stop reports whether it stopped an active timer. It does not close C.
	Stop() bool
	// Reset reschedules from the current instant and reports prior activity.
	// Non-positive durations are immediately due. Rejected manual resets leave
	// the prior schedule/value intact; successful manual resets drain old values.
	Reset(time.Duration) (bool, error)
}

Timer is an owned, one-shot time event.

The owner must stop a timer it no longer needs. System uses the synchronous channel semantics of Go 1.23 and later; manual timers buffer one value and retain an already-delivered value after Stop. Implementations document their delivery and reset rules. A timer channel is not closed by Stop.

type TimerFactory

type TimerFactory interface {
	// NewTimer returns an owned timer, or nil and an error on failure.
	// Non-positive durations are immediately due rather than invalid.
	NewTimer(time.Duration) (Timer, error)
}

TimerFactory creates owned one-shot timers.

Directories

Path Synopsis
Package clocktest bridges clock capabilities to testing/synctest and manual advancement.
Package clocktest bridges clock capabilities to testing/synctest and manual advancement.
Package manual provides fixed timestamps and explicitly advanced test clocks.
Package manual provides fixed timestamps and explicitly advanced test clocks.

Jump to

Keyboard shortcuts

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