Clockworks is a .NET library for deterministic, fully controllable time in distributed-system simulations and tests.
It is built around TimeProvider so that time becomes an injectable dependency you can control (including timers/timeouts), while also providing time-ordered identifiers and causal timestamps.
-
Deterministic
TimeProviderSimulatedTimeProviderwith controllable wall time (SetUtcNow, etc.)- Monotonic scheduler time that advances only via
Advance(...) - Deterministic timer ordering and predictable periodic behavior
-
TimeProvider-driven timeoutsTimeouts.CreateTimeout(...)for aCancellationTokenSourcecancelled by the providerTimeouts.CreateTimeoutHandle(...)for a disposable handle that ties timer lifetime to disposal
-
UUIDv7 generation
UuidV7Factoryproduces RFC 9562 UUIDv7 values asGuid- Works with real or simulated time
- Configurable counter overflow behavior
- Optional
rand_bnode partitioning for distributed fleets with assigned node/shard IDs - Optional restart frontier state for services that persist and restore the UUIDv7 logical cursor
- Optional statistics for rollback, overflow, spin-wait, contention, and random-buffer refill diagnostics
- Per-instance monotonicity under clock rollback; cross-factory uniqueness remains probabilistic unless coordinated externally
-
Hybrid Logical Clock (HLC)
- HLC timestamps and utilities to preserve causality in distributed simulations
- Helpers to witness remote timestamps and generate outbound timestamps
- Provides both a canonical 10-byte big-endian encoding (
HlcTimestamp.WriteTo/ReadFrom) and an optimized packed 64-bit encoding (ToPackedInt64/FromPackedInt64)
-
Vector Clock
- Full vector clock implementation for exact causality tracking and concurrency detection
- Sorted-array representation optimized for sparse clocks
VectorClockCoordinatorfor thread-safe clock management across distributed nodes (allocation-conscious hot path)- Canonical binary wire format (
VectorClock.WriteTo/ReadFrom) and string form for HTTP/gRPC headers
-
Lightweight instrumentation
- Counters for timers, advances, timeouts, and UUIDv7 factory behavior useful in simulation/test assertions
dotnet add package Clockworksvar tp = new SimulatedTimeProvider();
var fired = 0;
using var timer = tp.CreateTimer(_ => fired++, null, TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
tp.Advance(TimeSpan.FromSeconds(1));
// fired == 1var tp = new SimulatedTimeProvider();
using var timeout = Timeouts.CreateTimeoutHandle(tp, TimeSpan.FromSeconds(5));
tp.Advance(TimeSpan.FromSeconds(5));
// timeout.Token.IsCancellationRequested == truevar factory = new UuidV7Factory(TimeProvider.System);
var id = factory.NewGuid();For production services, prefer one shared UuidV7Factory instance per process. Its monotonic (timestamp, counter) allocation is deterministic within that live factory, including when wall time moves backwards. Independent factories and multi-node fleets do not share that logical frontier; global uniqueness remains probabilistic and should be backed by storage uniqueness constraints where collisions are unacceptable. Services that need restart-aware monotonicity can persist UuidV7FactoryState from GetState() and restore it into a later factory.
UuidV7Factory owns a cryptographically secure RNG by default. Custom deterministic RNGs are useful for replayable tests and simulations, but identical deterministic RNG state plus identical time and call patterns can intentionally reproduce the same UUID sequence. Do not use seeded or deterministic RNGs for production UUID issuance.
// Create coordinators for two nodes
var nodeA = new VectorClockCoordinator(nodeId: 1);
var nodeB = new VectorClockCoordinator(nodeId: 2);
// Node A sends a message
var clockA = nodeA.BeforeSend();
// Node B receives the message
nodeB.BeforeReceive(clockA);
// Node B sends a reply
var clockB = nodeB.BeforeSend();
// Verify causality
Console.WriteLine(clockA.HappensBefore(clockB)); // true
// Propagate via HTTP headers
var header = new VectorClockMessageHeader(
clock: clockA,
correlationId: Guid.NewGuid()
);
var headerString = header.ToString(); // "1:1;{correlationId:N}" (example)In Clockworks, a "remote timestamp" is the HlcTimestamp produced on a different node and carried over the wire
via HlcMessageHeader (format: walltime.counter@node[;correlationId;causationId]). The receiver should call BeforeReceive(...) with that
timestamp to preserve causality.
Note: HlcTimestamp.ToPackedInt64()/FromPackedInt64() is an optimization encoding with a 48-bit wall time and a 4-bit node id (node id is truncated). Use WriteTo/ReadFrom when you need a full-fidelity representation.
var tp = new SimulatedTimeProvider();
using var aFactory = new HlcGuidFactory(tp, nodeId: 1);
using var bFactory = new HlcGuidFactory(tp, nodeId: 2);
var a = new HlcCoordinator(aFactory);
var b = new HlcCoordinator(bFactory);
// A sends
var t1 = a.BeforeSend();
var header = new HlcMessageHeader(t1, correlationId: Guid.NewGuid());
var headerValue = header.ToString();
// B receives
var parsed = HlcMessageHeader.Parse(headerValue);
b.BeforeReceive(parsed.Timestamp);
// B sends after receiving; should be > received timestamp
var t2 = b.BeforeSend();
Console.WriteLine(t1 < t2); // trueWhen using HlcGuidFactory, nodeId is encoded into generated UUIDv7 values as a 14-bit field. Use values in 0..HlcGuidFactory.MaxNodeId (0..16383); larger values are rejected because the UUID variant bits leave only 14 recoverable node-id bits.
HLC provides causality tracking that stays close to physical time, with a configurable maximum drift (HlcOptions.MaxDriftMs).
When ThrowOnExcessiveDrift=true, drift is enforced by throwing HlcDriftException once the bound is exceeded.
Best for:
- Systems where wall-clock time matters (e.g., trading systems with time-based SLAs)
- High-throughput systems where O(1) overhead is critical
- Scenarios where approximate causality is sufficient
Trade-offs:
- ✅ O(1) space and time complexity
- ✅ Stays close to physical time (bounded drift enforcement is configurable)
- ❌ Cannot detect concurrency (only ordering)
- ❌ Requires synchronized physical clocks for best results
Example: strict vs. high-throughput drift behavior
var tp = TimeProvider.System;
// Strict: enforce bounded drift by throwing
using var strict = new HlcGuidFactory(tp, nodeId: 1, options: new HlcOptions
{
MaxDriftMs = 1_000,
ThrowOnExcessiveDrift = true
});
// High-throughput: allow drift to exceed MaxDriftMs (maintains monotonicity)
using var highThroughput = new HlcGuidFactory(tp, nodeId: 1, options: new HlcOptions
{
MaxDriftMs = 60_000,
ThrowOnExcessiveDrift = false
});Vector clocks provide exact causality tracking and concurrency detection. Best for:
- Systems requiring precise happens-before relationships
- Conflict detection in replicated data stores
- Debugging distributed race conditions
- Academic/research scenarios
Trade-offs:
- ✅ Exact causality tracking
- ✅ Detects concurrent events (neither happened-before the other)
- ✅ No dependency on physical time
- ❌ O(n) space per clock (where n = number of nodes)
- ❌ O(n) time for merge and compare operations
- ❌ Metadata grows with cluster size
Example: Detecting concurrency
var coordA = new VectorClockCoordinator(1);
var coordB = new VectorClockCoordinator(2);
// Two nodes generate events independently (no message passing)
var clockA = coordA.BeforeSend();
var clockB = coordB.BeforeSend();
// Vector clocks detect they are concurrent
Console.WriteLine(clockA.IsConcurrentWith(clockB)); // true
// HLC would show one as "less than" the other based on physical timeWire formats
- Binary (canonical):
VectorClock.WriteTo/VectorClock.ReadFrom- Format:
[count:u32 big-endian][(nodeId:u16 big-endian, counter:u64 big-endian)]* ReadFromcanonicalizes unsorted input and deduplicates node IDs by taking the maximum counter.
- Format:
- String (for headers):
VectorClock.ToString()/VectorClock.Parse(...)("node:counter,node:counter", sorted by node id)
The demo/Clockworks.Demo project is a small CLI with multiple focused demos.
List demos:
dotnet run --project demo/Clockworks.Demo -- listRun a demo:
dotnet run --project demo/Clockworks.Demo -- uuidv7Useful ones to try:
# UUIDv7 sortability and time decoding
dotnet run --project demo/Clockworks.Demo -- uuidv7-sortability
# Fast-forwardable timeouts driven by simulated time
dotnet run --project demo/Clockworks.Demo -- timeouts
# Simulated timers, periodic coalescing, and scheduler statistics
dotnet run --project demo/Clockworks.Demo -- simulated-time
# Propagating HLC across service boundaries (header format)
dotnet run --project demo/Clockworks.Demo -- hlc-messaging
# BeforeSend/BeforeReceive workflow with coordinator stats
dotnet run --project demo/Clockworks.Demo -- hlc-coordinator
# Distributed simulation: at-least-once delivery + idempotency + HLC + vector clocks + timeouts
dotnet run --project demo/Clockworks.Demo -- ds-atleastonceThe uuidv7 demo also has an optional benchmark mode:
dotnet run --project demo/Clockworks.Demo -- uuidv7 --benchdemo/Clockworks.IntegrationDemo is a minimal ASP.NET Core app that demonstrates a realistic integration:
- SQLite-backed outbox + inbox (idempotency)
- An in-memory queued "transport" to keep the demo simple
- Clockworks HLC propagation (
HlcCoordinator+HlcMessageHeader) - A deterministic simulation using
SimulatedTimeProvider - Failure injection (drop/duplicate/reorder/delay)
Run it:
dotnet run --project demo/Clockworks.IntegrationDemoThen POST to /simulate (and watch the console trace):
# Default is simulated time mode
curl -X POST "http://localhost:5000/simulate"
# Run the same simulation under real wall clock time
curl -X POST "http://localhost:5000/simulate?mode=System"
# Tweak knobs
curl -X POST "http://localhost:5000/simulate?mode=Simulated&orders=10&tickMs=5&maxSteps=20000"- Target framework:
net10.0 - License: MIT
- Repository: https://github.com/dexcompiler/Clockworks
Clockworks follows Semantic Versioning (SemVer).
- Package version is defined in
src/Clockworks.csproj. - Release tags use the format
vX.Y.Z(example:v1.2.0). - See
CHANGELOG.mdfor notable changes. - CI runs on pushes/PRs to
mainvia.github/workflows/ci.yml. - Publishing is automated by
.github/workflows/release.yml:- Trigger by pushing a tag like
v1.3.1to publish to NuGet and create a GitHub Release. - Or run the workflow manually with
versioninput (the matchingvX.Y.Ztag must already exist).
- Trigger by pushing a tag like
Build-wide defaults are centralized in Directory.Build.props.
UUIDv7 values embed a millisecond-resolution timestamp by design (RFC 9562). As a result, any UUIDv7 generated by
UuidV7Factory can be decoded to reveal an approximate creation time, and ordering/rate information can sometimes be inferred
from sequences of IDs.
If you are issuing identifiers across untrusted/public boundaries (URLs, externally-visible resource IDs, third-party logs), do not treat UUIDv7 as opaque. Common mitigations are:
- Use a random UUID (e.g., UUIDv4) for externally-visible identifiers.
- Keep UUIDv7 as an internal primary key, and expose a separate opaque token externally.
- Wrap/encrypt identifiers for external presentation if you need internal ordering but external opacity.
Clockworks is designed so that advancing simulated scheduler time deterministically drives timers/timeouts. Wall time can be modified independently for clock-skew/rewind simulations.
Issues and PRs are welcome. Please include tests for behavioral changes.
Property-based tests live under property-tests/ and are implemented with FsCheck + xUnit.
See property-tests/README.md for how to run them and what invariants are covered.
