Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project aims to follow [Semantic Versioning](https://semver.org/spec/v2
### Changed
- `HlcGuidFactory` constructor now enforces a 14-bit node ID constraint and throws `ArgumentOutOfRangeException` for values above `HlcGuidFactory.MaxNodeId` (16383). Previously, higher values were silently truncated in generated UUIDv7 values.

### Documentation
- Clarify `UuidV7Factory` collision and clock-skew guarantees, including the distinction between per-instance deterministic monotonicity and probabilistic cross-factory uniqueness.

## [1.3.1] - 2026-04-02

### Build
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ It is built around `TimeProvider` so that *time becomes an injectable dependency
- `UuidV7Factory` produces RFC 9562 UUIDv7 values as `Guid`
- Works with real or simulated time
- Configurable counter overflow behavior
- 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
Expand Down Expand Up @@ -74,6 +75,8 @@ 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, restarts, 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.

### Vector Clock usage

```csharp
Expand Down
17 changes: 16 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,22 @@ This page mirrors the repository root `CHANGELOG.md`.

## [Unreleased]

### Changed
- `HlcGuidFactory` constructor now enforces a 14-bit node ID constraint and throws `ArgumentOutOfRangeException` for values above `HlcGuidFactory.MaxNodeId` (16383). Previously, higher values were silently truncated in generated UUIDv7 values.

### Documentation
- Clarify `UuidV7Factory` collision and clock-skew guarantees, including the distinction between per-instance deterministic monotonicity and probabilistic cross-factory uniqueness.

## [1.3.1] - 2026-04-02

### Build
- Add GitHub Actions CI workflow for pushes and pull requests to `main`.
- Add tag-driven release workflow to publish NuGet packages and create GitHub Releases.
- Remove `setup-dotnet` lock-file caching requirement from workflows (no `packages.lock.json` needed).

### Infrastructure
- Add a repository `commit-msg` hook under `.githooks/` to strip auto-injected `Co-authored-by` trailers for Copilot/dexcompiler identities.

## [1.3.0] - 2026-02-19

### Fixed
Expand Down Expand Up @@ -56,4 +72,3 @@ This page mirrors the repository root `CHANGELOG.md`.

### Build
- Centralized common build properties in `Directory.Build.props`.

21 changes: 19 additions & 2 deletions docs/guide/uuidv7.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,30 @@ Console.WriteLine(id2.IsVersion7()); // true/false

## Monotonicity Guarantees

Within a single millisecond, uniqueness and ordering are maintained by a **12-bit monotonic counter** appended to the 48-bit timestamp:
Within a single factory instance, ordering is maintained by a **12-bit monotonic counter** appended to the 48-bit timestamp:

- When physical time moves to a new millisecond, the counter is reset to a **random start value** (masked into the lower half of the counter space to leave room for increments).
- Each successive `NewGuid()` within the same millisecond increments the counter.
- If the wall clock moves backwards, the factory continues from the last logical `(timestamp, counter)` frontier instead of rewinding to the lower physical time.
- If the counter overflows (4,096 values exhausted), behavior depends on `CounterOverflowBehavior` (spin-wait, increment timestamp, or throw).

This guarantees strict monotonicity without locks.
This guarantees strict per-instance monotonicity without locks. The guarantee is scoped to the live factory instance, not to every factory in a fleet.

## Collision and Clock-Skew Semantics

`UuidV7Factory` separates local deterministic allocation from distributed probabilistic uniqueness:

| Scope | Guarantee |
|---|---|
| One live factory instance | Deterministic, lock-free allocation of unique and monotonically increasing `(timestamp, counter)` pairs. Backward wall-clock movement does not rewind the logical frontier. |
| Multiple threads sharing one factory | Same per-instance guarantee; CAS retries may occur under contention, but successful allocations do not reuse a pair. |
| Multiple factories in one process | No shared logical frontier. Full UUID collisions are still extremely unlikely with independent CSPRNG state, but uniqueness is probabilistic. |
| Multiple processes or machines | No built-in global coordination or node discriminator in `UuidV7Factory`. Clock skew can increase timestamp overlap, while uniqueness depends on randomized counter starts and the 62-bit random tail. |
| Process restart | The new factory starts from current wall time and a random counter start. It does not inherit the previous logical frontier. |

For production services, prefer a single `UuidV7Factory` singleton per process or service instance. The built-in DI helpers register it this way.

For high-assurance shared namespaces, use a storage uniqueness constraint as the final guardrail and retry on conflict. If you need node-aware ordering semantics, consider `HlcGuidFactory`; it embeds a node ID and HLC timestamp, but it should be chosen for causal/node-aware ordering rather than treated as a blanket substitute for storage-level uniqueness.

## Counter Overflow Behavior

Expand Down
10 changes: 7 additions & 3 deletions src/Abstractions/IUuidV7Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@ namespace Clockworks.Abstractions;
/// <summary>
/// Abstraction for UUIDv7 generation with time control.
/// </summary>
/// <remarks>
/// Implementations may provide deterministic monotonic allocation guarantees for a single factory instance. They do
/// not imply deterministic global uniqueness across independent factories, processes, or machines unless the
/// implementation explicitly documents such coordination.
/// </remarks>
public interface IUuidV7Factory
{
/// <summary>
/// Creates a new UUIDv7.
/// Creates a new UUIDv7 value.
/// </summary>
Guid NewGuid();

Expand All @@ -17,8 +22,7 @@ public interface IUuidV7Factory
(Guid Guid, long TimestampMs) NewGuidWithTimestamp();

/// <summary>
/// Batch generation for high-throughput scenarios.
/// More efficient than calling NewGuid() in a loop.
/// Batch generation for high-throughput scenarios. More efficient than calling <see cref="NewGuid"/> in a loop.
/// </summary>
void NewGuids(Span<Guid> destination);
}
6 changes: 4 additions & 2 deletions src/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the lock-free GUID factory with system time.
/// Use this for most production scenarios.
/// Use this for most production scenarios. Registers a singleton factory so per-instance monotonic state is
/// shared across callers in the process.
/// </summary>
public IServiceCollection AddLockFreeGuidFactory(
CounterOverflowBehavior overflowBehavior = CounterOverflowBehavior.SpinWait)
Expand All @@ -31,7 +32,8 @@ public IServiceCollection AddLockFreeGuidFactory(

/// <summary>
/// Adds the lock-free GUID factory with a custom TimeProvider.
/// Use this for testing or simulation.
/// Use this for testing or simulation. Registers a singleton factory so per-instance monotonic state is shared
/// across callers in the process.
/// </summary>
public IServiceCollection AddLockFreeGuidFactory(
TimeProvider timeProvider,
Expand Down
14 changes: 12 additions & 2 deletions src/UuidV7Factory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,19 @@ namespace Clockworks;
/// <remarks>
/// Implements RFC 9562 UUID version 7 and returns values as <see cref="Guid"/> with:
///
/// - Monotonic counter for sub-millisecond ordering
/// - Per-instance monotonic counter for sub-millisecond ordering
/// - Lock-free synchronization using CAS operations
/// - Configurable overflow behavior
/// - TimeProvider integration for testing/simulation
/// - Cryptographically secure random tail bytes by default
///
/// <para>
/// Guarantee boundary: one live factory instance deterministically allocates unique, monotonically increasing
/// <c>(timestamp, counter)</c> pairs, including when the supplied <see cref="TimeProvider"/> moves backwards. Global
/// uniqueness across independent factories, processes, restarts, or machines remains probabilistic and depends on
/// independent random tail bytes unless the caller adds coordination, node partitioning, or a storage uniqueness
/// constraint.
/// </para>
///
/// <para>
/// <b>UUIDv7 bit layout (RFC 9562):</b>
Expand Down Expand Up @@ -68,7 +77,8 @@ public sealed class UuidV7Factory : IUuidV7Factory, IDisposable
/// <param name="timeProvider">Time source (use <see cref="TimeProvider.System"/> for production).</param>
/// <param name="rng">
/// Random number generator to use for the random portion of the UUID. If <see langword="null"/>, a new
/// cryptographically-secure RNG is created and owned by this instance.
/// cryptographically-secure RNG is created and owned by this instance. Production deployments should use a
/// cryptographically strong RNG with independent state for each factory.
/// </param>
/// <param name="overflowBehavior">Behavior to apply when the per-millisecond counter overflows.</param>
public UuidV7Factory(
Expand Down
Loading