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
-
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, and timeouts useful in simulation/test assertions
var tp = new SimulatedTimeProvider(); var fired = 0; using var timer = tp.CreateTimer(_ => fired++, null, TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan); tp.Advance(TimeSpan.FromSeconds(1)); // fired == 1
var tp = new SimulatedTimeProvider(); using var timeout = Timeouts.CreateTimeoutHandle(tp, TimeSpan.FromSeconds(5)); tp.Advance(TimeSpan.FromSeconds(5)); // timeout.Token.IsCancellationRequested == true
var factory = new UuidV7Factory(TimeProvider.System); var id = factory.NewGuid();
// 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}" (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). 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); // true
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 time
Wire 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 -- list
Run a demo:
dotnet run --project demo/Clockworks.Demo -- uuidv7
Useful 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-atleastonce
The uuidv7 demo also has an optional benchmark mode:
dotnet run --project demo/Clockworks.Demo -- uuidv7 --bench
demo/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.IntegrationDemo
Then 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.
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.