mirror of
https://github.com/chatmail/core.git
synced 2026-04-18 14:06:29 +03:00
Add a new crate `deltachat_time` with a fake `struct SystemTimeTools` for mocking `SystemTime::now()` for test purposes. One still needs to use `std::time::SystemTime` as a struct representing a system time. I think such a minimalistic approach is ok -- even if somebody uses the original `SystemTime::now()` instead of the mock by mistake, that could break only tests but not the program itself. The worst thing that can happen is that tests using `SystemTime::shift()` and checking messages timestamps f.e. wouldn't catch the corresponding bugs, but now we don't have such tests at all which is much worse.
194 lines
6.6 KiB
Rust
194 lines
6.6 KiB
Rust
//! # Time smearing.
|
|
//!
|
|
//! As e-mails typically only use a second-based-resolution for timestamps,
|
|
//! the order of two mails sent within one second is unclear.
|
|
//! This is bad e.g. when forwarding some messages from a chat -
|
|
//! these messages will appear at the recipient easily out of order.
|
|
//!
|
|
//! We work around this issue by not sending out two mails with the same timestamp.
|
|
//! For this purpose, in short, we track the last timestamp used in `last_smeared_timestamp`
|
|
//! when another timestamp is needed in the same second, we use `last_smeared_timestamp+1`
|
|
//! after some moments without messages sent out,
|
|
//! `last_smeared_timestamp` is again in sync with the normal time.
|
|
//!
|
|
//! However, we do not do all this for the far future,
|
|
//! but at max `MAX_SECONDS_TO_LEND_FROM_FUTURE`
|
|
|
|
use std::cmp::{max, min};
|
|
use std::sync::atomic::{AtomicI64, Ordering};
|
|
|
|
pub(crate) const MAX_SECONDS_TO_LEND_FROM_FUTURE: i64 = 5;
|
|
|
|
/// Smeared timestamp generator.
|
|
#[derive(Debug)]
|
|
pub struct SmearedTimestamp {
|
|
/// Next timestamp available for allocation.
|
|
smeared_timestamp: AtomicI64,
|
|
}
|
|
|
|
impl SmearedTimestamp {
|
|
/// Creates a new smeared timestamp generator.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
smeared_timestamp: AtomicI64::new(0),
|
|
}
|
|
}
|
|
|
|
/// Allocates `count` unique timestamps.
|
|
///
|
|
/// Returns the first allocated timestamp.
|
|
pub fn create_n(&self, now: i64, count: i64) -> i64 {
|
|
let mut prev = self.smeared_timestamp.load(Ordering::Relaxed);
|
|
loop {
|
|
// Advance the timestamp if it is in the past,
|
|
// but keep `count - 1` timestamps from the past if possible.
|
|
let t = max(prev, now - count + 1);
|
|
|
|
// Rewind the time back if there is no room
|
|
// to allocate `count` timestamps without going too far into the future.
|
|
// Not going too far into the future
|
|
// is more important than generating unique timestamps.
|
|
let first = min(t, now + MAX_SECONDS_TO_LEND_FROM_FUTURE - count + 1);
|
|
|
|
// Allocate `count` timestamps by advancing the current timestamp.
|
|
let next = first + count;
|
|
|
|
if let Err(x) = self.smeared_timestamp.compare_exchange_weak(
|
|
prev,
|
|
next,
|
|
Ordering::Relaxed,
|
|
Ordering::Relaxed,
|
|
) {
|
|
prev = x;
|
|
} else {
|
|
return first;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Creates a single timestamp.
|
|
pub fn create(&self, now: i64) -> i64 {
|
|
self.create_n(now, 1)
|
|
}
|
|
|
|
/// Returns the current smeared timestamp.
|
|
pub fn current(&self) -> i64 {
|
|
self.smeared_timestamp.load(Ordering::Relaxed)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::test_utils::TestContext;
|
|
use crate::tools::{
|
|
create_smeared_timestamp, create_smeared_timestamps, smeared_time, time, SystemTime,
|
|
};
|
|
|
|
#[test]
|
|
fn test_smeared_timestamp() {
|
|
let smeared_timestamp = SmearedTimestamp::new();
|
|
let now = time();
|
|
|
|
assert_eq!(smeared_timestamp.current(), 0);
|
|
|
|
for i in 0..MAX_SECONDS_TO_LEND_FROM_FUTURE {
|
|
assert_eq!(smeared_timestamp.create(now), now + i);
|
|
}
|
|
assert_eq!(
|
|
smeared_timestamp.create(now),
|
|
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
|
|
);
|
|
assert_eq!(
|
|
smeared_timestamp.create(now),
|
|
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
|
|
);
|
|
|
|
// System time rewinds back by 1000 seconds.
|
|
let now = now - 1000;
|
|
assert_eq!(
|
|
smeared_timestamp.create(now),
|
|
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
|
|
);
|
|
assert_eq!(
|
|
smeared_timestamp.create(now),
|
|
now + MAX_SECONDS_TO_LEND_FROM_FUTURE
|
|
);
|
|
assert_eq!(
|
|
smeared_timestamp.create(now + 1),
|
|
now + MAX_SECONDS_TO_LEND_FROM_FUTURE + 1
|
|
);
|
|
assert_eq!(smeared_timestamp.create(now + 100), now + 100);
|
|
assert_eq!(smeared_timestamp.create(now + 100), now + 101);
|
|
assert_eq!(smeared_timestamp.create(now + 100), now + 102);
|
|
}
|
|
|
|
#[test]
|
|
fn test_create_n_smeared_timestamps() {
|
|
let smeared_timestamp = SmearedTimestamp::new();
|
|
let now = time();
|
|
|
|
// Create a single timestamp to initialize the generator.
|
|
assert_eq!(smeared_timestamp.create(now), now);
|
|
|
|
// Wait a minute.
|
|
let now = now + 60;
|
|
|
|
// Simulate forwarding 7 messages.
|
|
let forwarded_messages = 7;
|
|
|
|
// We have not sent anything for a minute,
|
|
// so we can take the current timestamp and take 6 timestamps from the past.
|
|
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 6);
|
|
|
|
assert_eq!(smeared_timestamp.current(), now + 1);
|
|
|
|
// Wait 4 seconds.
|
|
// Now we have 3 free timestamps in the past.
|
|
let now = now + 4;
|
|
|
|
assert_eq!(smeared_timestamp.current(), now - 3);
|
|
|
|
// Forward another 7 messages.
|
|
// We can only lend 3 timestamps from the past.
|
|
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 3);
|
|
|
|
// We had to borrow 3 timestamps from the future
|
|
// because there were not enough timestamps in the past.
|
|
assert_eq!(smeared_timestamp.current(), now + 4);
|
|
|
|
// Forward another 7 messages.
|
|
// We cannot use more than 5 timestamps from the future,
|
|
// so we use 5 timestamps from the future,
|
|
// the current timestamp and one timestamp from the past.
|
|
assert_eq!(smeared_timestamp.create_n(now, forwarded_messages), now - 1);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_smeared_timestamp() {
|
|
let t = TestContext::new().await;
|
|
assert_ne!(create_smeared_timestamp(&t), create_smeared_timestamp(&t));
|
|
assert!(
|
|
create_smeared_timestamp(&t)
|
|
>= SystemTime::now()
|
|
.duration_since(SystemTime::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs() as i64
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_smeared_timestamps() {
|
|
let t = TestContext::new().await;
|
|
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE - 1;
|
|
let start = create_smeared_timestamps(&t, count as usize);
|
|
let next = smeared_time(&t);
|
|
assert!((start + count - 1) < next);
|
|
|
|
let count = MAX_SECONDS_TO_LEND_FROM_FUTURE + 30;
|
|
let start = create_smeared_timestamps(&t, count as usize);
|
|
let next = smeared_time(&t);
|
|
assert!((start + count - 1) < next);
|
|
}
|
|
}
|