aster_forge_cloud_files_windows/
watchdog.rs

1//! Deterministic callback watchdog primitives.
2
3use std::time::{Duration, Instant};
4
5use crate::{Result, WindowsCloudFilesError};
6
7/// The fixed callback timeout documented by Windows CFAPI.
8pub const WINDOWS_CFAPI_CALLBACK_TIMEOUT: Duration = Duration::from_mins(1);
9
10/// Host-controlled watchdog configuration for one pending fetch.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct WindowsFetchDataWatchdogConfig {
13    timeout: Duration,
14}
15
16impl WindowsFetchDataWatchdogConfig {
17    /// Creates a positive timeout no longer than the platform callback contract.
18    /// # Errors
19    ///
20    /// Returns an error when validation fails or an underlying backend, store, or platform
21    /// operation fails.
22    pub fn new(timeout: Duration) -> Result<Self> {
23        if timeout.is_zero() {
24            return Err(WindowsCloudFilesError::InvalidWatchdogTimeout {
25                reason: "timeout must be positive",
26            });
27        }
28        if timeout > WINDOWS_CFAPI_CALLBACK_TIMEOUT {
29            return Err(WindowsCloudFilesError::InvalidWatchdogTimeout {
30                reason: "timeout exceeds the fixed CFAPI callback timeout",
31            });
32        }
33        Ok(Self { timeout })
34    }
35
36    /// Returns the configured timeout.
37    #[must_use]
38    pub const fn timeout(self) -> Duration {
39        self.timeout
40    }
41}
42
43impl Default for WindowsFetchDataWatchdogConfig {
44    fn default() -> Self {
45        Self {
46            timeout: WINDOWS_CFAPI_CALLBACK_TIMEOUT,
47        }
48    }
49}
50
51/// Deadline state for one pending fetch. The host supplies `Instant` values, making tests
52/// deterministic without a Tokio timer or a background thread in the native callback path.
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct WindowsFetchDataWatchdog {
55    config: WindowsFetchDataWatchdogConfig,
56    last_activity: Instant,
57}
58
59impl WindowsFetchDataWatchdog {
60    /// Starts a watchdog at `now`.
61    #[must_use]
62    pub const fn started(config: WindowsFetchDataWatchdogConfig, now: Instant) -> Self {
63        Self {
64            config,
65            last_activity: now,
66        }
67    }
68
69    /// Returns the next deadline, or `None` if the host clock cannot represent it.
70    #[must_use]
71    pub fn deadline(self) -> Option<Instant> {
72        self.last_activity.checked_add(self.config.timeout)
73    }
74
75    /// Records valid provider progress or another host operation and resets the deadline.
76    pub fn touch(&mut self, now: Instant) {
77        self.last_activity = now;
78    }
79
80    /// Returns whether the watchdog is due at `now`.
81    #[must_use]
82    pub fn is_due(self, now: Instant) -> bool {
83        self.deadline().is_some_and(|deadline| now >= deadline)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn watchdog_rejects_zero_and_longer_than_platform_deadline() {
93        assert!(WindowsFetchDataWatchdogConfig::new(Duration::ZERO).is_err());
94        assert!(
95            WindowsFetchDataWatchdogConfig::new(
96                WINDOWS_CFAPI_CALLBACK_TIMEOUT + Duration::from_secs(1)
97            )
98            .is_err()
99        );
100    }
101
102    #[test]
103    fn watchdog_deadline_is_refreshed_by_activity() {
104        let config = WindowsFetchDataWatchdogConfig::new(Duration::from_secs(5)).unwrap();
105        let start = Instant::now();
106        let mut watchdog = WindowsFetchDataWatchdog::started(config, start);
107        assert!(!watchdog.is_due(start + Duration::from_secs(4)));
108        watchdog.touch(start + Duration::from_secs(4));
109        assert!(!watchdog.is_due(start + Duration::from_secs(8)));
110        assert!(watchdog.is_due(start + Duration::from_secs(9)));
111    }
112}