diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 5f9ab048cdeb6c..2a99f43489f7bd 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -950,6 +950,7 @@ def test_uuid7_monotonicity(self): self.uuid, _last_timestamp_v7=0, _last_counter_v7=0, + _last_counter_v7_overflow=False, ): # 1 Jan 2023 12:34:56.123_456_789 timestamp_ns = 1672533296_123_456_789 # ns precision @@ -1024,6 +1025,7 @@ def test_uuid7_timestamp_backwards(self): self.uuid, _last_timestamp_v7=fake_last_timestamp_v7, _last_counter_v7=counter, + _last_counter_v7_overflow=False, ), mock.patch('time.time_ns', return_value=timestamp_ns), mock.patch('os.urandom', return_value=tail_bytes) as urand @@ -1049,9 +1051,13 @@ def test_uuid7_overflow_counter(self): timestamp_ns = 1672533296_123_456_789 # ns precision timestamp_ms, _ = divmod(timestamp_ns, 1_000_000) + # By design, counters have their MSB set to 0 so they + # will not be able to doubly overflow (they are still + # 42-bit integers). new_counter_hi = random.getrandbits(11) new_counter_lo = random.getrandbits(30) new_counter = (new_counter_hi << 30) | new_counter_lo + new_counter &= 0x1ff_ffff_ffff tail = random.getrandbits(32) random_bits = (new_counter << 32) | tail @@ -1063,11 +1069,14 @@ def test_uuid7_overflow_counter(self): _last_timestamp_v7=timestamp_ms, # same timestamp, but force an overflow on the counter _last_counter_v7=0x3ff_ffff_ffff, + _last_counter_v7_overflow=False, ), mock.patch('time.time_ns', return_value=timestamp_ns), mock.patch('os.urandom', return_value=random_data) as urand ): + self.assertFalse(self.uuid._last_counter_v7_overflow) u = self.uuid.uuid7() + self.assertTrue(self.uuid._last_counter_v7_overflow) urand.assert_called_with(10) equal(u.variant, self.uuid.RFC_4122) equal(u.version, 7) @@ -1082,6 +1091,17 @@ def test_uuid7_overflow_counter(self): equal((u.int >> 32) & 0x3fff_ffff, new_counter_lo) equal(u.int & 0xffff_ffff, tail) + # Check that the timestamp of future UUIDs created within + # the same logical millisecond does not advance after the + # counter overflowed. In addition, even if the counter could + # be incremented, we are still in an "overflow" state as the + # timestamp should not be modified unless we re-overflow. + # + # See https://github.com/python/cpython/issues/138862. + v = self.uuid.uuid7() + equal(v.time, unix_ts_ms) + self.assertTrue(self.uuid._last_counter_v7_overflow) + def test_uuid8(self): equal = self.assertEqual u = self.uuid.uuid8() diff --git a/Lib/uuid.py b/Lib/uuid.py index c0150a59d7cb9a..a0750326de831f 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -832,6 +832,18 @@ def uuid6(node=None, clock_seq=None): _last_timestamp_v7 = None _last_counter_v7 = 0 # 42-bit counter +# Indicate whether one or more counter overflow(s) happened in the same frame. +# +# Since the timestamp is incremented after a counter overflow by design, +# we must prevent incrementing the timestamp again in consecutive calls +# for which the logical timestamp millisecond remains the same. +# +# If the resampled counter hits an overflow again within the same time, +# we want to advance the timestamp again and resample the timestamp. +# +# See https://github.com/python/cpython/issues/138862. +_last_counter_v7_overflow = False + def _uuid7_get_counter_and_tail(): rand = int.from_bytes(os.urandom(10)) @@ -862,18 +874,29 @@ def uuid7(): global _last_timestamp_v7 global _last_counter_v7 + global _last_counter_v7_overflow nanoseconds = time.time_ns() timestamp_ms = nanoseconds // 1_000_000 if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7: counter, tail = _uuid7_get_counter_and_tail() + # Clear the overflow state every new millisecond. + _last_counter_v7_overflow = False else: if timestamp_ms < _last_timestamp_v7: - timestamp_ms = _last_timestamp_v7 + 1 + # The clock went backwards or we are within the same timestamp + # after a counter overflow. We follow the RFC for in the former + # case. In the latter case, we re-use the already advanced + # timestamp (it was updated when we detected the overflow). + if _last_counter_v7_overflow: + timestamp_ms = _last_timestamp_v7 + else: + timestamp_ms = _last_timestamp_v7 + 1 # advance the 42-bit counter counter = _last_counter_v7 + 1 if counter > 0x3ff_ffff_ffff: + _last_counter_v7_overflow = True # advance the 48-bit timestamp timestamp_ms += 1 counter, tail = _uuid7_get_counter_and_tail() diff --git a/Misc/NEWS.d/next/Library/2026-03-29-19-53-09.gh-issue-138862.kFhLY4.rst b/Misc/NEWS.d/next/Library/2026-03-29-19-53-09.gh-issue-138862.kFhLY4.rst new file mode 100644 index 00000000000000..939cdd71f55510 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-29-19-53-09.gh-issue-138862.kFhLY4.rst @@ -0,0 +1,4 @@ +:mod:`uuid`: the timestamp of UUIDv7 objects generated within the same +millisecond after encountering a counter overflow is only incremented once +for the entire batch of UUIDv7 objects instead at each object creation. +Patch by Bénédikt Tran.