From 1519e99f95d47784b12c7f9a26a3d7d391dd6da2 Mon Sep 17 00:00:00 2001 From: KodrAus Date: Mon, 13 May 2024 13:19:52 +1000 Subject: [PATCH 01/10] make counter width configurable --- src/lib.rs | 7 +--- src/timestamp.rs | 104 ++++++++++++++++++++++++++++++++--------------- src/v6.rs | 4 +- 3 files changed, 74 insertions(+), 41 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b5de83ca..0531b3e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -904,12 +904,7 @@ impl Uuid { let seconds = millis / 1000; let nanos = ((millis % 1000) * 1_000_000) as u32; - Some(Timestamp { - seconds, - nanos, - #[cfg(any(feature = "v1", feature = "v6"))] - counter: 0, - }) + Some(Timestamp::from_unix_time(seconds, nanos, 0)) } _ => None, } diff --git a/src/timestamp.rs b/src/timestamp.rs index 27112d17..de1bfe1e 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -39,10 +39,10 @@ pub const UUID_TICKS_BETWEEN_EPOCHS: u64 = 0x01B2_1DD2_1381_4000; /// * [Clock Sequence in RFC4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.5) #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Timestamp { - pub(crate) seconds: u64, - pub(crate) nanos: u32, - #[cfg(any(feature = "v1", feature = "v6"))] - pub(crate) counter: u16, + seconds: u64, + nanos: u32, + counter: u64, + usable_counter_bits: usize, } impl Timestamp { @@ -55,18 +55,32 @@ impl Timestamp { /// This method will panic if calculating the elapsed time since the Unix epoch fails. #[cfg(feature = "std")] pub fn now(context: impl ClockSequence) -> Self { - #[cfg(not(any(feature = "v1", feature = "v6")))] - { - let _ = context; + let (seconds, nanos) = now(); + + let counter = context.generate_sequence(seconds, nanos) as u64; + let usable_counter_bits = context.usable_bits(); + + Timestamp { + seconds, + nanos, + counter, + usable_counter_bits, } + } + /// Get a timestamp representing the current system time. + #[cfg(feature = "std")] + pub fn now_u64(context: impl ClockSequence) -> Self { let (seconds, nanos) = now(); + let counter = context.generate_sequence(seconds, nanos); + let usable_counter_bits = context.usable_bits(); + Timestamp { seconds, nanos, - #[cfg(any(feature = "v1", feature = "v6"))] - counter: context.generate_sequence(seconds, nanos), + counter, + usable_counter_bits, } } @@ -78,18 +92,28 @@ impl Timestamp { /// If conversion from RFC4122 ticks to the internal timestamp format would overflow /// it will wrap. pub const fn from_rfc4122(ticks: u64, counter: u16) -> Self { - #[cfg(not(any(feature = "v1", feature = "v6")))] - { - let _ = counter; - } - let (seconds, nanos) = Self::rfc4122_to_unix(ticks); Timestamp { seconds, nanos, - #[cfg(any(feature = "v1", feature = "v6"))] - counter, + counter: counter as u64, + usable_counter_bits: 16, + } + } + + /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. + /// + /// # Overflow + /// + /// If conversion from RFC4122 ticks to the internal timestamp format would overflow + /// it will wrap. + pub const fn from_unix_time(seconds: u64, nanos: u32, counter: u16) -> Self { + Timestamp { + seconds, + nanos, + counter: counter as u64, + usable_counter_bits: 16, } } @@ -100,21 +124,14 @@ impl Timestamp { /// If conversion from RFC4122 ticks to the internal timestamp format would overflow /// it will wrap. pub fn from_unix(context: impl ClockSequence, seconds: u64, nanos: u32) -> Self { - #[cfg(not(any(feature = "v1", feature = "v6")))] - { - let _ = context; + let counter = context.generate_sequence(seconds, nanos) as u64; + let usable_counter_bits = context.usable_bits(); - Timestamp { seconds, nanos } - } - #[cfg(any(feature = "v1", feature = "v6"))] - { - let counter = context.generate_sequence(seconds, nanos); - - Timestamp { - seconds, - nanos, - counter, - } + Timestamp { + seconds, + nanos, + counter, + usable_counter_bits, } } @@ -125,11 +142,10 @@ impl Timestamp { /// /// If conversion from RFC4122 ticks to the internal timestamp format would overflow /// it will wrap. - #[cfg(any(feature = "v1", feature = "v6"))] pub const fn to_rfc4122(&self) -> (u64, u16) { ( Self::unix_to_rfc4122_ticks(self.seconds, self.nanos), - self.counter, + self.counter as u16, ) } @@ -143,7 +159,6 @@ impl Timestamp { (self.seconds, self.nanos) } - #[cfg(any(feature = "v1", feature = "v6"))] const fn unix_to_rfc4122_ticks(seconds: u64, nanos: u32) -> u64 { UUID_TICKS_BETWEEN_EPOCHS .wrapping_add(seconds.wrapping_mul(10_000_000)) @@ -338,13 +353,28 @@ pub trait ClockSequence { /// /// This method will be called each time a [`Timestamp`] is constructed. fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output; + + /// The number of usable bits from the least significant bit in the result of [`ClockSequence::generate_sequence`]. + fn usable_bits(&self) -> usize + where + Self::Output: Sized, + { + core::mem::size_of::() + } } impl<'a, T: ClockSequence + ?Sized> ClockSequence for &'a T { type Output = T::Output; + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { (**self).generate_sequence(seconds, subsec_nanos) } + + fn usable_bits(&self) -> usize + where + Self::Output: Sized, { + (**self).usable_bits() + } } /// Default implementations for the [`ClockSequence`] trait. @@ -367,6 +397,10 @@ pub mod context { fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output { 0 } + + fn usable_bits(&self) -> usize { + 0 + } } #[cfg(all(any(feature = "v1", feature = "v6"), feature = "std", feature = "rng"))] @@ -434,6 +468,10 @@ pub mod context { // where the clock sequence doesn't change regardless of the timestamp self.count.fetch_add(1, Ordering::AcqRel) & (u16::MAX >> 2) } + + fn usable_bits(&self) -> usize { + 14 + } } } diff --git a/src/v6.rs b/src/v6.rs index 25fe60e6..cafe71ff 100644 --- a/src/v6.rs +++ b/src/v6.rs @@ -200,7 +200,7 @@ mod tests { let uuid3 = Uuid::new_v6(Timestamp::from_unix(&context, time, time_fraction), &node); let uuid4 = Uuid::new_v6(Timestamp::from_unix(&context, time, time_fraction), &node); - assert_eq!(uuid3.get_timestamp().unwrap().counter, 1); - assert_eq!(uuid4.get_timestamp().unwrap().counter, 2); + assert_eq!(uuid3.get_timestamp().unwrap().to_rfc4122().1, 1); + assert_eq!(uuid4.get_timestamp().unwrap().to_rfc4122().1, 2); } } From 34c603c7c8edbd4f691be42a53a9efa77743e9dc Mon Sep 17 00:00:00 2001 From: KodrAus Date: Mon, 13 May 2024 13:54:36 +1000 Subject: [PATCH 02/10] include the counter in v7 UUIDs --- src/builder.rs | 4 ++-- src/rng.rs | 5 +++++ src/timestamp.rs | 37 +++++++++++++++++++++++-------------- src/v7.rs | 32 ++++++++++++++++++++++++++++++-- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 2dd68a2c..e0de9ba1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -636,10 +636,10 @@ impl Builder { /// # Ok(()) /// # } /// ``` - pub const fn from_unix_timestamp_millis(millis: u64, random_bytes: &[u8; 10]) -> Self { + pub const fn from_unix_timestamp_millis(millis: u64, counter_random_bytes: &[u8; 10]) -> Self { Builder(timestamp::encode_unix_timestamp_millis( millis, - random_bytes, + counter_random_bytes, )) } diff --git a/src/rng.rs b/src/rng.rs index dcfbb8d6..11a5c40b 100644 --- a/src/rng.rs +++ b/src/rng.rs @@ -18,6 +18,11 @@ pub(crate) fn bytes() -> [u8; 16] { } } +#[cfg(any(feature = "v4", feature = "v7"))] +pub(crate) fn u128() -> u128 { + u128::from_be_bytes(bytes()) +} + #[cfg(any(feature = "v1", feature = "v6"))] pub(crate) fn u16() -> u16 { #[cfg(not(feature = "fast-rng"))] diff --git a/src/timestamp.rs b/src/timestamp.rs index de1bfe1e..171fd5e0 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -103,7 +103,7 @@ impl Timestamp { } /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. - /// + /// /// # Overflow /// /// If conversion from RFC4122 ticks to the internal timestamp format would overflow @@ -149,6 +149,10 @@ impl Timestamp { ) } + pub(crate) const fn counter(&self) -> (u64, usize) { + (self.counter, self.usable_counter_bits) + } + /// Get the value of the timestamp as a Unix timestamp, as used in version 7 UUIDs. /// /// # Overflow @@ -259,23 +263,27 @@ pub(crate) const fn decode_sorted_rfc4122_timestamp(uuid: &Uuid) -> (u64, u16) { (ticks, counter) } -pub(crate) const fn encode_unix_timestamp_millis(millis: u64, random_bytes: &[u8; 10]) -> Uuid { +pub(crate) const fn encode_unix_timestamp_millis( + millis: u64, + counter_random_bytes: &[u8; 10], +) -> Uuid { let millis_high = ((millis >> 16) & 0xFFFF_FFFF) as u32; let millis_low = (millis & 0xFFFF) as u16; - let random_and_version = - (random_bytes[1] as u16 | ((random_bytes[0] as u16) << 8) & 0x0FFF) | (0x7 << 12); + let random_and_version = (counter_random_bytes[1] as u16 + | ((counter_random_bytes[0] as u16) << 8) & 0x0FFF) + | (0x7 << 12); let mut d4 = [0; 8]; - d4[0] = (random_bytes[2] & 0x3F) | 0x80; - d4[1] = random_bytes[3]; - d4[2] = random_bytes[4]; - d4[3] = random_bytes[5]; - d4[4] = random_bytes[6]; - d4[5] = random_bytes[7]; - d4[6] = random_bytes[8]; - d4[7] = random_bytes[9]; + d4[0] = (counter_random_bytes[2] & 0x3F) | 0x80; + d4[1] = counter_random_bytes[3]; + d4[2] = counter_random_bytes[4]; + d4[3] = counter_random_bytes[5]; + d4[4] = counter_random_bytes[6]; + d4[5] = counter_random_bytes[7]; + d4[6] = counter_random_bytes[8]; + d4[7] = counter_random_bytes[9]; Uuid::from_fields(millis_high, millis_low, random_and_version, &d4) } @@ -371,8 +379,9 @@ impl<'a, T: ClockSequence + ?Sized> ClockSequence for &'a T { } fn usable_bits(&self) -> usize - where - Self::Output: Sized, { + where + Self::Output: Sized, + { (**self).usable_bits() } } diff --git a/src/v7.rs b/src/v7.rs index ea8f4746..60396d96 100644 --- a/src/v7.rs +++ b/src/v7.rs @@ -3,6 +3,8 @@ //! Note that you need to enable the `v7` Cargo feature //! in order to use this module. +use core::cmp; + use crate::{rng, std::convert::TryInto, timestamp::Timestamp, Builder, Uuid}; impl Uuid { @@ -41,6 +43,18 @@ impl Uuid { /// ); /// ``` /// + /// A v7 UUID can also be created with a counter to ensure batches of + /// UUIDs created together remain sortable: + /// + /// ```rust + /// # use uuid::{Uuid, Timestamp, Context}; + /// let context = Context::new(42); + /// let uuid1 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234)); + /// let uuid2 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234)); + /// + /// assert!(uuid1 < uuid2); + /// ``` + /// /// # References /// /// * [Version 7 UUIDs in Draft RFC: New UUID Formats, Version 4](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-04#section-5.2) @@ -48,8 +62,22 @@ impl Uuid { let (secs, nanos) = ts.to_unix(); let millis = (secs * 1000).saturating_add(nanos as u64 / 1_000_000); - Builder::from_unix_timestamp_millis(millis, &rng::bytes()[..10].try_into().unwrap()) - .into_uuid() + let mut counter_and_random = rng::u128(); + + let (counter, counter_bits) = ts.counter(); + + counter_and_random &= u128::MAX + .overflowing_shr(cmp::min(128, counter_bits as u32)) + .0; + counter_and_random |= (counter as u128) + .overflowing_shl(128usize.saturating_sub(counter_bits) as u32) + .0; + + Builder::from_unix_timestamp_millis( + millis, + &counter_and_random.to_be_bytes()[..10].try_into().unwrap(), + ) + .into_uuid() } } From 0d19b7001241b030f5c645034b0c36cf8b26edeb Mon Sep 17 00:00:00 2001 From: KodrAus Date: Tue, 14 May 2024 08:20:19 +1000 Subject: [PATCH 03/10] work on new sequence APIs --- src/timestamp.rs | 69 ++++++++++++++++++++++++------------------------ src/v7.rs | 4 +-- 2 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/timestamp.rs b/src/timestamp.rs index 171fd5e0..6c33e2da 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -41,8 +41,8 @@ pub const UUID_TICKS_BETWEEN_EPOCHS: u64 = 0x01B2_1DD2_1381_4000; pub struct Timestamp { seconds: u64, nanos: u32, - counter: u64, - usable_counter_bits: usize, + counter: u128, + usable_counter_bits: u8, } impl Timestamp { @@ -55,26 +55,16 @@ impl Timestamp { /// This method will panic if calculating the elapsed time since the Unix epoch fails. #[cfg(feature = "std")] pub fn now(context: impl ClockSequence) -> Self { - let (seconds, nanos) = now(); - - let counter = context.generate_sequence(seconds, nanos) as u64; - let usable_counter_bits = context.usable_bits(); - - Timestamp { - seconds, - nanos, - counter, - usable_counter_bits, - } + Self::now_128(context) } /// Get a timestamp representing the current system time. #[cfg(feature = "std")] - pub fn now_u64(context: impl ClockSequence) -> Self { + pub fn now_128(context: impl ClockSequence>) -> Self { let (seconds, nanos) = now(); - let counter = context.generate_sequence(seconds, nanos); - let usable_counter_bits = context.usable_bits(); + let counter = context.generate_sequence(seconds, nanos).into(); + let usable_counter_bits = context.usable_bits() as u8; Timestamp { seconds, @@ -97,35 +87,44 @@ impl Timestamp { Timestamp { seconds, nanos, - counter: counter as u64, + counter: counter as u128, usable_counter_bits: 16, } } /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. - /// - /// # Overflow - /// - /// If conversion from RFC4122 ticks to the internal timestamp format would overflow - /// it will wrap. pub const fn from_unix_time(seconds: u64, nanos: u32, counter: u16) -> Self { + Self::from_unix_time_128(seconds, nanos, counter as u128, 16) + } + + /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. + pub const fn from_unix_time_128( + seconds: u64, + nanos: u32, + counter: u128, + usable_counter_bits: u8, + ) -> Self { Timestamp { seconds, nanos, - counter: counter as u64, - usable_counter_bits: 16, + counter, + usable_counter_bits, } } /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. - /// - /// # Overflow - /// - /// If conversion from RFC4122 ticks to the internal timestamp format would overflow - /// it will wrap. pub fn from_unix(context: impl ClockSequence, seconds: u64, nanos: u32) -> Self { - let counter = context.generate_sequence(seconds, nanos) as u64; - let usable_counter_bits = context.usable_bits(); + Self::from_unix_128(context, seconds, nanos) + } + + /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. + pub fn from_unix_128( + context: impl ClockSequence>, + seconds: u64, + nanos: u32, + ) -> Self { + let counter = context.generate_sequence(seconds, nanos).into(); + let usable_counter_bits = context.usable_bits() as u8; Timestamp { seconds, @@ -149,7 +148,7 @@ impl Timestamp { ) } - pub(crate) const fn counter(&self) -> (u64, usize) { + pub(crate) const fn counter(&self) -> (u128, u8) { (self.counter, self.usable_counter_bits) } @@ -270,7 +269,9 @@ pub(crate) const fn encode_unix_timestamp_millis( let millis_high = ((millis >> 16) & 0xFFFF_FFFF) as u32; let millis_low = (millis & 0xFFFF) as u16; - let random_and_version = (counter_random_bytes[1] as u16 + // TODO: Ensure we shift the value around the version, rather than overwrite it + // Otherwise there will be a patch of counters that don't observably change ordering + let counter_random_version = (counter_random_bytes[1] as u16 | ((counter_random_bytes[0] as u16) << 8) & 0x0FFF) | (0x7 << 12); @@ -285,7 +286,7 @@ pub(crate) const fn encode_unix_timestamp_millis( d4[6] = counter_random_bytes[8]; d4[7] = counter_random_bytes[9]; - Uuid::from_fields(millis_high, millis_low, random_and_version, &d4) + Uuid::from_fields(millis_high, millis_low, counter_random_version, &d4) } pub(crate) const fn decode_unix_timestamp_millis(uuid: &Uuid) -> u64 { diff --git a/src/v7.rs b/src/v7.rs index 60396d96..675835cf 100644 --- a/src/v7.rs +++ b/src/v7.rs @@ -69,8 +69,8 @@ impl Uuid { counter_and_random &= u128::MAX .overflowing_shr(cmp::min(128, counter_bits as u32)) .0; - counter_and_random |= (counter as u128) - .overflowing_shl(128usize.saturating_sub(counter_bits) as u32) + counter_and_random |= counter + .overflowing_shl(128u8.saturating_sub(counter_bits) as u32) .0; Builder::from_unix_timestamp_millis( From e671884eadd6149b36408f35b3785ec170e64b27 Mon Sep 17 00:00:00 2001 From: Ashley Mannix Date: Tue, 18 Jun 2024 08:00:19 +1000 Subject: [PATCH 04/10] add a method to ClockSequence for adjusting the timestamp --- src/timestamp.rs | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/timestamp.rs b/src/timestamp.rs index 6c33e2da..4e12b783 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -63,7 +63,8 @@ impl Timestamp { pub fn now_128(context: impl ClockSequence>) -> Self { let (seconds, nanos) = now(); - let counter = context.generate_sequence(seconds, nanos).into(); + let (counter, seconds, nanos) = context.generate_timestamp_sequence(seconds, nanos); + let counter = counter.into(); let usable_counter_bits = context.usable_bits() as u8; Timestamp { @@ -123,7 +124,8 @@ impl Timestamp { seconds: u64, nanos: u32, ) -> Self { - let counter = context.generate_sequence(seconds, nanos).into(); + let (counter, seconds, nanos) = context.generate_timestamp_sequence(seconds, nanos); + let counter = counter.into(); let usable_counter_bits = context.usable_bits() as u8; Timestamp { @@ -363,7 +365,26 @@ pub trait ClockSequence { /// This method will be called each time a [`Timestamp`] is constructed. fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output; - /// The number of usable bits from the least significant bit in the result of [`ClockSequence::generate_sequence`]. + /// Get the next value in the sequence, potentially also adjusting the timestamp. + /// + /// This method should be preferred over `generate_sequence`. + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + ( + self.generate_sequence(seconds, subsec_nanos), + seconds, + subsec_nanos, + ) + } + + /// The number of usable bits from the least significant bit in the result of [`ClockSequence::generate_sequence`] + /// or [`ClockSequence::generate_timestamp_sequence`]. + /// + /// The number of usable bits is not expected to change between calls. An implementation of `ClockSequence` should + /// always return the same value from this method. fn usable_bits(&self) -> usize where Self::Output: Sized, @@ -379,6 +400,14 @@ impl<'a, T: ClockSequence + ?Sized> ClockSequence for &'a T { (**self).generate_sequence(seconds, subsec_nanos) } + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + (**self).generate_timestamp_sequence(seconds, subsec_nanos) + } + fn usable_bits(&self) -> usize where Self::Output: Sized, From 64a576c92e46e42e800edd3b4ef3e2cf688eb3e0 Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 20 Jun 2024 08:01:04 +1000 Subject: [PATCH 05/10] start filling in a new v7 context --- src/rng.rs | 24 ++++++++- src/timestamp.rs | 131 +++++++++++++++++++++++++++++++++++++++++++++++ src/v7.rs | 2 +- 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/src/rng.rs b/src/rng.rs index 11a5c40b..fa6c4c20 100644 --- a/src/rng.rs +++ b/src/rng.rs @@ -20,7 +20,7 @@ pub(crate) fn bytes() -> [u8; 16] { #[cfg(any(feature = "v4", feature = "v7"))] pub(crate) fn u128() -> u128 { - u128::from_be_bytes(bytes()) + u128::from_ne_bytes(bytes()) } #[cfg(any(feature = "v1", feature = "v6"))] @@ -34,7 +34,27 @@ pub(crate) fn u16() -> u16 { panic!("could not retrieve random bytes for uuid: {}", err) }); - ((bytes[0] as u16) << 8) | (bytes[1] as u16) + u16::from_ne_bytes(bytes) + } + + #[cfg(feature = "fast-rng")] + { + rand::random() + } +} + +#[cfg(feature = "v7")] +pub(crate) fn u64() -> u64 { + #[cfg(not(feature = "fast-rng"))] + { + let mut bytes = [0u8; 8]; + + getrandom::getrandom(&mut bytes).unwrap_or_else(|err| { + // NB: getrandom::Error has no source; this is adequate display + panic!("could not retrieve random bytes for uuid: {}", err) + }); + + u64::from_ne_bytes(bytes) } #[cfg(feature = "fast-rng")] diff --git a/src/timestamp.rs b/src/timestamp.rs index 4e12b783..b323aac8 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -418,6 +418,8 @@ impl<'a, T: ClockSequence + ?Sized> ClockSequence for &'a T { /// Default implementations for the [`ClockSequence`] trait. pub mod context { + use core::cell::Cell; + use super::ClockSequence; #[cfg(any(feature = "v1", feature = "v6"))] @@ -442,6 +444,46 @@ pub mod context { } } + /// A wrapper for a context that uses thread-local storage. + #[cfg(feature = "std")] + pub struct ThreadLocalContext(&'static std::thread::LocalKey); + + #[cfg(feature = "std")] + impl std::fmt::Debug for ThreadLocalContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ThreadLocalContext").finish_non_exhaustive() + } + } + + #[cfg(feature = "std")] + impl ThreadLocalContext { + /// Wrap a thread-local container with a context. + pub const fn new(local_key: &'static std::thread::LocalKey) -> Self { + ThreadLocalContext(local_key) + } + } + + #[cfg(feature = "std")] + impl ClockSequence for ThreadLocalContext { + type Output = C::Output; + + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { + self.0.with(|ctxt| ctxt.generate_sequence(seconds, subsec_nanos)) + } + + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + self.0.with(|ctxt| ctxt.generate_timestamp_sequence(seconds, subsec_nanos)) + } + + fn usable_bits(&self) -> usize { + self.0.with(|ctxt| ctxt.usable_bits()) + } + } + #[cfg(all(any(feature = "v1", feature = "v6"), feature = "std", feature = "rng"))] static CONTEXT: Context = Context { count: Atomic::new(0), @@ -512,6 +554,95 @@ pub mod context { 14 } } + + #[cfg(feature = "v7")] + thread_local! { + static THREAD_CONTEXT_V7: ContextV7 = ContextV7::new(); + } + + #[cfg(feature = "v7")] + static CONTEXT_V7: ThreadLocalContext = ThreadLocalContext(&THREAD_CONTEXT_V7); + + #[cfg(feature = "v7")] + pub(crate) fn shared_context_v7() -> &'static ThreadLocalContext { + &CONTEXT_V7 + } + + /// A non-thread-safe, wrapping counter that produces 42-bit numbers. + /// + /// This type should be used when constructing version 7 UUIDs where strict + /// ordering is required. + /// + /// This type should not be used when constructing version 1 or version 6 UUIDs. + #[cfg(feature = "v7")] + #[derive(Debug)] + pub struct ContextV7 { + last_reseed: Cell, + counter: Cell, + } + + impl ContextV7 { + /// Construct a new context that will reseed its counter on the first + /// non-zero timestamp it receives. + pub const fn new() -> Self { + ContextV7 { + last_reseed: Cell::new(0), + counter: Cell::new(0), + } + } + } + + #[cfg(feature = "v7")] + impl ClockSequence for ContextV7 { + type Output = u64; + + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { + self.generate_timestamp_sequence(seconds, subsec_nanos).0 + } + + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + use std::time::Duration; + + let reseed = || { + let counter = crate::rng::u64() & (u64::MAX >> 23); + self.counter.set(counter); + + counter + }; + + let millis = (seconds * 1000).saturating_add(subsec_nanos as u64 / 1_000_000); + + // TODO: Allow some grace period so we don't reseed every millisecond + if millis != self.last_reseed.get() { + let counter = reseed(); + + (counter, seconds, subsec_nanos) + } else { + // Guaranteed to never overflow u64 + let counter = self.counter.get() + 1; + + if counter > u64::MAX >> 22 { + let counter = reseed(); + + let new_ts = Duration::new(seconds, subsec_nanos) + Duration::from_millis(1); + + (counter, new_ts.as_secs(), new_ts.subsec_nanos()) + } else { + self.counter.set(counter); + + (counter, seconds, subsec_nanos) + } + } + } + + fn usable_bits(&self) -> usize { + 42 + } + } } #[cfg(all(test, any(feature = "v1", feature = "v6")))] diff --git a/src/v7.rs b/src/v7.rs index 675835cf..3b8793e1 100644 --- a/src/v7.rs +++ b/src/v7.rs @@ -14,7 +14,7 @@ impl Uuid { /// as the source timestamp. #[cfg(feature = "std")] pub fn now_v7() -> Self { - Self::new_v7(Timestamp::now(crate::NoContext)) + Self::new_v7(Timestamp::now_128(crate::timestamp::context::shared_context_v7())) } /// Create a new version 7 UUID using a time value and random bytes. From 9c309f8a0c945cce8865893436088cd966fa76aa Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 20 Jun 2024 09:04:02 +1000 Subject: [PATCH 06/10] refactor context organization --- src/lib.rs | 3 + src/timestamp.rs | 388 ++++++++++++++++++++++++++--------------------- src/v7.rs | 14 +- 3 files changed, 226 insertions(+), 179 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0531b3e3..e160d9a1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -241,6 +241,9 @@ pub use timestamp::{context::NoContext, ClockSequence, Timestamp}; #[cfg(any(feature = "v1", feature = "v6"))] pub use timestamp::context::Context; +#[cfg(feature = "v7")] +pub use timestamp::context::ContextV7; + #[cfg(feature = "v1")] #[doc(hidden)] // Soft-deprecated (Rust doesn't support deprecating re-exports) diff --git a/src/timestamp.rs b/src/timestamp.rs index b323aac8..3125b8fc 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -418,229 +418,271 @@ impl<'a, T: ClockSequence + ?Sized> ClockSequence for &'a T { /// Default implementations for the [`ClockSequence`] trait. pub mod context { - use core::cell::Cell; - use super::ClockSequence; #[cfg(any(feature = "v1", feature = "v6"))] - use atomic::{Atomic, Ordering}; - - /// An empty counter that will always return the value `0`. - /// - /// This type should be used when constructing timestamps for version 7 UUIDs, - /// since they don't need a counter for uniqueness. - #[derive(Debug, Clone, Copy, Default)] - pub struct NoContext; - - impl ClockSequence for NoContext { - type Output = u16; + mod v1_support { + use super::*; + + use atomic::{Atomic, Ordering}; + + #[cfg(all(feature = "std", feature = "rng"))] + static CONTEXT: Context = Context { + count: Atomic::new(0), + }; + + #[cfg(all(feature = "std", feature = "rng"))] + static CONTEXT_INITIALIZED: Atomic = Atomic::new(false); + + #[cfg(all(feature = "std", feature = "rng"))] + pub(crate) fn shared_context() -> &'static Context { + // If the context is in its initial state then assign it to a random value + // It doesn't matter if multiple threads observe `false` here and initialize the context + if CONTEXT_INITIALIZED + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + CONTEXT.count.store(crate::rng::u16(), Ordering::Release); + } - fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output { - 0 + &CONTEXT } - fn usable_bits(&self) -> usize { - 0 + /// A thread-safe, wrapping counter that produces 14-bit values. + /// + /// This type works by: + /// + /// 1. Atomically incrementing the counter value for each timestamp. + /// 2. Wrapping the counter back to zero if it overflows its 14-bit storage. + /// + /// This type should be used when constructing version 1 and version 6 UUIDs. + /// + /// This type should not be used when constructing version 7 UUIDs. When used to + /// construct a version 7 UUID, the 14-bit counter will be padded with random data. + /// Counter overflows are more likely with a 14-bit counter than they are with a + /// 42-bit counter when working at millisecond precision. This type doesn't attempt + /// to adjust the timestamp on overflow. + #[derive(Debug)] + pub struct Context { + count: Atomic, } - } - /// A wrapper for a context that uses thread-local storage. - #[cfg(feature = "std")] - pub struct ThreadLocalContext(&'static std::thread::LocalKey); + impl Context { + /// Construct a new context that's initialized with the given value. + /// + /// The starting value should be a random number, so that UUIDs from + /// different systems with the same timestamps are less likely to collide. + /// When the `rng` feature is enabled, prefer the [`Context::new_random`] method. + pub const fn new(count: u16) -> Self { + Self { + count: Atomic::::new(count), + } + } - #[cfg(feature = "std")] - impl std::fmt::Debug for ThreadLocalContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ThreadLocalContext").finish_non_exhaustive() + /// Construct a new context that's initialized with a random value. + #[cfg(feature = "rng")] + pub fn new_random() -> Self { + Self { + count: Atomic::::new(crate::rng::u16()), + } + } } - } - #[cfg(feature = "std")] - impl ThreadLocalContext { - /// Wrap a thread-local container with a context. - pub const fn new(local_key: &'static std::thread::LocalKey) -> Self { - ThreadLocalContext(local_key) - } - } + impl ClockSequence for Context { + type Output = u16; - #[cfg(feature = "std")] - impl ClockSequence for ThreadLocalContext { - type Output = C::Output; - - fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { - self.0.with(|ctxt| ctxt.generate_sequence(seconds, subsec_nanos)) - } - - fn generate_timestamp_sequence( - &self, - seconds: u64, - subsec_nanos: u32, - ) -> (Self::Output, u64, u32) { - self.0.with(|ctxt| ctxt.generate_timestamp_sequence(seconds, subsec_nanos)) - } - - fn usable_bits(&self) -> usize { - self.0.with(|ctxt| ctxt.usable_bits()) - } - } + fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output { + // RFC4122 reserves 2 bits of the clock sequence so the actual + // maximum value is smaller than `u16::MAX`. Since we unconditionally + // increment the clock sequence we want to wrap once it becomes larger + // than what we can represent in a "u14". Otherwise there'd be patches + // where the clock sequence doesn't change regardless of the timestamp + self.count.fetch_add(1, Ordering::AcqRel) & (u16::MAX >> 2) + } - #[cfg(all(any(feature = "v1", feature = "v6"), feature = "std", feature = "rng"))] - static CONTEXT: Context = Context { - count: Atomic::new(0), - }; - - #[cfg(all(any(feature = "v1", feature = "v6"), feature = "std", feature = "rng"))] - static CONTEXT_INITIALIZED: Atomic = Atomic::new(false); - - #[cfg(all(any(feature = "v1", feature = "v6"), feature = "std", feature = "rng"))] - pub(crate) fn shared_context() -> &'static Context { - // If the context is in its initial state then assign it to a random value - // It doesn't matter if multiple threads observe `false` here and initialize the context - if CONTEXT_INITIALIZED - .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) - .is_ok() - { - CONTEXT.count.store(crate::rng::u16(), Ordering::Release); + fn usable_bits(&self) -> usize { + 14 + } } - - &CONTEXT } - /// A thread-safe, wrapping counter that produces 14-bit numbers. - /// - /// This type should be used when constructing version 1 and version 6 UUIDs. - #[derive(Debug)] #[cfg(any(feature = "v1", feature = "v6"))] - pub struct Context { - count: Atomic, - } + pub use v1_support::*; - #[cfg(any(feature = "v1", feature = "v6"))] - impl Context { - /// Construct a new context that's initialized with the given value. - /// - /// The starting value should be a random number, so that UUIDs from - /// different systems with the same timestamps are less likely to collide. - /// When the `rng` feature is enabled, prefer the [`Context::new_random`] method. - pub const fn new(count: u16) -> Self { - Self { - count: Atomic::::new(count), - } - } + #[cfg(feature = "std")] + mod std_support { + use super::*; - /// Construct a new context that's initialized with a random value. - #[cfg(feature = "rng")] - pub fn new_random() -> Self { - Self { - count: Atomic::::new(crate::rng::u16()), + use std::thread::LocalKey; + + /// A wrapper for a context that uses thread-local storage. + pub struct ThreadLocalContext(&'static LocalKey); + + impl std::fmt::Debug for ThreadLocalContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ThreadLocalContext").finish_non_exhaustive() } } - } - #[cfg(any(feature = "v1", feature = "v6"))] - impl ClockSequence for Context { - type Output = u16; - - fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output { - // RFC4122 reserves 2 bits of the clock sequence so the actual - // maximum value is smaller than `u16::MAX`. Since we unconditionally - // increment the clock sequence we want to wrap once it becomes larger - // than what we can represent in a "u14". Otherwise there'd be patches - // where the clock sequence doesn't change regardless of the timestamp - self.count.fetch_add(1, Ordering::AcqRel) & (u16::MAX >> 2) + impl ThreadLocalContext { + /// Wrap a thread-local container with a context. + pub const fn new(local_key: &'static LocalKey) -> Self { + ThreadLocalContext(local_key) + } } - fn usable_bits(&self) -> usize { - 14 + impl ClockSequence for ThreadLocalContext { + type Output = C::Output; + + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { + self.0.with(|ctxt| ctxt.generate_sequence(seconds, subsec_nanos)) + } + + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + self.0.with(|ctxt| ctxt.generate_timestamp_sequence(seconds, subsec_nanos)) + } + + fn usable_bits(&self) -> usize { + self.0.with(|ctxt| ctxt.usable_bits()) + } } } - #[cfg(feature = "v7")] - thread_local! { - static THREAD_CONTEXT_V7: ContextV7 = ContextV7::new(); - } + #[cfg(feature = "std")] + pub use std_support::*; #[cfg(feature = "v7")] - static CONTEXT_V7: ThreadLocalContext = ThreadLocalContext(&THREAD_CONTEXT_V7); + mod v7_support { + use super::*; - #[cfg(feature = "v7")] - pub(crate) fn shared_context_v7() -> &'static ThreadLocalContext { - &CONTEXT_V7 - } + use core::cell::Cell; - /// A non-thread-safe, wrapping counter that produces 42-bit numbers. - /// - /// This type should be used when constructing version 7 UUIDs where strict - /// ordering is required. - /// - /// This type should not be used when constructing version 1 or version 6 UUIDs. - #[cfg(feature = "v7")] - #[derive(Debug)] - pub struct ContextV7 { - last_reseed: Cell, - counter: Cell, - } + #[cfg(feature = "std")] + use crate::timestamp::context::ThreadLocalContext; - impl ContextV7 { - /// Construct a new context that will reseed its counter on the first - /// non-zero timestamp it receives. - pub const fn new() -> Self { - ContextV7 { - last_reseed: Cell::new(0), - counter: Cell::new(0), - } + #[cfg(feature = "std")] + thread_local! { + static THREAD_CONTEXT_V7: ContextV7 = ContextV7::new(); } - } - #[cfg(feature = "v7")] - impl ClockSequence for ContextV7 { - type Output = u64; - - fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { - self.generate_timestamp_sequence(seconds, subsec_nanos).0 + #[cfg(feature = "std")] + static CONTEXT_V7: ThreadLocalContext = ThreadLocalContext::new(&THREAD_CONTEXT_V7); + + #[cfg(feature = "std")] + pub(crate) fn shared_context_v7() -> &'static ThreadLocalContext { + &CONTEXT_V7 } - fn generate_timestamp_sequence( - &self, - seconds: u64, - subsec_nanos: u32, - ) -> (Self::Output, u64, u32) { - use std::time::Duration; + /// A non-thread-safe, reseeding counter that produces 42-bit values. + /// + /// This type works by: + /// + /// 1. Reseeding the counter each millisecond with a random 41-bit value. The 42nd bit + /// is left unset so the counter can safely increment over the millisecond. + /// 2. Wrapping the counter back to zero if it overflows its 42-bit storage and adding a + /// millisecond to the timestamp. + /// + /// This type can be used when constructing version 7 UUIDs. When used to construct a + /// version 7 UUID, the 42-bit counter will be padded with random data. This type can + /// be used to maintain ordering of UUIDs within the same millisecond. + /// + /// This type should not be used when constructing version 1 or version 6 UUIDs. + /// When used to construct a version 1 or version 6 UUID, only the 14 least significant + /// bits of the counter will be used. + #[derive(Debug)] + pub struct ContextV7 { + last_reseed: Cell, + counter: Cell, + } - let reseed = || { - let counter = crate::rng::u64() & (u64::MAX >> 23); - self.counter.set(counter); + impl ContextV7 { + /// Construct a new context that will reseed its counter on the first + /// non-zero timestamp it receives. + pub const fn new() -> Self { + ContextV7 { + last_reseed: Cell::new(0), + counter: Cell::new(0), + } + } + } - counter - }; + impl ClockSequence for ContextV7 { + type Output = u64; + + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { + self.generate_timestamp_sequence(seconds, subsec_nanos).0 + } - let millis = (seconds * 1000).saturating_add(subsec_nanos as u64 / 1_000_000); + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + use std::time::Duration; - // TODO: Allow some grace period so we don't reseed every millisecond - if millis != self.last_reseed.get() { - let counter = reseed(); + let millis = (seconds * 1000).saturating_add(subsec_nanos as u64 / 1_000_000); - (counter, seconds, subsec_nanos) - } else { - // Guaranteed to never overflow u64 - let counter = self.counter.get() + 1; + if millis > self.last_reseed.get() { + let counter = crate::rng::u64() & (u64::MAX >> 23); + self.counter.set(counter); + self.last_reseed.set(millis); - if counter > u64::MAX >> 22 { - let counter = reseed(); + (counter, seconds, subsec_nanos) + } else { + // Guaranteed to never overflow u64 + let counter = self.counter.get() + 1; - let new_ts = Duration::new(seconds, subsec_nanos) + Duration::from_millis(1); + if counter > u64::MAX >> 22 { + let counter = 0; + self.counter.set(counter); + self.last_reseed.set(millis + 1); - (counter, new_ts.as_secs(), new_ts.subsec_nanos()) - } else { - self.counter.set(counter); + let new_ts = Duration::new(seconds, subsec_nanos) + Duration::from_millis(1); - (counter, seconds, subsec_nanos) + (counter, new_ts.as_secs(), new_ts.subsec_nanos()) + } else { + self.counter.set(counter); + + (counter, seconds, subsec_nanos) + } } } + + fn usable_bits(&self) -> usize { + 42 + } + } + } + + #[cfg(feature = "v7")] + pub use v7_support::*; + + /// An empty counter that will always return the value `0`. + /// + /// This type can be used when constructing version 7 UUIDs. When used to + /// construct a version 7 UUID, the entire counter segment of the UUID will be + /// filled with a random value. This type does not maintain ordering of UUIDs + /// within a millisecond but is efficient. + /// + /// This type should not be used when constructing version 1 or version 6 UUIDs. + /// When used to construct a version 1 or version 6 UUID, the counter + /// segment will remain zero. + #[derive(Debug, Clone, Copy, Default)] + pub struct NoContext; + + impl ClockSequence for NoContext { + type Output = u16; + + fn generate_sequence(&self, _seconds: u64, _nanos: u32) -> Self::Output { + 0 } fn usable_bits(&self) -> usize { - 42 + 0 } } } diff --git a/src/v7.rs b/src/v7.rs index 3b8793e1..478df478 100644 --- a/src/v7.rs +++ b/src/v7.rs @@ -8,10 +8,12 @@ use core::cmp; use crate::{rng, std::convert::TryInto, timestamp::Timestamp, Builder, Uuid}; impl Uuid { - /// Create a new version 7 UUID using the current time value and random bytes. + /// Create a new version 7 UUID using the current time value. /// /// This method is a convenient alternative to [`Uuid::new_v7`] that uses the current system time - /// as the source timestamp. + /// as the source timestamp. UUIDs generated on the same thread will remain ordered based on the + /// order they were created in. UUIDs generated on multiple threads are not guaranteed to share a + /// global ordering within the same millisecond. #[cfg(feature = "std")] pub fn now_v7() -> Self { Self::new_v7(Timestamp::now_128(crate::timestamp::context::shared_context_v7())) @@ -47,10 +49,10 @@ impl Uuid { /// UUIDs created together remain sortable: /// /// ```rust - /// # use uuid::{Uuid, Timestamp, Context}; - /// let context = Context::new(42); - /// let uuid1 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234)); - /// let uuid2 = Uuid::new_v7(Timestamp::from_unix(&context, 1497624119, 1234)); + /// # use uuid::{Uuid, Timestamp, ContextV7}; + /// let context = ContextV7::new(); + /// let uuid1 = Uuid::new_v7(Timestamp::from_unix_128(&context, 1497624119, 1234)); + /// let uuid2 = Uuid::new_v7(Timestamp::from_unix_128(&context, 1497624119, 1234)); /// /// assert!(uuid1 < uuid2); /// ``` From 2a5b943d4b5ae4976bd2390aba26cf70febea94f Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 20 Jun 2024 10:31:53 +1000 Subject: [PATCH 07/10] optimize v4 and v7 UUID generation --- src/rng.rs | 9 ++------ src/timestamp.rs | 53 ++++++++++++++++++++++-------------------------- src/v4.rs | 4 +++- 3 files changed, 29 insertions(+), 37 deletions(-) diff --git a/src/rng.rs b/src/rng.rs index fa6c4c20..2ae23cdb 100644 --- a/src/rng.rs +++ b/src/rng.rs @@ -1,5 +1,5 @@ #[cfg(any(feature = "v4", feature = "v7"))] -pub(crate) fn bytes() -> [u8; 16] { +pub(crate) fn u128() -> u128 { #[cfg(not(feature = "fast-rng"))] { let mut bytes = [0u8; 16]; @@ -9,7 +9,7 @@ pub(crate) fn bytes() -> [u8; 16] { panic!("could not retrieve random bytes for uuid: {}", err) }); - bytes + u128::from_ne_bytes(bytes) } #[cfg(feature = "fast-rng")] @@ -18,11 +18,6 @@ pub(crate) fn bytes() -> [u8; 16] { } } -#[cfg(any(feature = "v4", feature = "v7"))] -pub(crate) fn u128() -> u128 { - u128::from_ne_bytes(bytes()) -} - #[cfg(any(feature = "v1", feature = "v6"))] pub(crate) fn u16() -> u16 { #[cfg(not(feature = "fast-rng"))] diff --git a/src/timestamp.rs b/src/timestamp.rs index 608be7eb..e2a1f41a 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -40,13 +40,13 @@ pub const UUID_TICKS_BETWEEN_EPOCHS: u64 = 0x01B2_1DD2_1381_4000; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Timestamp { seconds: u64, - nanos: u32, + subsec_nanos: u32, counter: u128, usable_counter_bits: u8, } impl Timestamp { - /// Get a timestamp representing the current system time. + /// Get a timestamp representing the current system time and up to a 16-bit counter. /// /// This method defers to the standard library's `SystemTime` type. /// @@ -58,24 +58,24 @@ impl Timestamp { Self::now_128(context) } - /// Get a timestamp representing the current system time. + /// Get a timestamp representing the current system time and up to a 128-bit counter. #[cfg(feature = "std")] pub fn now_128(context: impl ClockSequence>) -> Self { - let (seconds, nanos) = now(); + let (seconds, subsec_nanos) = now(); - let (counter, seconds, nanos) = context.generate_timestamp_sequence(seconds, nanos); + let (counter, seconds, subsec_nanos) = context.generate_timestamp_sequence(seconds, subsec_nanos); let counter = counter.into(); let usable_counter_bits = context.usable_bits() as u8; Timestamp { seconds, - nanos, + subsec_nanos, counter, usable_counter_bits, } } - /// Construct a `Timestamp` from an RFC 9562 timestamp and counter, as used + /// Construct a `Timestamp` from an RFC 9562 timestamp and 16-bit counter, as used /// in versions 1 and 6 UUIDs. /// /// # Overflow @@ -83,54 +83,54 @@ impl Timestamp { /// If conversion from RFC 9562 ticks to the internal timestamp format would overflow /// it will wrap. pub const fn from_rfc4122(ticks: u64, counter: u16) -> Self { - let (seconds, nanos) = Self::rfc4122_to_unix(ticks); + let (seconds, subsec_nanos) = Self::rfc4122_to_unix(ticks); Timestamp { seconds, - nanos, + subsec_nanos, counter: counter as u128, usable_counter_bits: 16, } } - /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. - pub const fn from_unix_time(seconds: u64, nanos: u32, counter: u16) -> Self { - Self::from_unix_time_128(seconds, nanos, counter as u128, 16) + /// Construct a `Timestamp` from a Unix timestamp and a 16-bit counter, as used in version 7 UUIDs. + pub const fn from_unix_time(seconds: u64, subsec_nanos: u32, counter: u16) -> Self { + Self::from_unix_time_128(seconds, subsec_nanos, counter as u128, 16) } - /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. + /// Construct a `Timestamp` from a Unix timestamp and up to a 128-bit counter, as used in version 7 UUIDs. pub const fn from_unix_time_128( seconds: u64, - nanos: u32, + subsec_nanos: u32, counter: u128, usable_counter_bits: u8, ) -> Self { Timestamp { seconds, - nanos, + subsec_nanos, counter, usable_counter_bits, } } - /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. + /// Construct a `Timestamp` from a Unix timestamp and up to a 16-bit counter, as used in version 7 UUIDs. pub fn from_unix(context: impl ClockSequence, seconds: u64, nanos: u32) -> Self { Self::from_unix_128(context, seconds, nanos) } - /// Construct a `Timestamp` from a Unix timestamp, as used in version 7 UUIDs. + /// Construct a `Timestamp` from a Unix timestamp and up to a 128-bit counter, as used in version 7 UUIDs. pub fn from_unix_128( context: impl ClockSequence>, seconds: u64, - nanos: u32, + subsec_nanos: u32, ) -> Self { - let (counter, seconds, nanos) = context.generate_timestamp_sequence(seconds, nanos); + let (counter, seconds, subsec_nanos) = context.generate_timestamp_sequence(seconds, subsec_nanos); let counter = counter.into(); let usable_counter_bits = context.usable_bits() as u8; Timestamp { seconds, - nanos, + subsec_nanos, counter, usable_counter_bits, } @@ -145,23 +145,20 @@ impl Timestamp { /// it will wrap. pub const fn to_rfc4122(&self) -> (u64, u16) { ( - Self::unix_to_rfc4122_ticks(self.seconds, self.nanos), + Self::unix_to_rfc4122_ticks(self.seconds, self.subsec_nanos), self.counter as u16, ) } + // NOTE: This method is not public; the usable counter bits are lost in a version 7 UUID + // so can't be reliably recovered. pub(crate) const fn counter(&self) -> (u128, u8) { (self.counter, self.usable_counter_bits) } /// Get the value of the timestamp as a Unix timestamp, as used in version 7 UUIDs. - /// - /// # Overflow - /// - /// If conversion from RFC 9562 ticks to the internal timestamp format would overflow - /// it will wrap. pub const fn to_unix(&self) -> (u64, u32) { - (self.seconds, self.nanos) + (self.seconds, self.subsec_nanos) } const fn unix_to_rfc4122_ticks(seconds: u64, nanos: u32) -> u64 { @@ -271,8 +268,6 @@ pub(crate) const fn encode_unix_timestamp_millis( let millis_high = ((millis >> 16) & 0xFFFF_FFFF) as u32; let millis_low = (millis & 0xFFFF) as u16; - // TODO: Ensure we shift the value around the version, rather than overwrite it - // Otherwise there will be a patch of counters that don't observably change ordering let counter_random_version = (counter_random_bytes[1] as u16 | ((counter_random_bytes[0] as u16) << 8) & 0x0FFF) | (0x7 << 12); diff --git a/src/v4.rs b/src/v4.rs index 2760c881..f65a7b62 100644 --- a/src/v4.rs +++ b/src/v4.rs @@ -31,7 +31,9 @@ impl Uuid { /// [`getrandom`]: https://crates.io/crates/getrandom /// [from_random_bytes]: struct.Builder.html#method.from_random_bytes pub fn new_v4() -> Uuid { - crate::Builder::from_random_bytes(crate::rng::bytes()).into_uuid() + // This is an optimized method for generating random UUIDs that just masks + // out the bits for the version and variant and sets them both together + Uuid::from_u128(crate::rng::u128() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000) } } From 362d431b071003045c0973d26d7f76739a7431ad Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 20 Jun 2024 10:50:48 +1000 Subject: [PATCH 08/10] ensure the returned timestamp doesn't shift backwards --- src/timestamp.rs | 39 ++++++++++++++++++++++++++++++--------- src/v4.rs | 4 +++- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/timestamp.rs b/src/timestamp.rs index e2a1f41a..4cb96a8e 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -63,7 +63,8 @@ impl Timestamp { pub fn now_128(context: impl ClockSequence>) -> Self { let (seconds, subsec_nanos) = now(); - let (counter, seconds, subsec_nanos) = context.generate_timestamp_sequence(seconds, subsec_nanos); + let (counter, seconds, subsec_nanos) = + context.generate_timestamp_sequence(seconds, subsec_nanos); let counter = counter.into(); let usable_counter_bits = context.usable_bits() as u8; @@ -124,7 +125,8 @@ impl Timestamp { seconds: u64, subsec_nanos: u32, ) -> Self { - let (counter, seconds, subsec_nanos) = context.generate_timestamp_sequence(seconds, subsec_nanos); + let (counter, seconds, subsec_nanos) = + context.generate_timestamp_sequence(seconds, subsec_nanos); let counter = counter.into(); let usable_counter_bits = context.usable_bits() as u8; @@ -626,17 +628,40 @@ pub mod context { let millis = (seconds * 1000).saturating_add(subsec_nanos as u64 / 1_000_000); - if millis > self.last_reseed.get() { + let last_reseed = self.last_reseed.get(); + + // If the observed system time has shifted forwards then regenerate the counter + if millis > last_reseed { + // Leave the most significant bit unset + // This guarantees the counter has at least 2,199,023,255,552 + // values before it will overflow, which is exceptionally unlikely + // even in the worst case let counter = crate::rng::u64() & (u64::MAX >> 23); self.counter.set(counter); self.last_reseed.set(millis); (counter, seconds, subsec_nanos) - } else { + } + // If the observed system time has not shifted forwards then increment the counter + else { + // If the incoming timestamp is earlier than the last observed one then + // use it instead. This may happen if the system clock jitters, or if the counter + // has wrapped and the timestamp is artificially incremented + let millis = last_reseed; + // Guaranteed to never overflow u64 let counter = self.counter.get() + 1; - if counter > u64::MAX >> 22 { + // If the counter has not overflowed its 42-bit storage then return it + if counter <= u64::MAX >> 22 { + self.counter.set(counter); + + (counter, seconds, subsec_nanos) + } + // Unlikely: If the counter has overflowed its 42-bit storage then wrap it + // and increment the timestamp. Until the observed system time shifts past + // this incremented value, all timestamps will use it to maintain monotonicity + else { let counter = 0; self.counter.set(counter); self.last_reseed.set(millis + 1); @@ -645,10 +670,6 @@ pub mod context { Duration::new(seconds, subsec_nanos) + Duration::from_millis(1); (counter, new_ts.as_secs(), new_ts.subsec_nanos()) - } else { - self.counter.set(counter); - - (counter, seconds, subsec_nanos) } } } diff --git a/src/v4.rs b/src/v4.rs index f65a7b62..14d755e2 100644 --- a/src/v4.rs +++ b/src/v4.rs @@ -33,7 +33,9 @@ impl Uuid { pub fn new_v4() -> Uuid { // This is an optimized method for generating random UUIDs that just masks // out the bits for the version and variant and sets them both together - Uuid::from_u128(crate::rng::u128() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000) + Uuid::from_u128( + crate::rng::u128() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000, + ) } } From bd10a000c2d7069d95051f6df45bf27cbaf78043 Mon Sep 17 00:00:00 2001 From: KodrAus Date: Thu, 20 Jun 2024 17:03:50 +1000 Subject: [PATCH 09/10] shift the counter around the variant --- src/builder.rs | 6 ++++-- src/timestamp.rs | 10 +++++++++- src/v7.rs | 25 ++++++++++++++++++------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 0c2a0257..880327bb 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -609,9 +609,11 @@ impl Builder { )) } - /// Creates a `Builder` for a version 7 UUID using the supplied Unix timestamp and random bytes. + /// Creates a `Builder` for a version 7 UUID using the supplied Unix timestamp and counter bytes. /// - /// This method assumes the bytes are already sufficiently random. + /// This method will set the variant field within the counter bytes without attempting to shift + /// the data around it. Callers using the counter as a monotonic value should be careful not to + /// store significant data in the 2 least significant bits of the 3rd byte. /// /// # Examples /// diff --git a/src/timestamp.rs b/src/timestamp.rs index 4cb96a8e..be8cfa5d 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -21,6 +21,8 @@ //! * [UUID Version 7 in RFC 9562](https://www.ietf.org/rfc/rfc9562.html#section-5.7) //! * [Timestamp Considerations in RFC 9562](https://www.ietf.org/rfc/rfc9562.html#section-6.1) +use core::cmp; + use crate::Uuid; /// The number of 100 nanosecond ticks between the RFC 9562 epoch @@ -362,11 +364,15 @@ pub trait ClockSequence { /// Get the next value in the sequence to feed into a timestamp. /// /// This method will be called each time a [`Timestamp`] is constructed. + /// + /// Any bits beyond [`ClockSequence::usable_bits`] in the output must be unset. fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output; /// Get the next value in the sequence, potentially also adjusting the timestamp. /// /// This method should be preferred over `generate_sequence`. + /// + /// Any bits beyond [`ClockSequence::usable_bits`] in the output must be unset. fn generate_timestamp_sequence( &self, seconds: u64, @@ -382,13 +388,15 @@ pub trait ClockSequence { /// The number of usable bits from the least significant bit in the result of [`ClockSequence::generate_sequence`] /// or [`ClockSequence::generate_timestamp_sequence`]. /// + /// The number of usable bits must not exceed 128. + /// /// The number of usable bits is not expected to change between calls. An implementation of `ClockSequence` should /// always return the same value from this method. fn usable_bits(&self) -> usize where Self::Output: Sized, { - core::mem::size_of::() + cmp::min(128, core::mem::size_of::()) } } diff --git a/src/v7.rs b/src/v7.rs index 097335de..5d4041b6 100644 --- a/src/v7.rs +++ b/src/v7.rs @@ -3,8 +3,6 @@ //! Note that you need to enable the `v7` Cargo feature //! in order to use this module. -use core::cmp; - use crate::{rng, std::convert::TryInto, timestamp::Timestamp, Builder, Uuid}; impl Uuid { @@ -68,13 +66,26 @@ impl Uuid { let mut counter_and_random = rng::u128(); - let (counter, counter_bits) = ts.counter(); + let (mut counter, counter_bits) = ts.counter(); - counter_and_random &= u128::MAX - .overflowing_shr(cmp::min(128, counter_bits as u32)) - .0; + debug_assert!(counter_bits <= 128); + + let mut counter_bits = counter_bits as u32; + + // If the counter intersects the variant field then shift around it. + // This ensures that any bits set in the counter that would intersect + // the variant are still preserved + if counter_bits > 12 { + let mask = u128::MAX << (counter_bits - 12); + + counter = (counter & !mask) | ((counter & mask) << 2); + + counter_bits += 2; + } + + counter_and_random &= u128::MAX.overflowing_shr(counter_bits).0; counter_and_random |= counter - .overflowing_shl(128u8.saturating_sub(counter_bits) as u32) + .overflowing_shl(128u32.saturating_sub(counter_bits)) .0; Builder::from_unix_timestamp_millis( From c270b3d66ae809517663ffeb38d56e004c517f7f Mon Sep 17 00:00:00 2001 From: KodrAus Date: Mon, 24 Jun 2024 08:39:43 +1000 Subject: [PATCH 10/10] improve testing for contexts --- src/timestamp.rs | 245 ++++++++++++++++++++++++++++++++++++++++------- src/v1.rs | 35 ------- src/v6.rs | 35 ------- src/v7.rs | 5 +- 4 files changed, 210 insertions(+), 110 deletions(-) diff --git a/src/timestamp.rs b/src/timestamp.rs index be8cfa5d..b863639c 100644 --- a/src/timestamp.rs +++ b/src/timestamp.rs @@ -117,8 +117,12 @@ impl Timestamp { } /// Construct a `Timestamp` from a Unix timestamp and up to a 16-bit counter, as used in version 7 UUIDs. - pub fn from_unix(context: impl ClockSequence, seconds: u64, nanos: u32) -> Self { - Self::from_unix_128(context, seconds, nanos) + pub fn from_unix( + context: impl ClockSequence, + seconds: u64, + subsec_nanos: u32, + ) -> Self { + Self::from_unix_128(context, seconds, subsec_nanos) } /// Construct a `Timestamp` from a Unix timestamp and up to a 128-bit counter, as used in version 7 UUIDs. @@ -511,6 +515,35 @@ pub mod context { 14 } } + + #[cfg(test)] + mod tests { + use crate::Timestamp; + + use super::*; + + #[test] + fn context() { + let seconds = 1_496_854_535; + let subsec_nanos = 812_946_000; + + let context = Context::new(u16::MAX >> 2); + + let ts = Timestamp::from_unix(&context, seconds, subsec_nanos); + assert_eq!(16383, ts.counter); + assert_eq!(14, ts.usable_counter_bits); + + let seconds = 1_496_854_536; + + let ts = Timestamp::from_unix(&context, seconds, subsec_nanos); + assert_eq!(0, ts.counter); + + let seconds = 1_496_854_535; + + let ts = Timestamp::from_unix(&context, seconds, subsec_nanos); + assert_eq!(1, ts.counter); + } + } } #[cfg(any(feature = "v1", feature = "v6"))] @@ -520,7 +553,8 @@ pub mod context { mod std_support { use super::*; - use std::thread::LocalKey; + use core::panic::{AssertUnwindSafe, RefUnwindSafe}; + use std::{sync::Mutex, thread::LocalKey}; /// A wrapper for a context that uses thread-local storage. pub struct ThreadLocalContext(&'static LocalKey); @@ -559,6 +593,58 @@ pub mod context { self.0.with(|ctxt| ctxt.usable_bits()) } } + + impl ClockSequence for AssertUnwindSafe { + type Output = C::Output; + + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { + self.0.generate_sequence(seconds, subsec_nanos) + } + + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + self.0.generate_timestamp_sequence(seconds, subsec_nanos) + } + + fn usable_bits(&self) -> usize + where + Self::Output: Sized, + { + self.0.usable_bits() + } + } + + impl ClockSequence for Mutex { + type Output = C::Output; + + fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> Self::Output { + self.lock() + .unwrap_or_else(|err| err.into_inner()) + .generate_sequence(seconds, subsec_nanos) + } + + fn generate_timestamp_sequence( + &self, + seconds: u64, + subsec_nanos: u32, + ) -> (Self::Output, u64, u32) { + self.lock() + .unwrap_or_else(|err| err.into_inner()) + .generate_timestamp_sequence(seconds, subsec_nanos) + } + + fn usable_bits(&self) -> usize + where + Self::Output: Sized, + { + self.lock() + .unwrap_or_else(|err| err.into_inner()) + .usable_bits() + } + } } #[cfg(feature = "std")] @@ -568,26 +654,17 @@ pub mod context { mod v7_support { use super::*; - use core::cell::Cell; + use core::{cell::Cell, panic::RefUnwindSafe}; #[cfg(feature = "std")] - use crate::timestamp::context::ThreadLocalContext; + static CONTEXT_V7: std::sync::Mutex = std::sync::Mutex::new(ContextV7::new()); #[cfg(feature = "std")] - thread_local! { - static THREAD_CONTEXT_V7: ContextV7 = ContextV7::new(); - } - - #[cfg(feature = "std")] - static CONTEXT_V7: ThreadLocalContext = - ThreadLocalContext::new(&THREAD_CONTEXT_V7); - - #[cfg(feature = "std")] - pub(crate) fn shared_context_v7() -> &'static ThreadLocalContext { + pub(crate) fn shared_context_v7() -> &'static std::sync::Mutex { &CONTEXT_V7 } - /// A non-thread-safe, reseeding counter that produces 42-bit values. + /// An unsynchronized, reseeding counter that produces 42-bit values. /// /// This type works by: /// @@ -605,16 +682,39 @@ pub mod context { /// bits of the counter will be used. #[derive(Debug)] pub struct ContextV7 { - last_reseed: Cell, + last_reseed: Cell, counter: Cell, } + #[derive(Debug, Default, Clone, Copy)] + struct LastReseed { + millis: u64, + ts_seconds: u64, + ts_subsec_nanos: u32, + } + + impl LastReseed { + fn from_millis(millis: u64) -> Self { + LastReseed { + millis, + ts_seconds: millis / 1_000, + ts_subsec_nanos: (millis % 1_000) as u32 * 1_000_000, + } + } + } + + impl RefUnwindSafe for ContextV7 {} + impl ContextV7 { /// Construct a new context that will reseed its counter on the first /// non-zero timestamp it receives. pub const fn new() -> Self { ContextV7 { - last_reseed: Cell::new(0), + last_reseed: Cell::new(LastReseed { + millis: 0, + ts_seconds: 0, + ts_subsec_nanos: 0, + }), counter: Cell::new(0), } } @@ -632,52 +732,57 @@ pub mod context { seconds: u64, subsec_nanos: u32, ) -> (Self::Output, u64, u32) { - use std::time::Duration; + // Leave the most significant bit unset + // This guarantees the counter has at least 2,199,023,255,552 + // values before it will overflow, which is exceptionally unlikely + // even in the worst case + const RESEED_MASK: u64 = u64::MAX >> 23; + const MAX_COUNTER: u64 = u64::MAX >> 22; - let millis = (seconds * 1000).saturating_add(subsec_nanos as u64 / 1_000_000); + let millis = (seconds * 1_000).saturating_add(subsec_nanos as u64 / 1_000_000); let last_reseed = self.last_reseed.get(); // If the observed system time has shifted forwards then regenerate the counter - if millis > last_reseed { - // Leave the most significant bit unset - // This guarantees the counter has at least 2,199,023,255,552 - // values before it will overflow, which is exceptionally unlikely - // even in the worst case - let counter = crate::rng::u64() & (u64::MAX >> 23); + if millis > last_reseed.millis { + let last_reseed = LastReseed::from_millis(millis); + self.last_reseed.set(last_reseed); + + let counter = crate::rng::u64() & RESEED_MASK; self.counter.set(counter); - self.last_reseed.set(millis); - (counter, seconds, subsec_nanos) + (counter, last_reseed.ts_seconds, last_reseed.ts_subsec_nanos) } // If the observed system time has not shifted forwards then increment the counter else { // If the incoming timestamp is earlier than the last observed one then // use it instead. This may happen if the system clock jitters, or if the counter // has wrapped and the timestamp is artificially incremented - let millis = last_reseed; + let millis = (); + let _ = millis; // Guaranteed to never overflow u64 let counter = self.counter.get() + 1; // If the counter has not overflowed its 42-bit storage then return it - if counter <= u64::MAX >> 22 { + if counter <= MAX_COUNTER { self.counter.set(counter); - (counter, seconds, subsec_nanos) + (counter, last_reseed.ts_seconds, last_reseed.ts_subsec_nanos) } // Unlikely: If the counter has overflowed its 42-bit storage then wrap it // and increment the timestamp. Until the observed system time shifts past // this incremented value, all timestamps will use it to maintain monotonicity else { - let counter = 0; - self.counter.set(counter); - self.last_reseed.set(millis + 1); + // Increment the timestamp by 1 milli + let last_reseed = LastReseed::from_millis(last_reseed.millis + 1); + self.last_reseed.set(last_reseed); - let new_ts = - Duration::new(seconds, subsec_nanos) + Duration::from_millis(1); + // Reseed the counter + let counter = crate::rng::u64() & RESEED_MASK; + self.counter.set(counter); - (counter, new_ts.as_secs(), new_ts.subsec_nanos()) + (counter, last_reseed.ts_seconds, last_reseed.ts_subsec_nanos) } } } @@ -686,6 +791,72 @@ pub mod context { 42 } } + + #[cfg(test)] + mod tests { + use core::time::Duration; + + use super::*; + + use crate::Timestamp; + + #[test] + fn context() { + let seconds = 1_496_854_535; + let subsec_nanos = 812_946_000; + + let context = ContextV7::new(); + + let ts1 = Timestamp::from_unix_128(&context, seconds, subsec_nanos); + assert_eq!(42, ts1.usable_counter_bits); + + // Backwards second + let seconds = 1_496_854_534; + + let ts2 = Timestamp::from_unix_128(&context, seconds, subsec_nanos); + + // The backwards time should be ignored + // The counter should still increment + assert_eq!(ts1.seconds, ts2.seconds); + assert_eq!(ts1.subsec_nanos, ts2.subsec_nanos); + assert_eq!(ts1.counter + 1, ts2.counter); + + // Forwards second + let seconds = 1_496_854_536; + + let ts3 = Timestamp::from_unix_128(&context, seconds, subsec_nanos); + + // The counter should have reseeded + assert_ne!(ts2.counter + 1, ts3.counter); + assert_ne!(0, ts3.counter); + } + + #[test] + fn context_wrap() { + let seconds = 1_496_854_535u64; + let subsec_nanos = 812_946_000u32; + + let millis = (seconds * 1000).saturating_add(subsec_nanos as u64 / 1_000_000); + + // This context will wrap + let context = ContextV7 { + last_reseed: Cell::new(LastReseed::from_millis(millis)), + counter: Cell::new(u64::MAX >> 22), + }; + + let ts = Timestamp::from_unix_128(&context, seconds, subsec_nanos); + + // The timestamp should be incremented by 1ms + let expected_ts = Duration::new(seconds, subsec_nanos / 1_000_000 * 1_000_000) + + Duration::from_millis(1); + assert_eq!(expected_ts.as_secs(), ts.seconds); + assert_eq!(expected_ts.subsec_nanos(), ts.subsec_nanos); + + // The counter should have reseeded + assert!(ts.counter < (u64::MAX >> 22) as u128); + assert_ne!(0, ts.counter); + } + } } #[cfg(feature = "v7")] diff --git a/src/v1.rs b/src/v1.rs index cf5ba14d..dc8dd80f 100644 --- a/src/v1.rs +++ b/src/v1.rs @@ -166,39 +166,4 @@ mod tests { assert_eq!(uuid.get_version(), Some(Version::Mac)); assert_eq!(uuid.get_variant(), Variant::RFC4122); } - - #[test] - #[cfg_attr( - all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown" - ), - wasm_bindgen_test - )] - fn test_new_context() { - let time: u64 = 1_496_854_535; - let time_fraction: u32 = 812_946_000; - let node = [1, 2, 3, 4, 5, 6]; - - // This context will wrap - let context = Context::new(u16::MAX >> 2); - - let uuid1 = Uuid::new_v1(Timestamp::from_unix(&context, time, time_fraction), &node); - - let time: u64 = 1_496_854_536; - - let uuid2 = Uuid::new_v1(Timestamp::from_unix(&context, time, time_fraction), &node); - - assert_eq!(uuid1.get_timestamp().unwrap().to_rfc4122().1, 16383); - assert_eq!(uuid2.get_timestamp().unwrap().to_rfc4122().1, 0); - - let time = 1_496_854_535; - - let uuid3 = Uuid::new_v1(Timestamp::from_unix(&context, time, time_fraction), &node); - let uuid4 = Uuid::new_v1(Timestamp::from_unix(&context, time, time_fraction), &node); - - assert_eq!(uuid3.get_timestamp().unwrap().to_rfc4122().1, 1); - assert_eq!(uuid4.get_timestamp().unwrap().to_rfc4122().1, 2); - } } diff --git a/src/v6.rs b/src/v6.rs index cc7e50ba..02db3e9e 100644 --- a/src/v6.rs +++ b/src/v6.rs @@ -168,39 +168,4 @@ mod tests { assert_eq!(uuid.get_version(), Some(Version::SortMac)); assert_eq!(uuid.get_variant(), Variant::RFC4122); } - - #[test] - #[cfg_attr( - all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown" - ), - wasm_bindgen_test - )] - fn test_new_context() { - let time: u64 = 1_496_854_535; - let time_fraction: u32 = 812_946_000; - let node = [1, 2, 3, 4, 5, 6]; - - // This context will wrap - let context = Context::new(u16::MAX >> 2); - - let uuid1 = Uuid::new_v6(Timestamp::from_unix(&context, time, time_fraction), &node); - - let time: u64 = 1_496_854_536; - - let uuid2 = Uuid::new_v6(Timestamp::from_unix(&context, time, time_fraction), &node); - - assert_eq!(uuid1.get_timestamp().unwrap().to_rfc4122().1, 16383); - assert_eq!(uuid2.get_timestamp().unwrap().to_rfc4122().1, 0); - - let time = 1_496_854_535; - - let uuid3 = Uuid::new_v6(Timestamp::from_unix(&context, time, time_fraction), &node); - let uuid4 = Uuid::new_v6(Timestamp::from_unix(&context, time, time_fraction), &node); - - assert_eq!(uuid3.get_timestamp().unwrap().to_rfc4122().1, 1); - assert_eq!(uuid4.get_timestamp().unwrap().to_rfc4122().1, 2); - } } diff --git a/src/v7.rs b/src/v7.rs index 5d4041b6..5dbda3b9 100644 --- a/src/v7.rs +++ b/src/v7.rs @@ -9,9 +9,8 @@ impl Uuid { /// Create a new version 7 UUID using the current time value. /// /// This method is a convenient alternative to [`Uuid::new_v7`] that uses the current system time - /// as the source timestamp. UUIDs generated on the same thread will remain ordered based on the - /// order they were created in. UUIDs generated on multiple threads are not guaranteed to share a - /// global ordering within the same millisecond. + /// as the source timestamp. All UUIDs generated through this method by the same process are + /// guaranteed to be ordered by their creation. #[cfg(feature = "std")] pub fn now_v7() -> Self { Self::new_v7(Timestamp::now_128(