Skip to content

Commit

Permalink
improved timestamp handling: first draft
Browse files Browse the repository at this point in the history
Draft implementation of improved timestamp api discussed in github issue #405.

Establishes new `Timestamp` struct in v1 mod that stores the (timestamp, counter) values that had been used previously. The core rationale/improvement is that `Timestamp` offers several conversion routines to/from RFC4122 and unix timestamp formats. The RFC4122 timestamp format in V1 UUIDs is relatively obscure -- this provides ready access to unix timestamp formats instead. The design also prevents possible confusion/misuse by use of clearly named constructors and ample documentation about the various formats in play.

Tests covering previously existing functionality updated to reflect new api and passing. New functionality untested.

Moved `Uuid::to_timestamp` from impl in src/lib.rs to v1 mod as it requires access to the new `Timestamp` struct defined in v1. Noting as this places `to_timestamp` behind the v1 feature gate, unlike prior to the change.
  • Loading branch information
jonathanstrong authored and KodrAus committed Oct 2, 2019
1 parent be51f82 commit dfa9bb9
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ version = "1.0"
version = "1.0.56"

[features]
default = ["std", "v1", "v3", "v4", "v5", "serde"]
default = ["std"]
guid = ["winapi"]
std = []
stdweb = [ "rand/stdweb" ]
Expand Down
29 changes: 0 additions & 29 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -451,35 +451,6 @@ impl Uuid {
&self.0
}

/// Returns an Optional Tuple of (u64, u16) representing the timestamp and
/// counter portion of a V1 UUID. The timestamp represents the number of
/// 100 nanosecond intervals since midnight 15 October 1582 UTC.
///
/// If the supplied UUID is not V1, this will return None.
pub fn to_timestamp(&self) -> Option<(u64, u16)> {
if self
.get_version()
.map(|v| v != Version::Mac)
.unwrap_or(true)
{
return None;
}

let ts: u64 = u64::from(self.as_bytes()[6] & 0x0F) << 56
| u64::from(self.as_bytes()[7]) << 48
| u64::from(self.as_bytes()[4]) << 40
| u64::from(self.as_bytes()[5]) << 32
| u64::from(self.as_bytes()[0]) << 24
| u64::from(self.as_bytes()[1]) << 16
| u64::from(self.as_bytes()[2]) << 8
| u64::from(self.as_bytes()[3]);

let count: u16 = u16::from(self.as_bytes()[8] & 0x3F) << 8
| u16::from(self.as_bytes()[9]);

Some((ts, count))
}

/// Tests if the UUID is nil
pub fn is_nil(&self) -> bool {
self.as_bytes().iter().all(|&b| b == 0)
Expand Down
157 changes: 150 additions & 7 deletions src/v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,102 @@
use crate::prelude::*;
use core::sync::atomic;

/// The number of 100 ns ticks between the UUID epoch
/// `1582-10-15 00:00:00` and the Unix epoch `1970-01-01 00:00:00`.
const UUID_TICKS_BETWEEN_EPOCHS: u64 = 0x01B2_1DD2_1381_4000;

/// A thread-safe, stateful context for the v1 generator to help ensure
/// process-wide uniqueness.
#[derive(Debug)]
pub struct Context {
count: atomic::AtomicUsize,
}

/// Stores the number of nanoseconds from an epoch and a counter for ensuring
/// V1 ids generated on the same host are unique.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Timestamp {
ticks: u64,
counter: u16
}

impl Timestamp {
/// Construct a `Timestamp` from its raw component values: an RFC4122 timestamp
/// and counter.
///
/// RFC4122, which defines the V1 UUID, specifies a 60-byte timestamp format as
/// the number of 100-nanosecond intervals elapsed since 00:00:00.00, 15 Oct 1582,
/// "the date of the Gregorian reform of the Christian calendar."
///
/// The counter value is used to differentiate between ids generated by
/// the same host computer in rapid succession (i.e. with the same observed
/// time). See `ClockSequence` trait for a generic interface to any counter
/// generators that might be used.
///
/// Internally, the timestamp is stored as a `u64`. For this reason, dates prior
/// to October 1582 are not supported.
///
pub fn from_rfc4122(hundred_ns_intervals: u64, counter: u16) -> Self {
Timestamp { ticks: hundred_ns_intervals, counter }
}

/// Construct a `Timestamp` from a unix timestamp and sequence-generating `context`.
///
/// A unix timestamp represents the elapsed time since Jan 1 1970. Libc's `clock_gettime`
/// and other popular implementations traditionally represent this duration as a
/// `timespec`: a struct with `i64` and `u32` fields representing the seconds, and
/// "subsecond" or fractional nanoseconds elapsed since the timestamp's second began,
/// respectively.
///
/// This constructs a `Timestamp` from the seconds and fractional nanoseconds of a unix
/// timestamp, converting the duration since 1970 into the number of 100-nanosecond
/// intervals since 00:00:00.00, 15 Oct 1982 specified by RFC4122 and used internally
/// by `Timestamp`.
///
pub fn from_unix<T: ClockSequence>(secs: i64, subsec_nanos: u32, context: &T) -> Self {
let counter = context.generate_sequence(secs.abs() as u64, subsec_nanos);
let ticks =
(UUID_TICKS_BETWEEN_EPOCHS as i64 + secs * 10_000_000 + ((subsec_nanos / 100) as i64)) as u64;
Timestamp { ticks, counter }
}

/// Returns the raw RFC4122 timestamp and counter values stored by the `Timestamp`.
/// The timestamp (the first, `u64` element in the tuple) represents the number of
/// 100-nanosecond intervals since 00:00:00.00, 15 Oct 1582. The counter is used to
/// differentiate between ids generated on the same host computer with the same
/// observed time.
///
pub fn to_rfc4122(&self) -> (u64, u16) {
(self.ticks, self.counter)
}

/// Returns the timestamp converted to the seconds and fractional nanoseconds
/// since Jan 1 1970. Internally, the time is stored in 100-nanosecond intervals,
/// thus the maximum precision represented by the fractional nanoseconds value
/// is less than its unit size (100 ns vs. 1 ns).
///
pub fn to_unix(&self) -> (i64, u32) {
let unix_ticks = (self.ticks - UUID_TICKS_BETWEEN_EPOCHS) as i64;
(unix_ticks / 10_000_000, (unix_ticks % 10_000_000).abs() as u32 * 100)
}

/// Returns the timestamp converted into nanoseconds elapsed since Jan 1 1970.
/// Internally, the time is stored in 100-nanosecond intervals, thus the maximum
/// precision represented is less than the units it is measured in (100 ns vs. 1 ns).
/// The value returned represents the same duration as `to_unix`; this provides
/// it in nanosecond units for convenience.
///
pub fn to_unix_nanos(&self) -> i64 {
(self.ticks - UUID_TICKS_BETWEEN_EPOCHS) as i64 * 100
}
}

/// A trait that abstracts over generation of Uuid v1 "Clock Sequence" values.
pub trait ClockSequence {
/// Return a 16-bit number that will be used as the "clock sequence" in
/// the Uuid. The number must be different if the time has changed since
/// the last time a clock sequence was requested.
fn generate_sequence(&self, seconds: u64, nano_seconds: u32) -> u16;
fn generate_sequence(&self, seconds: u64, subsec_nanos: u32) -> u16;
}

impl Uuid {
Expand Down Expand Up @@ -100,10 +183,6 @@ impl Uuid {
let time_high_and_version;

{
/// The number of 100 ns ticks between the UUID epoch
/// `1582-10-15 00:00:00` and the Unix epoch `1970-01-01 00:00:00`.
const UUID_TICKS_BETWEEN_EPOCHS: u64 = 0x01B2_1DD2_1381_4000;

let timestamp =
seconds * 10_000_000 + u64::from(nano_seconds / 100);
let uuid_time = timestamp + UUID_TICKS_BETWEEN_EPOCHS;
Expand All @@ -126,6 +205,70 @@ impl Uuid {

Uuid::from_fields(time_low, time_mid, time_high_and_version, &d4)
}

/// Construct V1 UUID from a `Timestamp` and `node_id`.
pub fn with_timestamp(
ts: &Timestamp,
node_id: &[u8],
) -> Result<Self, crate::Error> {
const NODE_ID_LEN: usize = 6;

let len = node_id.len();
if len != NODE_ID_LEN {
Err(crate::builder::Error::new(NODE_ID_LEN, len))?;
}

let time_low = (ts.ticks & 0xFFFF_FFFF) as u32;
let time_mid = ((ts.ticks >> 32) & 0xFFFF) as u16;
let time_high_and_version =
(((ts.ticks >> 48) & 0x0FFF) as u16) | (1 << 12);

let mut d4 = [0; 8];

{
d4[0] = (((ts.counter & 0x3F00) >> 8) as u8) | 0x80;
d4[1] = (ts.counter & 0xFF) as u8;
}

d4[2..].copy_from_slice(node_id);

Uuid::from_fields(time_low, time_mid, time_high_and_version, &d4)
}

/// Returns an Optional `Timestamp` storing the timestamp and
/// counter portion parsed from a V1 UUID.
///
/// Returns `None` if the supplied UUID is not V1.
///
/// The V1 timestamp format defined in RFC4122 specifies a 60-bit
/// integer representing the number of 100-nanosecond intervals
/// since 00:00:00.00, 15 Oct 1582.
///
/// `Timestamp` offers several options for converting the raw RFC4122
/// value into more commonly-used formats, such as a unix timestamp.
pub fn to_timestamp(&self) -> Option<Timestamp> {
if self
.get_version()
.map(|v| v != Version::Mac)
.unwrap_or(true)
{
return None;
}

let ticks: u64 = u64::from(self.as_bytes()[6] & 0x0F) << 56
| u64::from(self.as_bytes()[7]) << 48
| u64::from(self.as_bytes()[4]) << 40
| u64::from(self.as_bytes()[5]) << 32
| u64::from(self.as_bytes()[0]) << 24
| u64::from(self.as_bytes()[1]) << 16
| u64::from(self.as_bytes()[2]) << 8
| u64::from(self.as_bytes()[3]);

let counter: u16 = u16::from(self.as_bytes()[8] & 0x3F) << 8
| u16::from(self.as_bytes()[9]);

Some(Timestamp::from_rfc4122(ticks, counter))
}
}

impl Context {
Expand Down Expand Up @@ -178,7 +321,7 @@ mod tests {
"20616934-4ba2-11e7-8000-010203040506"
);

let ts = uuid.to_timestamp().unwrap();
let ts = uuid.to_timestamp().unwrap().to_rfc4122();

assert_eq!(ts.0 - 0x01B2_1DD2_1381_4000, 14_968_545_358_129_460);
assert_eq!(ts.1, 0);
Expand All @@ -192,7 +335,7 @@ mod tests {
uuid2.to_hyphenated().to_string(),
"20616934-4ba2-11e7-8001-010203040506"
);
assert_eq!(uuid2.to_timestamp().unwrap().1, 1)
assert_eq!(uuid2.to_timestamp().unwrap().to_rfc4122().1, 1)
};
}
}

0 comments on commit dfa9bb9

Please sign in to comment.