use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Default)]
pub struct ServerClock {
offset_ms: i64,
}
impl ServerClock {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn sync(&mut self, local_ms: i64, server_ms: i64) {
self.offset_ms = server_ms - local_ms;
}
#[must_use]
pub fn offset_ms(&self) -> i64 {
self.offset_ms
}
#[must_use]
pub fn server_time_ms(&self, local_ms: i64) -> i64 {
local_ms + self.offset_ms
}
}
#[derive(Debug)]
pub struct NonceGenerator {
last: AtomicU64,
}
impl NonceGenerator {
#[must_use]
pub fn new(start: u64) -> Self {
Self {
last: AtomicU64::new(start),
}
}
pub fn next(&self, candidate_ms: u64) -> u64 {
let mut prev = self.last.load(Ordering::Relaxed);
loop {
let next = candidate_ms.max(prev + 1);
match self
.last
.compare_exchange_weak(prev, next, Ordering::Relaxed, Ordering::Relaxed)
{
Ok(_) => return next,
Err(actual) => prev = actual,
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TokenTtl {
issued_at_ms: i64,
ttl_ms: i64,
}
impl TokenTtl {
#[must_use]
pub fn new(issued_at_ms: i64, ttl_ms: i64) -> Self {
Self {
issued_at_ms,
ttl_ms,
}
}
#[must_use]
pub fn expires_at_ms(&self) -> i64 {
self.issued_at_ms + self.ttl_ms
}
#[must_use]
pub fn is_expired(&self, now_ms: i64) -> bool {
now_ms >= self.expires_at_ms()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn server_clock_offsets_both_directions() {
let mut clock = ServerClock::new();
assert_eq!(clock.offset_ms(), 0);
clock.sync(1_000, 1_250);
assert_eq!(clock.offset_ms(), 250);
assert_eq!(clock.server_time_ms(2_000), 2_250);
clock.sync(5_000, 4_900);
assert_eq!(clock.offset_ms(), -100);
assert_eq!(clock.server_time_ms(5_000), 4_900);
}
#[test]
fn nonce_is_strictly_increasing_even_within_a_millisecond() {
let gen = NonceGenerator::new(0);
assert_eq!(gen.next(1_000), 1_000);
assert_eq!(gen.next(1_000), 1_001);
assert_eq!(gen.next(1_000), 1_002);
assert_eq!(gen.next(5_000), 5_000);
assert_eq!(gen.next(10), 5_001);
}
#[test]
fn nonce_starts_above_seed() {
let gen = NonceGenerator::new(100);
assert_eq!(gen.next(0), 101);
}
#[test]
fn token_ttl_expiry() {
let token = TokenTtl::new(1_000, 30_000);
assert_eq!(token.expires_at_ms(), 31_000);
assert!(!token.is_expired(30_999));
assert!(token.is_expired(31_000));
assert!(token.is_expired(40_000));
}
}