Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
19 changes: 19 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,16 @@ 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, since the counter could
# be incremented, we are no more in an "overflow" state.
Comment thread
picnixz marked this conversation as resolved.
Outdated
#
# 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)
Comment thread
picnixz marked this conversation as resolved.
Outdated

def test_uuid8(self):
equal = self.assertEqual
u = self.uuid.uuid8()
Expand Down
26 changes: 24 additions & 2 deletions 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,23 +874,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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note: if we initialize _last_timestamp_v7 to a large negative value (e.g. -2**64), then we can remove the last_timestamp_v7 is None check.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just -1 and it's ok. You won't have a negative timestamp I think.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could (according to Claude when setting the system time to before 1970). But since the timestamp is generated via a int64_t the value -2**64 should be safe.

counter, tail = _uuid7_get_counter_and_tail()
_last_counter_v7_overflow = False
Comment thread
picnixz marked this conversation as resolved.
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:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first case from the if-else is the "latter" case in the comments above. Maybe swap the order in the comments.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right :') I changed so much this block that I forgot to update the comment. Thanks!

timestamp_ms = _last_timestamp_v7
else:
timestamp_ms = _last_timestamp_v7 + 1
Comment thread
picnixz marked this conversation as resolved.
# 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
Comment thread
picnixz marked this conversation as resolved.
timestamp_ms += 1
counter, tail = _uuid7_get_counter_and_tail()
else:
# 32-bit random data
_last_counter_v7_overflow = False
Comment thread
picnixz marked this conversation as resolved.
Outdated
tail = int.from_bytes(os.urandom(4))

unix_ts_ms = timestamp_ms & 0xffff_ffff_ffff
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