Skip to main content

wickra_exchange_core/
idempotency.rs

1//! Client order ids for idempotent placement.
2//!
3//! Attaching a client-supplied id to an order makes placement safe to retry: if
4//! a network blip hides the response to a placed order, re-sending the same
5//! client id lets the venue deduplicate instead of double-filling.
6//!
7//! [`ClientIdGenerator`] produces unique ids from a fixed prefix and a monotonic
8//! counter — deterministic given its seed, so it is testable and reproducible,
9//! with no hidden randomness or clock.
10
11use std::sync::atomic::{AtomicU64, Ordering};
12
13/// A monotonic generator of unique client order ids of the form
14/// `"{prefix}-{counter}"`.
15#[derive(Debug)]
16pub struct ClientIdGenerator {
17    prefix: String,
18    counter: AtomicU64,
19}
20
21impl ClientIdGenerator {
22    /// A generator with the given id prefix, starting its counter at zero.
23    pub fn new(prefix: impl Into<String>) -> Self {
24        Self {
25            prefix: prefix.into(),
26            counter: AtomicU64::new(0),
27        }
28    }
29
30    /// A generator whose counter starts at `seed` (for reproducible sequences).
31    pub fn with_seed(prefix: impl Into<String>, seed: u64) -> Self {
32        Self {
33            prefix: prefix.into(),
34            counter: AtomicU64::new(seed),
35        }
36    }
37
38    /// The next unique id. Thread-safe and lock-free.
39    pub fn next_id(&self) -> String {
40        let n = self.counter.fetch_add(1, Ordering::Relaxed);
41        format!("{}-{n}", self.prefix)
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use std::collections::HashSet;
49
50    #[test]
51    fn ids_carry_the_prefix_and_increment() {
52        let gen = ClientIdGenerator::new("wkex");
53        assert_eq!(gen.next_id(), "wkex-0");
54        assert_eq!(gen.next_id(), "wkex-1");
55        assert_eq!(gen.next_id(), "wkex-2");
56    }
57
58    #[test]
59    fn seed_makes_the_sequence_reproducible() {
60        let gen = ClientIdGenerator::with_seed("x", 100);
61        assert_eq!(gen.next_id(), "x-100");
62        assert_eq!(gen.next_id(), "x-101");
63    }
64
65    #[test]
66    fn ids_are_unique_over_a_run() {
67        let gen = ClientIdGenerator::new("p");
68        let ids: HashSet<String> = (0..1000).map(|_| gen.next_id()).collect();
69        assert_eq!(ids.len(), 1000);
70    }
71}