diff --git a/CHANGELOG.md b/CHANGELOG.md index 62163e4..5e5227b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 0d9acaf..2cee190 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/docs/changelog.md b/docs/changelog.md index 50ca4a8..4f80cb8 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -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 @@ -56,4 +72,3 @@ This page mirrors the repository root `CHANGELOG.md`. ### Build - Centralized common build properties in `Directory.Build.props`. - diff --git a/docs/guide/uuidv7.md b/docs/guide/uuidv7.md index c4f19e0..ab0a95a 100644 --- a/docs/guide/uuidv7.md +++ b/docs/guide/uuidv7.md @@ -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 diff --git a/src/Abstractions/IUuidV7Factory.cs b/src/Abstractions/IUuidV7Factory.cs index e1c0f51..04a2173 100644 --- a/src/Abstractions/IUuidV7Factory.cs +++ b/src/Abstractions/IUuidV7Factory.cs @@ -3,10 +3,15 @@ namespace Clockworks.Abstractions; /// /// Abstraction for UUIDv7 generation with time control. /// +/// +/// 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. +/// public interface IUuidV7Factory { /// - /// Creates a new UUIDv7. + /// Creates a new UUIDv7 value. /// Guid NewGuid(); @@ -17,8 +22,7 @@ public interface IUuidV7Factory (Guid Guid, long TimestampMs) NewGuidWithTimestamp(); /// - /// Batch generation for high-throughput scenarios. - /// More efficient than calling NewGuid() in a loop. + /// Batch generation for high-throughput scenarios. More efficient than calling in a loop. /// void NewGuids(Span destination); } diff --git a/src/Extensions.cs b/src/Extensions.cs index 9e4f5ab..1b3af62 100644 --- a/src/Extensions.cs +++ b/src/Extensions.cs @@ -15,7 +15,8 @@ public static class ServiceCollectionExtensions { /// /// 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. /// public IServiceCollection AddLockFreeGuidFactory( CounterOverflowBehavior overflowBehavior = CounterOverflowBehavior.SpinWait) @@ -31,7 +32,8 @@ public IServiceCollection AddLockFreeGuidFactory( /// /// 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. /// public IServiceCollection AddLockFreeGuidFactory( TimeProvider timeProvider, diff --git a/src/UuidV7Factory.cs b/src/UuidV7Factory.cs index c296567..8b16070 100644 --- a/src/UuidV7Factory.cs +++ b/src/UuidV7Factory.cs @@ -12,10 +12,19 @@ namespace Clockworks; /// /// Implements RFC 9562 UUID version 7 and returns values as 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 +/// +/// +/// Guarantee boundary: one live factory instance deterministically allocates unique, monotonically increasing +/// (timestamp, counter) pairs, including when the supplied 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. +/// /// /// /// UUIDv7 bit layout (RFC 9562): @@ -68,7 +77,8 @@ public sealed class UuidV7Factory : IUuidV7Factory, IDisposable /// Time source (use for production). /// /// Random number generator to use for the random portion of the UUID. If , 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. /// /// Behavior to apply when the per-millisecond counter overflows. public UuidV7Factory(