Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Lib/test/test_uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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()
Expand Down
25 changes: 24 additions & 1 deletion Lib/uuid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading