wp_model_core/
event_id.rs1use std::collections::hash_map::{DefaultHasher, RandomState};
2use std::hash::{BuildHasher, Hash, Hasher};
3use std::sync::OnceLock;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6use std::{process, thread};
7
8static WP_EVENT_ID_SEED: OnceLock<AtomicU64> = OnceLock::new();
9
10pub fn next_wp_event_id() -> u64 {
15 WP_EVENT_ID_SEED
16 .get_or_init(|| AtomicU64::new(init_wp_event_id_seed()))
17 .fetch_add(1, Ordering::Relaxed)
18}
19
20fn init_wp_event_id_seed() -> u64 {
21 compose_wp_event_id_seed(
22 SystemTime::now().duration_since(UNIX_EPOCH).ok(),
23 process::id(),
24 runtime_entropy(),
25 )
26}
27
28fn compose_wp_event_id_seed(time_since_epoch: Option<Duration>, pid: u32, entropy: u64) -> u64 {
29 let time_nanos = time_since_epoch.map(duration_to_u64_nanos).unwrap_or(0);
30 let pid_bits = u64::from(pid).rotate_left(32);
31
32 let mut seed = entropy ^ time_nanos.rotate_left(13) ^ pid_bits ^ 0x9E37_79B9_7F4A_7C15;
33
34 seed ^= seed >> 33;
35 seed = seed.wrapping_mul(0xFF51_AFD7_ED55_8CCD);
36 seed ^= seed >> 33;
37 seed = seed.wrapping_mul(0xC4CE_B9FE_1A85_EC53);
38 seed ^= seed >> 33;
39
40 if seed == 0 { 1 } else { seed }
41}
42
43fn duration_to_u64_nanos(duration: Duration) -> u64 {
44 duration.as_nanos() as u64
45}
46
47fn runtime_entropy() -> u64 {
48 let mut stack_marker = 0_u8;
49 let stack_addr = (&mut stack_marker as *mut u8 as usize) as u64;
50 let thread_id_hash = hash_thread_id(thread::current().id());
51
52 let mut hasher = RandomState::new().build_hasher();
53 process::id().hash(&mut hasher);
54 stack_addr.hash(&mut hasher);
55 thread_id_hash.hash(&mut hasher);
56
57 hasher.finish() ^ thread_id_hash.rotate_left(17) ^ stack_addr.rotate_left(29)
58}
59
60fn hash_thread_id(thread_id: thread::ThreadId) -> u64 {
61 let mut hasher = DefaultHasher::new();
62 thread_id.hash(&mut hasher);
63 hasher.finish()
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn compose_wp_event_id_seed_is_non_zero_when_time_is_unavailable() {
72 let seed = compose_wp_event_id_seed(None, 42, 0x1234_5678_9ABC_DEF0);
73 assert_ne!(seed, 0);
74 }
75
76 #[test]
77 fn compose_wp_event_id_seed_changes_across_restarts_even_without_time() {
78 let first = compose_wp_event_id_seed(None, 42, 0x1111_2222_3333_4444);
79 let second = compose_wp_event_id_seed(None, 42, 0x5555_6666_7777_8888);
80 assert_ne!(first, second);
81 }
82
83 #[test]
84 fn compose_wp_event_id_seed_changes_with_same_entropy_but_different_time() {
85 let first = compose_wp_event_id_seed(Some(Duration::from_secs(1)), 42, 7);
86 let second = compose_wp_event_id_seed(Some(Duration::from_secs(2)), 42, 7);
87 assert_ne!(first, second);
88 }
89
90 #[test]
91 fn next_wp_event_id_is_monotonic_within_one_process() {
92 let first = next_wp_event_id();
93 let second = next_wp_event_id();
94 assert_eq!(second, first + 1);
95 }
96}