Skip to content

Add UUIDv7 restart frontier state#79

Merged
dexcompiler merged 4 commits into
mainfrom
codex/69-uuidv7-frontier-state
May 15, 2026
Merged

Add UUIDv7 restart frontier state#79
dexcompiler merged 4 commits into
mainfrom
codex/69-uuidv7-frontier-state

Conversation

@dexcompiler
Copy link
Copy Markdown
Owner

Summary

  • add opt-in UuidV7FactoryState snapshots with compact 8-byte canonical encoding
  • add restored-state constructors and RestoreState/GetState APIs that only advance the UUIDv7 logical frontier
  • document restart-state invariants, crash-window semantics, and multi-writer limits

Correctness / performance notes

  • restored factories never allocate below the persisted logical frontier
  • unsigned packed-state ordering preserves correctness across the full 48-bit UUIDv7 timestamp domain
  • the default NewGuid hot path stays unchanged for users that do not opt in

Validation

  • dotnet test tests/Clockworks.Tests.csproj --filter UuidV7FactoryStateTests -v minimal
  • dotnet test property-tests/Clockworks.PropertyTests.fsproj --filter FullyQualifiedName~UuidV7FactoryProperties -v minimal
  • dotnet test Clockworks.sln -v minimal -m:1
  • npm run docs:build
  • dotnet pack src/Clockworks.csproj -c Release -v minimal

Fixes #69

@dexcompiler
Copy link
Copy Markdown
Owner Author

@copilot review this pr

Copy link
Copy Markdown
Contributor

Copilot AI commented May 15, 2026

@copilot review this pr

PR Review: Add UUIDv7 restart frontier state

Overall this is well-designed and the core logic is correct. All 227 tests pass (152 unit + 75 property). Here's a detailed review:


✅ Correctness — looks good

Unsigned packed-state arithmetic (PackState/UnpackState/IsPackedAfter): The refactor to ulong internally is the right call. The old signed-shift approach would break for timestamps at or above bit 47 (roughly year 10895), where the MSB of the 48-bit field would set the sign bit of the 64-bit packed long, making CAS comparisons incorrect. The new code handles the full UUIDv7 timestamp domain. The test RestoreState_HandlesPackedTimestampSignBitBoundary validates this specific edge case — good.

RestoreState CAS-max loop: Correctly only advances the frontier. The loop re-reads currentPacked on CAS failure before re-evaluating IsPackedAfter, so concurrent writers that advance past the target won't cause a spurious write-back. ✓

Constructor state seeding: When restoredState.TimestampMs >= initialTimestamp, the factory seeds from (state.TimestampMs, state.Counter), and AllocateTimestampAndCounter then increments the counter on the first call. Tests confirm the first generated UUID lands at state.Counter + 1. ✓

Removed constants: TimestampMask and CounterMask were defined but never referenced in the original file — dead code. Their removal is clean.


⚠️ Minor issues

1. Property test modulo boundary gap (UuidV7FactoryProperties.fs, line 243):

let safeCounter = counter % UuidV7FactoryState.MaxCounter

MaxCounter % MaxCounter = 0, so the MaxCounter value (4095) is never exercised as a property input. The boundary is tested explicitly in UuidV7FactoryStateTests, but for completeness this should be counter % (UuidV7FactoryState.MaxCounter + 1us).

2. WriteTo inconsistency (UuidV7FactoryState.cs, lines 86-92): The 6-byte timestamp is written byte-by-byte while the 2-byte counter uses BinaryPrimitives.WriteUInt16BigEndian. Consider BinaryPrimitives.WriteInt64BigEndian into a temp span and slicing, or at least a comment explaining the manual path (e.g., because long is 8 bytes and you only want 6).

3. GetState() missing <returns> XML doc tag — the summary describes what the method does but there's no <returns> on the returned UuidV7FactoryState.

4. GetState/RestoreState are not on IUuidV7Factory — if consumers depend on the interface rather than the concrete type they cannot access these methods. Intentional? If so, worth a doc comment noting the concrete type is required for checkpointing.


📋 Pre-existing (out of scope)

The clock-rollback counter overflow path in AllocateTimestampAndCounter always uses IncrementTimestamp semantics regardless of _effectiveOverflowBehavior. This was present before this PR — not introduced here, but worth a follow-up issue.


Summary

No blocking issues. The two items I'd suggest actioning before merge are the property test modulo fix and the WriteTo consistency note. The rest are documentation polish.

@dexcompiler
Copy link
Copy Markdown
Owner Author

Review verification update:

  • Accepted the property-test boundary note. The F# property now generates the full 0..4095 counter range and asserts the real invariant, generated_frontier > restored_frontier, instead of assuming counter + 1 is always representable in the same millisecond.
  • Kept the manual six-byte timestamp writes in UuidV7FactoryState.WriteTo. That is the lower-allocation path here: BinaryPrimitives.WriteInt64BigEndian would require an 8-byte temporary or extra shifting/slicing work for a 48-bit field. I added a short code comment to make that intentional.
  • Added the missing <returns> XML doc on GetState().
  • Documented that checkpointing is intentionally exposed on the concrete UuidV7Factory, while IUuidV7Factory remains generation-only so alternate implementations are not forced into this persistence model.

I also verified the adjacent rollback/drift overflow concern. It predates this PR and needs a deliberate semantics decision because honoring SpinWait while logical time is far ahead of physical time can become a long wait, while the current behavior favors availability and monotonicity. I opened #80 to track that separately with tests/docs scope.

Validation after the review-response commit:

  • dotnet test Clockworks.sln -v minimal -m:1
  • npm run docs:build
  • dotnet pack src/Clockworks.csproj -c Release -v minimal

@dexcompiler dexcompiler merged commit f4b7c1c into main May 15, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: add optional persistent logical frontier for UuidV7Factory restarts

2 participants