use std::sync::Arc;
use std::time::Duration;
use crate::control::security::sessions::{KillReason, SessionRegistry};
use crate::control::security::time::now_ms;
use crate::control::state::SharedState;
pub fn next_close_deadline_ms(
token_exp_ms: u64,
last_active_ms: u64,
idle_cap_secs: u64,
) -> Option<u64> {
let idle_deadline = if idle_cap_secs > 0 {
Some(last_active_ms.saturating_add(idle_cap_secs * 1_000))
} else {
None
};
let token_deadline = if token_exp_ms > 0 {
Some(token_exp_ms)
} else {
None
};
match (idle_deadline, token_deadline) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
}
}
fn kill_reason_for(
token_exp_ms: u64,
last_active_ms: u64,
idle_cap_secs: u64,
now_ms: u64,
) -> KillReason {
let token_fired = token_exp_ms > 0 && now_ms >= token_exp_ms;
let idle_fired =
idle_cap_secs > 0 && now_ms >= last_active_ms.saturating_add(idle_cap_secs * 1_000);
match (token_fired, idle_fired) {
(true, false) => KillReason::TokenExpired,
(false, true) => KillReason::IdleTimeout,
(true, true) => {
let token_dl = token_exp_ms;
let idle_dl = last_active_ms.saturating_add(idle_cap_secs * 1_000);
if token_dl <= idle_dl {
KillReason::TokenExpired
} else {
KillReason::IdleTimeout
}
}
(false, false) => KillReason::Alive,
}
}
pub fn sweep_once(session_registry: &Arc<SessionRegistry>, global_idle_secs: u64) {
let now = now_ms();
let entries = session_registry.sweep_snapshot();
for entry in entries {
let idle_secs = if entry.idle_timeout_secs > 0 {
entry.idle_timeout_secs
} else {
global_idle_secs
};
let last_active_ms = entry.last_active_secs * 1_000;
let deadline = next_close_deadline_ms(entry.token_expiry_ms, last_active_ms, idle_secs);
let Some(dl) = deadline else {
continue;
};
if now >= dl {
let reason = kill_reason_for(entry.token_expiry_ms, last_active_ms, idle_secs, now);
if reason != KillReason::Alive {
let _ = session_registry.kill_session_by_id(&entry.session_id, reason);
}
}
}
}
pub fn spawn_idle_sweep_loop(shared: &Arc<SharedState>) {
let shared_sweep = Arc::clone(shared);
crate::control::shutdown::spawn_loop(
&shared.loop_registry,
&shared.shutdown,
"idle_session_sweep",
move |mut shutdown| async move {
let mut tick = tokio::time::interval(Duration::from_secs(5));
loop {
tokio::select! {
_ = shutdown.wait_cancelled() => break,
_ = tick.tick() => {}
}
if shutdown.is_cancelled() {
break;
}
let global_idle = shared_sweep.idle_timeout_secs();
sweep_once(&shared_sweep.session_registry, global_idle);
}
},
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_deadline_when_both_zero() {
assert_eq!(next_close_deadline_ms(0, 1_000_000, 0), None);
}
#[test]
fn idle_deadline_only() {
let dl = next_close_deadline_ms(0, 1_000_000, 300);
assert_eq!(dl, Some(1_300_000));
}
#[test]
fn token_deadline_only() {
let dl = next_close_deadline_ms(9_999_000, 1_000_000, 0);
assert_eq!(dl, Some(9_999_000));
}
#[test]
fn picks_earlier_idle_over_token() {
let dl = next_close_deadline_ms(9_999_000, 1_000_000, 300);
assert_eq!(dl, Some(1_300_000));
}
#[test]
fn picks_earlier_token_over_idle() {
let dl = next_close_deadline_ms(500_000, 1_000_000, 300);
assert_eq!(dl, Some(500_000));
}
#[test]
fn kill_reason_token_when_token_earlier() {
let now = 600_000u64;
let reason = kill_reason_for(500_000, 1_000_000, 300, now);
assert_eq!(reason, KillReason::TokenExpired);
}
#[test]
fn kill_reason_idle_when_idle_fires() {
let now = 1_400_000u64;
let reason = kill_reason_for(9_999_000, 1_000_000, 300, now);
assert_eq!(reason, KillReason::IdleTimeout);
}
#[test]
fn alive_when_not_expired() {
let now = 100_000u64;
let reason = kill_reason_for(9_999_000, 1_000_000, 300, now);
assert_eq!(reason, KillReason::Alive);
}
}