Skip to content

Commit b3fa2d0

Browse files
committed
gh-138862: fix timestamp increment after UUIDv7 counter overflow
1 parent 1efe441 commit b3fa2d0

File tree

3 files changed

+48
-2
lines changed

3 files changed

+48
-2
lines changed

Lib/test/test_uuid.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,7 @@ def test_uuid7_monotonicity(self):
950950
self.uuid,
951951
_last_timestamp_v7=0,
952952
_last_counter_v7=0,
953+
_last_counter_v7_overflow=False,
953954
):
954955
# 1 Jan 2023 12:34:56.123_456_789
955956
timestamp_ns = 1672533296_123_456_789 # ns precision
@@ -1024,6 +1025,7 @@ def test_uuid7_timestamp_backwards(self):
10241025
self.uuid,
10251026
_last_timestamp_v7=fake_last_timestamp_v7,
10261027
_last_counter_v7=counter,
1028+
_last_counter_v7_overflow=False,
10271029
),
10281030
mock.patch('time.time_ns', return_value=timestamp_ns),
10291031
mock.patch('os.urandom', return_value=tail_bytes) as urand
@@ -1049,9 +1051,13 @@ def test_uuid7_overflow_counter(self):
10491051
timestamp_ns = 1672533296_123_456_789 # ns precision
10501052
timestamp_ms, _ = divmod(timestamp_ns, 1_000_000)
10511053

1054+
# By design, counters have their MSB set to 0 so they
1055+
# will not be able to doubly overflow (they are still
1056+
# 42-bit integers).
10521057
new_counter_hi = random.getrandbits(11)
10531058
new_counter_lo = random.getrandbits(30)
10541059
new_counter = (new_counter_hi << 30) | new_counter_lo
1060+
new_counter &= 0x1ff_ffff_ffff
10551061

10561062
tail = random.getrandbits(32)
10571063
random_bits = (new_counter << 32) | tail
@@ -1063,11 +1069,14 @@ def test_uuid7_overflow_counter(self):
10631069
_last_timestamp_v7=timestamp_ms,
10641070
# same timestamp, but force an overflow on the counter
10651071
_last_counter_v7=0x3ff_ffff_ffff,
1072+
_last_counter_v7_overflow=False,
10661073
),
10671074
mock.patch('time.time_ns', return_value=timestamp_ns),
10681075
mock.patch('os.urandom', return_value=random_data) as urand
10691076
):
1077+
self.assertFalse(self.uuid._last_counter_v7_overflow)
10701078
u = self.uuid.uuid7()
1079+
self.assertTrue(self.uuid._last_counter_v7_overflow)
10711080
urand.assert_called_with(10)
10721081
equal(u.variant, self.uuid.RFC_4122)
10731082
equal(u.version, 7)
@@ -1082,6 +1091,16 @@ def test_uuid7_overflow_counter(self):
10821091
equal((u.int >> 32) & 0x3fff_ffff, new_counter_lo)
10831092
equal(u.int & 0xffff_ffff, tail)
10841093

1094+
# Reflect the global state changes from the previous UUIDv7 call.
1095+
# Check that the timestamp of future UUIDs created within
1096+
# the same logical millisecond does not advance after the
1097+
# counter overflowed.
1098+
#
1099+
# See https://github.com/python/cpython/issues/138862.
1100+
v = self.uuid.uuid7()
1101+
equal(v.time, unix_ts_ms)
1102+
self.assertFalse(self.uuid._last_counter_v7_overflow)
1103+
10851104
def test_uuid8(self):
10861105
equal = self.assertEqual
10871106
u = self.uuid.uuid8()

Lib/uuid.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -832,6 +832,19 @@ def uuid6(node=None, clock_seq=None):
832832

833833
_last_timestamp_v7 = None
834834
_last_counter_v7 = 0 # 42-bit counter
835+
# Indicate whether one or more counter overflow(s) happened in the same frame.
836+
#
837+
# Since the timestamp is advanced after a counter overflow by design,
838+
# we must prevent advancing the timestamp again in the calls that
839+
# follow a call with a counter overflow and for which the logical
840+
# timestamp millisecond is the same.
841+
#
842+
# If the resampled counter hits an overflow again within the same time,
843+
# we want to advance the timestamp again and resample the timestamp.
844+
#
845+
# See https://github.com/python/cpython/issues/138862.
846+
_last_counter_v7_overflow = False
847+
835848

836849
def _uuid7_get_counter_and_tail():
837850
rand = int.from_bytes(os.urandom(10))
@@ -862,23 +875,33 @@ def uuid7():
862875

863876
global _last_timestamp_v7
864877
global _last_counter_v7
878+
global _last_counter_v7_overflow
865879

866880
nanoseconds = time.time_ns()
867881
timestamp_ms = nanoseconds // 1_000_000
868882

869883
if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7:
870884
counter, tail = _uuid7_get_counter_and_tail()
885+
_last_counter_v7_overflow = False
871886
else:
872887
if timestamp_ms < _last_timestamp_v7:
873-
timestamp_ms = _last_timestamp_v7 + 1
888+
if _last_counter_v7_overflow:
889+
# The clock went backward but RFC asks to update the timestamp
890+
# and advance the previous counter. We however do not want to
891+
# advance the timestamp again if we already advanced it once
892+
# due to an overflow (re-use the already advanced timestamp).
893+
timestamp_ms = _last_timestamp_v7
894+
else:
895+
timestamp_ms = _last_timestamp_v7 + 1
874896
# advance the 42-bit counter
875897
counter = _last_counter_v7 + 1
876898
if counter > 0x3ff_ffff_ffff:
877-
# advance the 48-bit timestamp
899+
_last_counter_v7_overflow = True
878900
timestamp_ms += 1
879901
counter, tail = _uuid7_get_counter_and_tail()
880902
else:
881903
# 32-bit random data
904+
_last_counter_v7_overflow = False
882905
tail = int.from_bytes(os.urandom(4))
883906

884907
unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`uuid`: the timestamp of UUIDv7 objects generated within the same
2+
millisecond after encountering a counter overflow is only incremented once
3+
for the entire batch of UUIDv7 objects instead at each object creation.
4+
Patch by Bénédikt Tran.

0 commit comments

Comments
 (0)