use std::time::Duration;
pub const DEFAULT_QUIESCENCE: Duration = Duration::from_secs(30);
pub const DEFAULT_PERIODIC: Duration = Duration::from_secs(300);
pub const DEFAULT_TICK: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TriggerPolicy {
pub quiescence: Duration,
pub periodic: Duration,
}
impl Default for TriggerPolicy {
fn default() -> Self {
Self {
quiescence: DEFAULT_QUIESCENCE,
periodic: DEFAULT_PERIODIC,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PushReason {
Quiescence,
Periodic,
Exit,
}
impl PushReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Quiescence => "quiescence",
Self::Periodic => "periodic",
Self::Exit => "exit",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TriggerInput {
pub dirty: bool,
pub exited: bool,
pub idle_for: Option<Duration>,
pub since_last_push: Duration,
pub in_backoff: bool,
}
#[must_use]
pub fn decide(policy: &TriggerPolicy, input: &TriggerInput) -> Option<PushReason> {
if input.in_backoff {
return None;
}
if !input.dirty {
return None;
}
if input.exited {
return Some(PushReason::Exit);
}
if input.idle_for.is_some_and(|idle| idle >= policy.quiescence) {
return Some(PushReason::Quiescence);
}
if input.since_last_push >= policy.periodic {
return Some(PushReason::Periodic);
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
fn base_input() -> TriggerInput {
TriggerInput {
dirty: true,
exited: false,
idle_for: Some(Duration::from_secs(0)),
since_last_push: Duration::from_secs(0),
in_backoff: false,
}
}
#[test]
fn clean_session_never_pushes() {
let input = TriggerInput {
dirty: false,
exited: false,
idle_for: Some(Duration::from_secs(3600)),
since_last_push: Duration::from_secs(3600),
..base_input()
};
assert_eq!(decide(&TriggerPolicy::default(), &input), None);
}
#[test]
fn quiescence_fires_after_idle_window() {
let policy = TriggerPolicy::default();
let input = TriggerInput {
idle_for: Some(Duration::from_secs(31)),
..base_input()
};
assert_eq!(decide(&policy, &input), Some(PushReason::Quiescence));
let busy = TriggerInput {
idle_for: Some(Duration::from_secs(5)),
..base_input()
};
assert_eq!(decide(&policy, &busy), None);
}
#[test]
fn periodic_fires_for_never_quiescent_session() {
let input = TriggerInput {
idle_for: Some(Duration::from_secs(2)),
since_last_push: Duration::from_secs(301),
..base_input()
};
assert_eq!(
decide(&TriggerPolicy::default(), &input),
Some(PushReason::Periodic),
);
}
#[test]
fn exit_outranks_timers() {
let input = TriggerInput {
exited: true,
idle_for: Some(Duration::from_secs(0)),
..base_input()
};
assert_eq!(
decide(&TriggerPolicy::default(), &input),
Some(PushReason::Exit),
);
}
#[test]
fn backoff_gates_everything() {
let input = TriggerInput {
exited: true,
idle_for: Some(Duration::from_secs(3600)),
since_last_push: Duration::from_secs(3600),
in_backoff: true,
..base_input()
};
assert_eq!(decide(&TriggerPolicy::default(), &input), None);
}
#[test]
fn thresholds_are_inclusive_at_the_boundary() {
let policy = TriggerPolicy::default();
assert_eq!(
decide(
&policy,
&TriggerInput {
idle_for: Some(policy.quiescence),
..base_input()
},
),
Some(PushReason::Quiescence),
);
assert_eq!(
decide(
&policy,
&TriggerInput {
idle_for: Some(policy.quiescence - Duration::from_nanos(1)),
since_last_push: policy.periodic,
..base_input()
},
),
Some(PushReason::Periodic),
);
}
#[test]
fn a_session_with_no_files_yet_still_reaches_the_other_triggers() {
let policy = TriggerPolicy::default();
let pending = TriggerInput {
idle_for: None,
..base_input()
};
assert_eq!(decide(&policy, &pending), None);
assert_eq!(
decide(
&policy,
&TriggerInput {
since_last_push: Duration::from_secs(301),
..pending
},
),
Some(PushReason::Periodic),
);
assert_eq!(
decide(
&policy,
&TriggerInput {
exited: true,
..pending
},
),
Some(PushReason::Exit),
);
}
#[test]
fn default_policy_matches_the_shipped_thresholds() {
let policy = TriggerPolicy::default();
assert_eq!(policy.quiescence, Duration::from_secs(30));
assert_eq!(policy.periodic, Duration::from_secs(300));
assert_eq!(DEFAULT_TICK, Duration::from_secs(5));
}
#[test]
fn push_reason_labels_are_stable() {
assert_eq!(PushReason::Quiescence.as_str(), "quiescence");
assert_eq!(PushReason::Periodic.as_str(), "periodic");
assert_eq!(PushReason::Exit.as_str(), "exit");
}
}