Keep monotonic_ulid() monotonic when the clock does not move forward - #1641
Open
dylanpulver wants to merge 1 commit into
Open
Keep monotonic_ulid() monotonic when the clock does not move forward#1641dylanpulver wants to merge 1 commit into
dylanpulver wants to merge 1 commit into
Conversation
The increment branch only fired when now_ms == last_ms, so a timestamp reading at or below the previous one produced a fresh, smaller ULID. Match the reference JavaScript monotonicFactory, which tests seed <= lastTime and reuses the previous timestamp.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
monotonic_ulid()promises a ULID strictly larger than every other from the same process, and claims parity with the reference JavaScriptmonotonicFactory. The increment branch fires only onnow_ms == last_ms(llm/utils.py:746), so a reading at or below the previous timestamp falls through to_fresh()and yields a smaller ULID.Two ways that reading arrives:
time.time_ns()is not monotonic, so an NTP step or VM resume moves it.time.time_ns()is read outside_lock, so a thread holding the earlier millisecond can reach the lock after a thread holding the later one. The lock is there because concurrent callers are in scope.These IDs become conversation and turn ids in
logs.db, where the ULID sorts rows by creation.The fix tests
now_ms <= last_ms, matchingulid/javascriptsource/ulid.ts:161(if (seed <= lastTime)), which likewise reuses the previous timestamp rather than encoding the regressed one. Movingtime.time_ns()inside the lock closes the thread window but not clock regression.Residual: the guarantee stays per-process, and a regressed clock now draws randomness from the pinned millisecond, so a long regression reaches the existing
OverflowError.1107 tests pass locally against 1105 on main. Both new tests fail on
==and pass on<=;test_monotonic_ulidspasses either way. Black, ruff, mypy and cog are clean. No changelog entry, sincedocs/contributing.mdassigns that to the release process.