Skip to main content

wp_model_core/
event_id.rs

1use 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
10/// Returns a process-local monotonically increasing `wp_event_id`.
11///
12/// The initial seed mixes wall-clock time, process identity, and runtime entropy
13/// so the sequence does not fall back to a fixed starting value after restart.
14pub 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}