Skip to content

dexcompiler/Clockworks

Repository files navigation

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.

Features

  • Deterministic TimeProvider

    • SimulatedTimeProvider with controllable wall time (SetUtcNow, etc.)
    • Monotonic scheduler time that advances only via Advance(...)
    • Deterministic timer ordering and predictable periodic behavior
  • TimeProvider-driven timeouts

    • Timeouts.CreateTimeout(...) for a CancellationTokenSource cancelled by the provider
    • Timeouts.CreateTimeoutHandle(...) for a disposable handle that ties timer lifetime to disposal
  • UUIDv7 generation

    • UuidV7Factory produces RFC 9562 UUIDv7 values as Guid
    • Works with real or simulated time
    • Configurable counter overflow behavior
    • Optional rand_b node 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
    • VectorClockCoordinator for 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

Installation

dotnet add package Clockworks

Quick start

Deterministic timers with simulated time

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

TimeProvider-driven timeouts

var tp = new SimulatedTimeProvider();

using var timeout = Timeouts.CreateTimeoutHandle(tp, TimeSpan.FromSeconds(5));

tp.Advance(TimeSpan.FromSeconds(5));
// timeout.Token.IsCancellationRequested == true

UUIDv7 generation

var 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.

Vector Clock usage

// 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)

Hybrid Logical Clock (HLC) propagation

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); // true

When 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.

Distributed Systems Support

Hybrid Logical Clock (HLC)

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 Clock

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)]*
    • ReadFrom canonicalizes unsorted input and deduplicates node IDs by taking the maximum counter.
  • String (for headers): VectorClock.ToString() / VectorClock.Parse(...) ("node:counter,node:counter", sorted by node id)

Demos

Console demos (demo/Clockworks.Demo)

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

ASP.NET Core integration demo (demo/Clockworks.IntegrationDemo)

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"

Package

Versioning & releases

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.md for notable changes.
  • CI runs on pushes/PRs to main via .github/workflows/ci.yml.
  • Publishing is automated by .github/workflows/release.yml:
    • Trigger by pushing a tag like v1.3.1 to publish to NuGet and create a GitHub Release.
    • Or run the workflow manually with version input (the matching vX.Y.Z tag must already exist).

Build-wide defaults are centralized in Directory.Build.props.

Security considerations

UUIDv7 time exposure

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.

Notes on determinism

Clockworks is designed so that advancing simulated scheduler time deterministically drives timers/timeouts. Wall time can be modified independently for clock-skew/rewind simulations.

Contributing

Issues and PRs are welcome. Please include tests for behavioral changes.

Property tests

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.

About

Deterministic, fully controllable time and time-ordered identifiers for distributed-system simulations and testing. Time is just another dependency.

Topics

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors