Skip to content

Commit 295593a

Browse files
committed
ensure sub-millisecond precision fits into the requested number of bits
1 parent 120c01c commit 295593a

File tree

1 file changed

+112
-74
lines changed

1 file changed

+112
-74
lines changed

src/timestamp.rs

+112-74
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ pub mod context {
709709
timestamp: Cell<ReseedingTimestamp>,
710710
counter: Cell<Counter>,
711711
adjust: Adjust,
712-
additional_precision_bits: usize,
712+
precision: Precision,
713713
}
714714

715715
impl RefUnwindSafe for ContextV7 {}
@@ -726,7 +726,12 @@ pub mod context {
726726
}),
727727
counter: Cell::new(Counter { value: 0 }),
728728
adjust: Adjust { by_ns: 0 },
729-
additional_precision_bits: 0,
729+
precision: Precision {
730+
bits: 0,
731+
mask: 0,
732+
factor: 0,
733+
shift: 0,
734+
},
730735
}
731736
}
732737

@@ -742,8 +747,8 @@ pub mod context {
742747
/// by trading a small amount of entropy for better counter synchronization. Note that the counter
743748
/// will still be reseeded on millisecond boundaries, even though some of its storage will be
744749
/// dedicated to the timestamp.
745-
pub fn with_nanosecond_precision(mut self) -> Self {
746-
self.additional_precision_bits = 12;
750+
pub fn with_additional_precision(mut self) -> Self {
751+
self.precision = Precision::new(12);
747752
self
748753
}
749754
}
@@ -768,26 +773,23 @@ pub mod context {
768773

769774
if should_reseed {
770775
// If the observed system time has shifted forwards then regenerate the counter
771-
counter = Counter::reseed(self.additional_precision_bits, &timestamp);
776+
counter = Counter::reseed(&self.precision, &timestamp);
772777
} else {
773778
// If the observed system time has not shifted forwards then increment the counter
774779

775780
// If the incoming timestamp is earlier than the last observed one then
776781
// use it instead. This may happen if the system clock jitters, or if the counter
777782
// has wrapped and the timestamp is artificially incremented
778783

779-
counter = self
780-
.counter
781-
.get()
782-
.increment(self.additional_precision_bits, &timestamp);
784+
counter = self.counter.get().increment(&self.precision, &timestamp);
783785

784786
// Unlikely: If the counter has overflowed its 42-bit storage then wrap it
785787
// and increment the timestamp. Until the observed system time shifts past
786788
// this incremented value, all timestamps will use it to maintain monotonicity
787789
if counter.has_overflowed() {
788790
// Increment the timestamp by 1 milli and reseed the counter
789791
timestamp = timestamp.increment();
790-
counter = Counter::reseed(self.additional_precision_bits, &timestamp);
792+
counter = Counter::reseed(&self.precision, &timestamp);
791793
}
792794
};
793795

@@ -802,6 +804,46 @@ pub mod context {
802804
}
803805
}
804806

807+
#[derive(Debug)]
808+
struct Adjust {
809+
by_ns: u32,
810+
}
811+
812+
impl Adjust {
813+
#[inline]
814+
fn by_millis(millis: u32) -> Self {
815+
Adjust {
816+
by_ns: millis.saturating_mul(1_000_000),
817+
}
818+
}
819+
820+
#[inline]
821+
fn apply(&self, seconds: u64, subsec_nanos: u32) -> (u64, u32) {
822+
if self.by_ns == 0 {
823+
// No shift applied
824+
return (seconds, subsec_nanos);
825+
}
826+
827+
let mut shifted_subsec_nanos =
828+
subsec_nanos.checked_add(self.by_ns).unwrap_or(subsec_nanos);
829+
830+
if shifted_subsec_nanos < 1_000_000_000 {
831+
// The shift hasn't overflowed into the next second
832+
(seconds, shifted_subsec_nanos)
833+
} else {
834+
// The shift has overflowed into the next second
835+
shifted_subsec_nanos -= 1_000_000_000;
836+
837+
if seconds < u64::MAX {
838+
(seconds + 1, shifted_subsec_nanos)
839+
} else {
840+
// The next second would overflow a `u64`
841+
(seconds, subsec_nanos)
842+
}
843+
}
844+
}
845+
}
846+
805847
#[derive(Debug, Default, Clone, Copy)]
806848
struct ReseedingTimestamp {
807849
last_seed: u64,
@@ -854,42 +896,42 @@ pub mod context {
854896
}
855897

856898
#[derive(Debug)]
857-
struct Adjust {
858-
by_ns: u32,
899+
struct Precision {
900+
bits: usize,
901+
factor: u64,
902+
mask: u64,
903+
shift: u64,
859904
}
860905

861-
impl Adjust {
862-
#[inline]
863-
fn by_millis(millis: u32) -> Self {
864-
Adjust {
865-
by_ns: millis.saturating_mul(1_000_000),
906+
impl Precision {
907+
fn new(bits: usize) -> Self {
908+
// The mask and shift are used to paste the sub-millisecond precision
909+
// into the most significant bits of the counter
910+
let mask = u64::MAX >> (64 - USABLE_BITS + bits);
911+
let shift = (USABLE_BITS - bits) as u64;
912+
913+
// The factor reduces the size of the sub-millisecond precision to
914+
// fit into the specified number of bits
915+
let factor = (999_999u64 / 2u64.pow(bits as u32)) + 1;
916+
917+
Precision {
918+
bits,
919+
factor,
920+
mask,
921+
shift,
866922
}
867923
}
868924

869925
#[inline]
870-
fn apply(&self, seconds: u64, subsec_nanos: u32) -> (u64, u32) {
871-
if self.by_ns == 0 {
872-
// No shift applied
873-
return (seconds, subsec_nanos);
926+
fn apply(&self, value: u64, timestamp: &ReseedingTimestamp) -> u64 {
927+
if self.bits == 0 {
928+
// No additional precision is being used
929+
return value;
874930
}
875931

876-
let mut shifted_subsec_nanos =
877-
subsec_nanos.checked_add(self.by_ns).unwrap_or(subsec_nanos);
878-
879-
if shifted_subsec_nanos < 1_000_000_000 {
880-
// The shift hasn't overflowed into the next second
881-
(seconds, shifted_subsec_nanos)
882-
} else {
883-
// The shift has overflowed into the next second
884-
shifted_subsec_nanos -= 1_000_000_000;
932+
let additional = timestamp.submilli_nanos() as u64 / self.factor;
885933

886-
if seconds < u64::MAX {
887-
(seconds + 1, shifted_subsec_nanos)
888-
} else {
889-
// The next second would overflow a `u64`
890-
(seconds, subsec_nanos)
891-
}
892-
}
934+
(value & self.mask) | (additional << self.shift)
893935
}
894936
}
895937

@@ -900,42 +942,22 @@ pub mod context {
900942

901943
impl Counter {
902944
#[inline]
903-
fn new(
904-
value: u64,
905-
additional_precision_bits: usize,
906-
timestamp: &ReseedingTimestamp,
907-
) -> Self {
945+
fn reseed(precision: &Precision, timestamp: &ReseedingTimestamp) -> Self {
908946
Counter {
909-
value: if additional_precision_bits != 0 {
910-
let precision_mask =
911-
u64::MAX >> (64 - USABLE_BITS + additional_precision_bits);
912-
let precision_shift = USABLE_BITS - additional_precision_bits;
913-
914-
let precision = timestamp.submilli_nanos() as u64;
915-
916-
(value & precision_mask) | (precision << precision_shift)
917-
} else {
918-
value
919-
},
947+
value: precision.apply(crate::rng::u64() & RESEED_MASK, timestamp),
920948
}
921949
}
922950

923951
#[inline]
924-
fn reseed(additional_precision_bits: usize, timestamp: &ReseedingTimestamp) -> Self {
925-
Counter::new(
926-
crate::rng::u64() & RESEED_MASK,
927-
additional_precision_bits,
928-
timestamp,
929-
)
930-
}
952+
fn increment(&self, precision: &Precision, timestamp: &ReseedingTimestamp) -> Self {
953+
let mut counter = Counter {
954+
value: precision.apply(self.value, timestamp),
955+
};
931956

932-
#[inline]
933-
fn increment(
934-
&self,
935-
additional_precision_bits: usize,
936-
timestamp: &ReseedingTimestamp,
937-
) -> Self {
938-
let mut counter = Counter::new(self.value, additional_precision_bits, timestamp);
957+
// We unconditionally increment the counter even though the precision
958+
// may have set higher bits already. This could technically be avoided,
959+
// but the higher bits are a coarse approximation so we just avoid the
960+
// `if` branch and increment it either way
939961

940962
// Guaranteed to never overflow u64
941963
counter.value += 1;
@@ -982,7 +1004,7 @@ pub mod context {
9821004

9831005
use super::*;
9841006

985-
use crate::Timestamp;
1007+
use crate::{Timestamp, Uuid};
9861008

9871009
#[test]
9881010
fn context() {
@@ -1024,7 +1046,12 @@ pub mod context {
10241046
let context = ContextV7 {
10251047
timestamp: Cell::new(ReseedingTimestamp::from_ts(seconds, subsec_nanos)),
10261048
adjust: Adjust::by_millis(0),
1027-
additional_precision_bits: 0,
1049+
precision: Precision {
1050+
bits: 0,
1051+
mask: 0,
1052+
factor: 0,
1053+
shift: 0,
1054+
},
10281055
counter: Cell::new(Counter {
10291056
value: u64::MAX >> 22,
10301057
}),
@@ -1059,15 +1086,26 @@ pub mod context {
10591086
let seconds = 1_496_854_535;
10601087
let subsec_nanos = 812_946_000;
10611088

1062-
let context = ContextV7::new().with_nanosecond_precision();
1089+
let context = ContextV7::new().with_additional_precision();
10631090

1064-
let ts = Timestamp::from_unix(&context, seconds, subsec_nanos);
1091+
let ts1 = Timestamp::from_unix(&context, seconds, subsec_nanos);
1092+
1093+
// NOTE: Future changes in rounding may change this value slightly
1094+
assert_eq!(3861, ts1.counter >> 30);
10651095

1066-
let (counter, width) = ts.counter();
1096+
assert!(ts1.counter < (u64::MAX >> 22) as u128);
10671097

1068-
assert_eq!(946_000, counter >> 30);
1098+
// Generate another timestamp; it should continue to sort
1099+
let ts2 = Timestamp::from_unix(&context, seconds, subsec_nanos);
1100+
1101+
assert!(Uuid::new_v7(ts2) > Uuid::new_v7(ts1));
1102+
1103+
// Generate another timestamp with an extra nanosecond
1104+
let subsec_nanos = subsec_nanos + 1;
1105+
1106+
let ts3 = Timestamp::from_unix(&context, seconds, subsec_nanos);
10691107

1070-
assert_eq!(42, width);
1108+
assert!(Uuid::new_v7(ts3) > Uuid::new_v7(ts2));
10711109
}
10721110
}
10731111
}

0 commit comments

Comments
 (0)