A Go library implementing the four major distributed-systems clock
primitives: Lamport, vector, hybrid logical, and bounded-uncertainty
(TrueTime-style), plus a proofs/ package that reproduces the exact
failure scenarios the original papers warn about.
Physical time is unreliable in distributed systems. Wall clocks drift, NTP corrections can step them backward, and two machines synchronized to the same NTP server still disagree by tens of milliseconds at any moment. Any system that timestamps events with raw wall time is one NTP correction away from producing wrong answers.
Each primitive in chrono solves a different version of this problem.
- Lamport clocks give you causal ordering with a single integer counter; enough to build distributed mutexes and ordered logs.
- Vector clocks add concurrency detection, which is what a leaderless KV store needs to surface conflicting writes instead of silently overwriting them.
- Hybrid logical clocks combine wall time with a logical counter, producing timestamps that are both causal and approximately meaningful to humans (the property CockroachDB, YugabyteDB, and MongoDB rely on).
- TrueTime-style bounded-uncertainty clocks return an interval containing true global time, which is what Spanner uses to achieve external consistency across regions.
When to use which: see docs/COMPARISON.md.
go get github.com/ani03sha/chronoGo 1.22 or newer. Core packages have no third-party dependencies; only
the tests use github.com/stretchr/testify.
import "github.com/ani03sha/chrono/lamport"
a := lamport.New()
b := lamport.New()
a.Tick() // a = 1 (internal event)
ts := a.Send() // a = 2; ts = 2 to attach to outgoing message
b.Receive(ts) // b = max(0, 2) + 1 = 3import "github.com/ani03sha/chrono/vector"
a := vector.New("a")
b := vector.New("b")
a.Tick() // a's vector: {a:1}
snap := a.Send() // a's vector: {a:2}; snap = {a:2}
b.Receive(snap) // b's vector: {a:2, b:1}
rel := vector.Compare(snap, b.Now())
// rel == vector.Beforeimport "github.com/ani03sha/chrono/dotted"
base := dotted.NewVersion()
fromA := base.Increment("clientA")
fromB := base.Increment("clientB")
fromA.Concurrent(fromB) // true — surface as siblings, do not overwriteimport (
"github.com/ani03sha/chrono/hlc"
"github.com/ani03sha/chrono/internal/wallclock"
)
clock := hlc.New(wallclock.Real())
ts := clock.Now() // hlc.Timestamp{WallTime: ..., Logical: 0}
remote := /* timestamp received over the wire */
merged, err := clock.Receive(remote)import (
"context"
"time"
"github.com/ani03sha/chrono/truetime"
)
src, _ := truetime.NewNTPSource("time.google.com", time.Minute)
src.Start(context.Background())
defer src.Stop()
clock := truetime.New(src)
interval := clock.Now() // [Earliest, Latest]
// External consistency: pick commit timestamp at Latest, then wait
// until the clock is provably past it before exposing the commit.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
commitAt := clock.Now().Latest
_ = truetime.CommitWait(ctx, clock, commitAt)This is what distinguishes chrono from a typical clock library. Every primitive's behavior is demonstrated against a specific scenario from the literature or from real production incidents:
| Test | Reproduces |
|---|---|
TestLamport1978Figure2 |
Lamport (1978) Figure 2 — three-process message ring with monotonic timestamps along every causal chain |
TestNTPStepCausesIncorrectOrderingWithWallClock |
Sorting events by raw wall time fails under an NTP backward step; the same sort by HLC stamps preserves real-time order |
TestSpannerExternalConsistency |
Corbett et al. (2012) — commit-wait on a TrueTime interval achieves external consistency |
TestDottedVersionDetectsConcurrentWrites |
Preguiça et al. (2010) — concurrent writes surface as siblings instead of being silently overwritten |
TestHLCPreservesCausalityAcrossSkewedNodes |
Kulkarni et al. (2014), as deployed in CockroachDB — HLC preserves causality across nodes whose wall clocks differ by tens of ms |
Run with make proofs (verbose) or go test -v ./proofs/. These are executable evidence, not unit tests — anyone skeptical of "this library handles X" can run the file that demonstrates X happening.
Hardware: Apple M3 Pro, Go 1.26.1, darwin/arm64. Full numbers in BENCHMARKS.md, generated by make bench.
| Operation | Target | Measured |
|---|---|---|
wallclock.Real.NowNanos (baseline) |
— | 30.1 ns/op, 0 allocs |
lamport.Tick |
< 50 ns/op, 0 allocs | 4.6 ns/op, 0 allocs |
lamport.Send |
< 50 ns/op, 0 allocs | 4.2 ns/op, 0 allocs |
lamport.Receive |
< 60 ns/op, 0 allocs | 4.0 ns/op, 0 allocs |
vector.Tick (3 nodes) |
< 200 ns/op | 132 ns/op, 2 allocs |
vector.Receive (10 nodes) |
< 1 µs/op | 442 ns/op, 4 allocs |
vector.Compare (10 nodes) |
< 200 ns/op | 298 ns/op, 0 allocs ⚠ |
vector.MarshalBinary (10 nodes) |
< 500 ns/op | 341 ns/op, 2 allocs |
vector.UnmarshalBinary (10 nodes) |
— | 257 ns/op, 14 allocs |
hlc.Now |
< 100 ns/op, 0 allocs | 40 ns/op, 0 allocs |
hlc.Send |
< 100 ns/op, 0 allocs | 38 ns/op, 0 allocs |
hlc.Receive |
< 150 ns/op, 0 allocs | 39 ns/op, 0 allocs |
hlc.MarshalBinary |
— | 8.8 ns/op, 1 alloc (16 B) |
truetime.Now (FakeSource) |
< 100 ns/op, 0 allocs | 8.8 ns/op, 0 allocs |
Notes:
vector.Compareexceeds its target. The algorithm performs ~20 map operations on 10-node maps; Go map access on M3 Pro is ~15ns each, putting the realistic floor near 300 ns/op. The original target was optimistic; the actual cost is honest.vector.UnmarshalBinary's 14 allocs/op come from per-nodestring(bytes)conversions plus map machinery. They could be eliminated withunsafe.String, but the safety trade isn't worth it for a teaching codebase.hlc.MarshalBinary's 16 B/op is Go's allocator rounding the 12-byte slice up to its minimum 16-byte size class.
| Clock | Total order | Causal | Wall-time | Use when |
|---|---|---|---|---|
| Lamport | No | Yes | No | basic happens-before; no node identity needed |
| Vector | No | Yes | No | detect concurrency between specific nodes |
| Dotted | No | Yes | No | leaderless replication with sibling resolution |
| HLC | Yes | Yes | Approx | timestamps that are both causal and human-readable |
| TrueTime | Yes | Yes | Bounded | external consistency (Spanner-class systems) |
Full discussion: docs/COMPARISON.md.
- Lamport: distributed mutexes (Ricart–Agrawala), various consensus protocols, log merging, foundational theory for everything else.
- Vector: Riak (pre-2.0), Voldemort, Amazon Dynamo (2007 paper).
- Dotted: Riak (2.0+), Antidote, various CRDT libraries.
- HLC: CockroachDB, YugabyteDB, MongoDB (cross-shard transactions since 4.0).
- TrueTime: Google Spanner.
Citations and links: docs/CITATIONS.md.
Before submitting a PR:
make test-racemust pass.make benchmust not regress relevant numbers (compare withbenchstat).- New behavior gets a unit test. New failure mode gets a
proofs/test.
Design rationale for non-obvious API choices: docs/DESIGN.md.
MIT. See LICENSE.