From b3fa2d07405e14517d25cbb0d3b40a7d4230192c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:53:41 +0200 Subject: [PATCH 1/8] gh-138862: fix timestamp increment after UUIDv7 counter overflow --- Lib/test/test_uuid.py | 19 +++++++++++++ Lib/uuid.py | 27 +++++++++++++++++-- ...-03-29-19-53-09.gh-issue-138862.kFhLY4.rst | 4 +++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-29-19-53-09.gh-issue-138862.kFhLY4.rst diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index 5f9ab048cdeb6c..e515f246eb1425 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,16 @@ def test_uuid7_overflow_counter(self): equal((u.int >> 32) & 0x3fff_ffff, new_counter_lo) equal(u.int & 0xffff_ffff, tail) + # Reflect the global state changes from the previous UUIDv7 call. + # Check that the timestamp of future UUIDs created within + # the same logical millisecond does not advance after the + # counter overflowed. + # + # See https://github.com/python/cpython/issues/138862. + v = self.uuid.uuid7() + equal(v.time, unix_ts_ms) + self.assertFalse(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..e4b6af953f94d5 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -832,6 +832,19 @@ 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 advanced after a counter overflow by design, +# we must prevent advancing the timestamp again in the calls that +# follow a call with a counter overflow and for which the logical +# timestamp millisecond is 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,23 +875,33 @@ 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() + _last_counter_v7_overflow = False else: if timestamp_ms < _last_timestamp_v7: - timestamp_ms = _last_timestamp_v7 + 1 + if _last_counter_v7_overflow: + # The clock went backward but RFC asks to update the timestamp + # and advance the previous counter. We however do not want to + # advance the timestamp again if we already advanced it once + # due to an overflow (re-use the already advanced timestamp). + 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: - # advance the 48-bit timestamp + _last_counter_v7_overflow = True timestamp_ms += 1 counter, tail = _uuid7_get_counter_and_tail() else: # 32-bit random data + _last_counter_v7_overflow = False tail = int.from_bytes(os.urandom(4)) unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff 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. From e60c1269cf87b19c646f7a1677c849f45c9d3f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:03:03 +0200 Subject: [PATCH 2/8] Update Lib/test/test_uuid.py --- Lib/test/test_uuid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index e515f246eb1425..f1429342c2342e 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1091,10 +1091,10 @@ def test_uuid7_overflow_counter(self): equal((u.int >> 32) & 0x3fff_ffff, new_counter_lo) equal(u.int & 0xffff_ffff, tail) - # Reflect the global state changes from the previous UUIDv7 call. # Check that the timestamp of future UUIDs created within # the same logical millisecond does not advance after the - # counter overflowed. + # counter overflowed. In addition, since the counter could + # be incremented, we are no more in an "overflow" state. # # See https://github.com/python/cpython/issues/138862. v = self.uuid.uuid7() From 299d7a51fb96a56def1a99112d25c2ae636366db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:04:02 +0200 Subject: [PATCH 3/8] Update Lib/uuid.py --- Lib/uuid.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index e4b6af953f94d5..2baa842c68c708 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -834,10 +834,9 @@ def uuid6(node=None, clock_seq=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 advanced after a counter overflow by design, -# we must prevent advancing the timestamp again in the calls that -# follow a call with a counter overflow and for which the logical -# timestamp millisecond is the same. +# 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. From 60877e4d9cd393228dbe3a72dca7c6fcbcd8532f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:08:05 +0200 Subject: [PATCH 4/8] Update Lib/uuid.py --- Lib/uuid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index 2baa842c68c708..ee2920e410f27c 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -884,11 +884,11 @@ def uuid7(): _last_counter_v7_overflow = False else: if timestamp_ms < _last_timestamp_v7: + # 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: - # The clock went backward but RFC asks to update the timestamp - # and advance the previous counter. We however do not want to - # advance the timestamp again if we already advanced it once - # due to an overflow (re-use the already advanced timestamp). timestamp_ms = _last_timestamp_v7 else: timestamp_ms = _last_timestamp_v7 + 1 From a6360d10797c580b3938c0287dba5ce914fc738a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:09:19 +0200 Subject: [PATCH 5/8] Update Lib/uuid.py --- Lib/uuid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/uuid.py b/Lib/uuid.py index ee2920e410f27c..ca5f693cd016d1 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -896,6 +896,7 @@ def uuid7(): 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() else: From cf1d6b4730894cbd7b094044d1ecf766b792d8e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:09:39 +0200 Subject: [PATCH 6/8] Update Lib/uuid.py --- Lib/uuid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/uuid.py b/Lib/uuid.py index ca5f693cd016d1..76fd115deab00b 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -900,8 +900,8 @@ def uuid7(): timestamp_ms += 1 counter, tail = _uuid7_get_counter_and_tail() else: - # 32-bit random data _last_counter_v7_overflow = False + # 32-bit random data tail = int.from_bytes(os.urandom(4)) unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff From 6611889e4191fb78348526cd3ccf82c4fcc871ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:40:58 +0200 Subject: [PATCH 7/8] Apply suggestions from code review --- Lib/test/test_uuid.py | 7 ++++--- Lib/uuid.py | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_uuid.py b/Lib/test/test_uuid.py index f1429342c2342e..2a99f43489f7bd 100755 --- a/Lib/test/test_uuid.py +++ b/Lib/test/test_uuid.py @@ -1093,13 +1093,14 @@ def test_uuid7_overflow_counter(self): # Check that the timestamp of future UUIDs created within # the same logical millisecond does not advance after the - # counter overflowed. In addition, since the counter could - # be incremented, we are no more in an "overflow" state. + # 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.assertFalse(self.uuid._last_counter_v7_overflow) + self.assertTrue(self.uuid._last_counter_v7_overflow) def test_uuid8(self): equal = self.assertEqual diff --git a/Lib/uuid.py b/Lib/uuid.py index 76fd115deab00b..36986f151046be 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -900,7 +900,6 @@ def uuid7(): timestamp_ms += 1 counter, tail = _uuid7_get_counter_and_tail() else: - _last_counter_v7_overflow = False # 32-bit random data tail = int.from_bytes(os.urandom(4)) From 10ae4c081d65cfd3a699fb3b8f4f0c239ed51916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Sun, 29 Mar 2026 21:42:58 +0200 Subject: [PATCH 8/8] Update Lib/uuid.py --- Lib/uuid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/uuid.py b/Lib/uuid.py index 36986f151046be..a0750326de831f 100644 --- a/Lib/uuid.py +++ b/Lib/uuid.py @@ -881,6 +881,7 @@ def uuid7(): 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: