aster_forge_cloud_files_windows/
waiter_registry.rs

1//! Active CFAPI hydration waiter correlation and cancellation ownership.
2
3use std::{
4    collections::HashMap,
5    fmt,
6    sync::{Arc, Mutex, MutexGuard},
7    time::Instant,
8};
9
10use aster_forge_cloud_files_core::{
11    ByteRange, HydrationCancellationHandle, HydrationCancellationOutcome,
12};
13
14use crate::{
15    Result, WindowsCallbackInfoSnapshot, WindowsCancelFetchDataSnapshot, WindowsCloudFilesError,
16    WindowsFetchDataCorrelation, WindowsFetchDataProgress, WindowsFetchDataSnapshot,
17    WindowsFetchDataWatchdog, WindowsFetchDataWatchdogConfig, connection::FetchTerminalGate,
18};
19
20type FetchCorrelation = WindowsFetchDataCorrelation;
21
22struct ActiveWaiter {
23    id: u64,
24    range: ByteRange,
25    cancellation: HydrationCancellationHandle,
26    terminal: FetchTerminalGate,
27    watchdog: WindowsFetchDataWatchdog,
28    progress: Option<WindowsFetchDataProgress>,
29}
30
31#[derive(Default)]
32struct RegistryState {
33    next_id: u64,
34    waiters: HashMap<FetchCorrelation, Vec<ActiveWaiter>>,
35    metrics: WindowsFetchDataWaiterMetrics,
36    watchdog_config: WindowsFetchDataWatchdogConfig,
37}
38
39fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
40    match mutex.lock() {
41        Ok(guard) => guard,
42        Err(poisoned) => poisoned.into_inner(),
43    }
44}
45
46/// Monotonic process-local counters for active fetch waiters and cancellation matching.
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
48pub struct WindowsFetchDataWaiterMetrics {
49    active_waiters: usize,
50    registered_waiters: u64,
51    cancellation_callbacks: u64,
52    unmatched_cancellations: u64,
53    fully_matched_waiters: u64,
54    partially_matched_waiters: u64,
55    cancelled_waiters: u64,
56    already_cancelled_waiters: u64,
57    completion_races: u64,
58    watchdog_timeouts: u64,
59    progress_callbacks: u64,
60    progress_updates: u64,
61}
62
63impl WindowsFetchDataWaiterMetrics {
64    /// Returns the number of hydration waiters currently registered for native cancellation.
65    #[must_use]
66    pub const fn active_waiters(self) -> usize {
67        self.active_waiters
68    }
69
70    /// Returns the total number of waiters registered since this registry was created.
71    #[must_use]
72    pub const fn registered_waiters(self) -> u64 {
73        self.registered_waiters
74    }
75
76    /// Returns the total number of cancellation callbacks applied to this registry.
77    #[must_use]
78    pub const fn cancellation_callbacks(self) -> u64 {
79        self.cancellation_callbacks
80    }
81
82    /// Returns cancellation callbacks that matched no active waiter range.
83    #[must_use]
84    pub const fn unmatched_cancellations(self) -> u64 {
85        self.unmatched_cancellations
86    }
87
88    /// Returns active waiters whose complete logical range was covered by a cancellation.
89    #[must_use]
90    pub const fn fully_matched_waiters(self) -> u64 {
91        self.fully_matched_waiters
92    }
93
94    /// Returns active waiters retained because cancellation covered only part of their range.
95    #[must_use]
96    pub const fn partially_matched_waiters(self) -> u64 {
97        self.partially_matched_waiters
98    }
99
100    /// Returns waiter cancellations that won the core terminal race.
101    #[must_use]
102    pub const fn cancelled_waiters(self) -> u64 {
103        self.cancelled_waiters
104    }
105
106    /// Returns full-range cancellations repeated before the cancelled waiter unregistered.
107    #[must_use]
108    pub const fn already_cancelled_waiters(self) -> u64 {
109        self.already_cancelled_waiters
110    }
111
112    /// Returns full-range cancellations that lost to normal waiter completion.
113    #[must_use]
114    pub const fn completion_races(self) -> u64 {
115        self.completion_races
116    }
117
118    /// Returns waiters cancelled by the local callback watchdog.
119    #[must_use]
120    pub const fn watchdog_timeouts(self) -> u64 {
121        self.watchdog_timeouts
122    }
123
124    /// Returns provider progress samples accepted by the registry.
125    #[must_use]
126    pub const fn progress_callbacks(self) -> u64 {
127        self.progress_callbacks
128    }
129
130    /// Returns progress samples that refreshed at least one active waiter deadline.
131    #[must_use]
132    pub const fn progress_updates(self) -> u64 {
133        self.progress_updates
134    }
135}
136
137/// Per-callback result of matching one CFAPI cancellation range to active hydration waiters.
138#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
139pub struct WindowsFetchDataCancellationOutcome {
140    fully_matched: usize,
141    partially_matched: usize,
142    cancelled: usize,
143    already_cancelled: usize,
144    already_completed: usize,
145}
146
147/// Result of one host watchdog poll.
148#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
149pub struct WindowsFetchDataWatchdogOutcome {
150    cancelled: usize,
151    already_cancelled: usize,
152    already_completed: usize,
153}
154
155impl WindowsFetchDataWatchdogOutcome {
156    /// Returns waiters whose cancellation won the core terminal race.
157    #[must_use]
158    pub const fn cancelled(self) -> usize {
159        self.cancelled
160    }
161
162    /// Returns waiters that were already cancelled before this poll.
163    #[must_use]
164    pub const fn already_cancelled(self) -> usize {
165        self.already_cancelled
166    }
167
168    /// Returns waiters that completed before the watchdog cancellation.
169    #[must_use]
170    pub const fn already_completed(self) -> usize {
171        self.already_completed
172    }
173}
174
175impl WindowsFetchDataCancellationOutcome {
176    /// Returns waiters whose complete range was covered by the callback range.
177    #[must_use]
178    pub const fn fully_matched(self) -> usize {
179        self.fully_matched
180    }
181
182    /// Returns waiters retained because only part of their range was cancelled.
183    #[must_use]
184    pub const fn partially_matched(self) -> usize {
185        self.partially_matched
186    }
187
188    /// Returns full-range cancellations that won the core waiter terminal race.
189    #[must_use]
190    pub const fn cancelled(self) -> usize {
191        self.cancelled
192    }
193
194    /// Returns full-range cancellations already applied to the same active waiter.
195    #[must_use]
196    pub const fn already_cancelled(self) -> usize {
197        self.already_cancelled
198    }
199
200    /// Returns full-range cancellations that arrived after waiter completion won.
201    #[must_use]
202    pub const fn already_completed(self) -> usize {
203        self.already_completed
204    }
205
206    /// Returns whether any active waiter range overlapped this cancellation.
207    #[must_use]
208    pub const fn matched(self) -> bool {
209        self.fully_matched != 0 || self.partially_matched != 0
210    }
211}
212
213/// Shareable active-waiter registry for one or more CFAPI connection generations.
214///
215/// Correlation includes the session generation and every native operation key. A cancellation is
216/// forwarded to core only when it covers an entire waiter range. Partial overlap is recorded but
217/// retained because core waiter cancellation is atomic and CFAPI still requires bytes outside the
218/// cancellation range.
219#[derive(Clone, Default)]
220pub struct WindowsFetchDataWaiterRegistry {
221    state: Arc<Mutex<RegistryState>>,
222}
223
224impl WindowsFetchDataWaiterRegistry {
225    /// Creates an empty registry.
226    #[must_use]
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// Creates a registry with a host-controlled watchdog configuration.
232    #[must_use]
233    pub fn with_watchdog_config(config: WindowsFetchDataWatchdogConfig) -> Self {
234        Self {
235            state: Arc::new(Mutex::new(RegistryState {
236                watchdog_config: config,
237                ..RegistryState::default()
238            })),
239        }
240    }
241
242    /// Returns a point-in-time counter snapshot.
243    #[must_use]
244    pub fn metrics(&self) -> WindowsFetchDataWaiterMetrics {
245        lock(&self.state).metrics
246    }
247
248    /// Applies one owned CFAPI cancellation snapshot to correlated active waiters.
249    /// # Errors
250    ///
251    /// Returns an error when validation fails or an underlying backend, store, or platform
252    /// operation fails.
253    pub fn cancel(
254        &self,
255        snapshot: &WindowsCancelFetchDataSnapshot,
256    ) -> Result<WindowsFetchDataCancellationOutcome> {
257        let cancellation_range = callback_range(snapshot.range(), snapshot.info().file_size())?;
258        let correlation = FetchCorrelation::from_info(snapshot.info());
259        let (targets, fully_matched, partially_matched) = {
260            let mut state = lock(&self.state);
261            state.metrics.cancellation_callbacks =
262                state.metrics.cancellation_callbacks.saturating_add(1);
263
264            let Some(waiters) = state.waiters.get(&correlation) else {
265                state.metrics.unmatched_cancellations =
266                    state.metrics.unmatched_cancellations.saturating_add(1);
267                return Ok(WindowsFetchDataCancellationOutcome::default());
268            };
269            let mut targets = Vec::new();
270            let mut fully_matched = 0usize;
271            let mut partially_matched = 0usize;
272            for waiter in waiters {
273                if range_covers(cancellation_range, waiter.range) {
274                    fully_matched = fully_matched.saturating_add(1);
275                    targets.push((waiter.cancellation.clone(), waiter.terminal.clone()));
276                } else if ranges_overlap(cancellation_range, waiter.range) {
277                    partially_matched = partially_matched.saturating_add(1);
278                }
279            }
280            if fully_matched == 0 && partially_matched == 0 {
281                state.metrics.unmatched_cancellations =
282                    state.metrics.unmatched_cancellations.saturating_add(1);
283            }
284            (targets, fully_matched, partially_matched)
285        };
286
287        let mut outcome = WindowsFetchDataCancellationOutcome {
288            fully_matched,
289            partially_matched,
290            ..WindowsFetchDataCancellationOutcome::default()
291        };
292        for (cancellation, terminal) in targets {
293            // Publish the terminal platform state before waking the waiter. Request drop observes
294            // the same atomic gate, so aborting the hydration task cannot emit ProviderTerminated.
295            terminal.cancel_by_platform();
296            match cancellation.cancel() {
297                HydrationCancellationOutcome::Cancelled => {
298                    outcome.cancelled = outcome.cancelled.saturating_add(1);
299                }
300                HydrationCancellationOutcome::AlreadyCancelled => {
301                    outcome.already_cancelled = outcome.already_cancelled.saturating_add(1);
302                }
303                HydrationCancellationOutcome::AlreadyCompleted => {
304                    outcome.already_completed = outcome.already_completed.saturating_add(1);
305                }
306            }
307        }
308
309        let mut state = lock(&self.state);
310        state.metrics.fully_matched_waiters = state
311            .metrics
312            .fully_matched_waiters
313            .saturating_add(usize_to_u64(outcome.fully_matched));
314        state.metrics.partially_matched_waiters = state
315            .metrics
316            .partially_matched_waiters
317            .saturating_add(usize_to_u64(outcome.partially_matched));
318        state.metrics.cancelled_waiters = state
319            .metrics
320            .cancelled_waiters
321            .saturating_add(usize_to_u64(outcome.cancelled));
322        state.metrics.already_cancelled_waiters = state
323            .metrics
324            .already_cancelled_waiters
325            .saturating_add(usize_to_u64(outcome.already_cancelled));
326        state.metrics.completion_races = state
327            .metrics
328            .completion_races
329            .saturating_add(usize_to_u64(outcome.already_completed));
330        Ok(outcome)
331    }
332
333    /// Applies provider progress to all active waiters with the same native correlation and
334    /// refreshes their local watchdog deadline.
335    /// # Errors
336    ///
337    /// Returns an error when validation fails or an underlying backend, store, or platform
338    /// operation fails.
339    pub fn report_progress(
340        &self,
341        snapshot: &WindowsCallbackInfoSnapshot,
342        progress: WindowsFetchDataProgress,
343        now: Instant,
344    ) -> Result<usize> {
345        self.report_progress_correlation(FetchCorrelation::from_info(snapshot), progress, now)
346    }
347
348    pub(crate) fn report_progress_correlation(
349        &self,
350        correlation: WindowsFetchDataCorrelation,
351        progress: WindowsFetchDataProgress,
352        now: Instant,
353    ) -> Result<usize> {
354        let mut state = lock(&self.state);
355        state.metrics.progress_callbacks = state.metrics.progress_callbacks.saturating_add(1);
356        let Some(waiters) = state.waiters.get_mut(&correlation) else {
357            return Ok(0);
358        };
359        for waiter in &*waiters {
360            if let Some(previous) = waiter.progress {
361                progress.advance_from(previous)?;
362            }
363        }
364        let mut updated = 0usize;
365        for waiter in waiters {
366            waiter.progress = Some(progress);
367            waiter.watchdog.touch(now);
368            updated = updated.saturating_add(1);
369        }
370        state.metrics.progress_updates = state
371            .metrics
372            .progress_updates
373            .saturating_add(usize_to_u64(updated));
374        Ok(updated)
375    }
376
377    /// Cancels pending waiters whose watchdog deadline has elapsed. The host owns the polling
378    /// cadence; this method never starts a timer or blocks a native callback thread.
379    #[must_use]
380    pub fn poll_watchdog(&self, now: Instant) -> WindowsFetchDataWatchdogOutcome {
381        let mut targets = Vec::new();
382        {
383            let mut state = lock(&self.state);
384            for waiters in state.waiters.values_mut() {
385                for waiter in waiters {
386                    if waiter.terminal.watchdog_timed_out() || !waiter.watchdog.is_due(now) {
387                        continue;
388                    }
389                    waiter.terminal.cancel_by_watchdog();
390                    targets.push(waiter.cancellation.clone());
391                }
392            }
393        }
394        let mut outcome = WindowsFetchDataWatchdogOutcome::default();
395        for cancellation in targets {
396            match cancellation.cancel() {
397                HydrationCancellationOutcome::Cancelled => outcome.cancelled += 1,
398                HydrationCancellationOutcome::AlreadyCancelled => outcome.already_cancelled += 1,
399                HydrationCancellationOutcome::AlreadyCompleted => outcome.already_completed += 1,
400            }
401        }
402        let mut state = lock(&self.state);
403        state.metrics.watchdog_timeouts = state
404            .metrics
405            .watchdog_timeouts
406            .saturating_add(usize_to_u64(outcome.cancelled));
407        outcome
408    }
409
410    pub(crate) fn register(
411        &self,
412        snapshot: &WindowsFetchDataSnapshot,
413        range: ByteRange,
414        cancellation: HydrationCancellationHandle,
415        terminal: FetchTerminalGate,
416    ) -> Result<WindowsFetchDataWaiterRegistration> {
417        self.register_at(snapshot, range, cancellation, terminal, Instant::now())
418    }
419
420    fn register_at(
421        &self,
422        snapshot: &WindowsFetchDataSnapshot,
423        range: ByteRange,
424        cancellation: HydrationCancellationHandle,
425        terminal: FetchTerminalGate,
426        now: Instant,
427    ) -> Result<WindowsFetchDataWaiterRegistration> {
428        let correlation = FetchCorrelation::from_info(snapshot.info());
429        let mut state = lock(&self.state);
430        let id = state.next_id;
431        let next_id = state
432            .next_id
433            .checked_add(1)
434            .ok_or(WindowsCloudFilesError::ActiveFetchWaiterIdOverflow)?;
435        let active_waiters = state
436            .metrics
437            .active_waiters
438            .checked_add(1)
439            .ok_or(WindowsCloudFilesError::ActiveFetchWaiterCountOverflow)?;
440        state.next_id = next_id;
441        state.metrics.active_waiters = active_waiters;
442        state.metrics.registered_waiters = state.metrics.registered_waiters.saturating_add(1);
443        let watchdog_config = state.watchdog_config;
444        state
445            .waiters
446            .entry(correlation)
447            .or_default()
448            .push(ActiveWaiter {
449                id,
450                range,
451                cancellation,
452                terminal: terminal.clone(),
453                watchdog: WindowsFetchDataWatchdog::started(watchdog_config, now),
454                progress: None,
455            });
456        drop(state);
457        Ok(WindowsFetchDataWaiterRegistration {
458            registry: self.clone(),
459            correlation,
460            id,
461            terminal,
462            registered: true,
463        })
464    }
465
466    fn unregister(&self, correlation: FetchCorrelation, id: u64) {
467        let mut state = lock(&self.state);
468        let mut removed = false;
469        let mut empty = false;
470        if let Some(waiters) = state.waiters.get_mut(&correlation) {
471            let previous = waiters.len();
472            waiters.retain(|waiter| waiter.id != id);
473            removed = waiters.len() != previous;
474            empty = waiters.is_empty();
475        }
476        if empty {
477            state.waiters.remove(&correlation);
478        }
479        if removed {
480            state.metrics.active_waiters = state.metrics.active_waiters.saturating_sub(1);
481        }
482    }
483}
484
485impl fmt::Debug for WindowsFetchDataWaiterRegistry {
486    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
487        formatter
488            .debug_struct("WindowsFetchDataWaiterRegistry")
489            .field("metrics", &self.metrics())
490            .finish()
491    }
492}
493
494pub(crate) struct WindowsFetchDataWaiterRegistration {
495    registry: WindowsFetchDataWaiterRegistry,
496    correlation: FetchCorrelation,
497    id: u64,
498    terminal: FetchTerminalGate,
499    registered: bool,
500}
501
502impl WindowsFetchDataWaiterRegistration {
503    pub(crate) fn platform_cancelled(&self) -> bool {
504        self.terminal.platform_cancelled()
505    }
506
507    pub(crate) fn watchdog_timed_out(&self) -> bool {
508        self.terminal.watchdog_timed_out()
509    }
510
511    fn unregister(&mut self) {
512        if !self.registered {
513            return;
514        }
515        self.registry.unregister(self.correlation, self.id);
516        self.registered = false;
517    }
518}
519
520impl Drop for WindowsFetchDataWaiterRegistration {
521    fn drop(&mut self) {
522        self.unregister();
523    }
524}
525
526fn callback_range(range: crate::WindowsCallbackRange, file_size: u64) -> Result<ByteRange> {
527    let length = match range.length() {
528        crate::WindowsCallbackRangeLength::Exact(length) => length,
529        crate::WindowsCallbackRangeLength::ToEnd => file_size.checked_sub(range.offset()).ok_or(
530            WindowsCloudFilesError::InvalidCallbackSnapshot {
531                reason: "cancellation range offset exceeds callback file size",
532            },
533        )?,
534    };
535    if length == 0 {
536        return Err(WindowsCloudFilesError::InvalidCallbackSnapshot {
537            reason: "cancellation range must not be empty",
538        });
539    }
540    let end = range.offset() + length;
541    if end > file_size {
542        return Err(WindowsCloudFilesError::InvalidCallbackSnapshot {
543            reason: "cancellation range exceeds callback file size",
544        });
545    }
546    ByteRange::new(range.offset(), length).map_err(Into::into)
547}
548
549const fn range_covers(outer: ByteRange, inner: ByteRange) -> bool {
550    outer.offset() <= inner.offset() && outer.end_exclusive() >= inner.end_exclusive()
551}
552
553const fn ranges_overlap(left: ByteRange, right: ByteRange) -> bool {
554    left.offset() < right.end_exclusive() && right.offset() < left.end_exclusive()
555}
556
557fn usize_to_u64(value: usize) -> u64 {
558    u64::try_from(value).unwrap_or(u64::MAX)
559}
560
561#[cfg(test)]
562mod tests {
563    use std::sync::Arc;
564
565    use aster_forge_cloud_files_core::{
566        Alignment, BackendResult, CloudBackendError, CloudBackendErrorKind, CloudContentBackend,
567        CloudItemId, CloudItemKey, CloudNamespaceId, CloudRootId, CloudScope, ContentReadRange,
568        ContentReadRequest, ContentReadResponse, ContentRevision, HydrationCoordinator,
569        HydrationError, HydrationRequest, SessionGeneration,
570    };
571    use async_trait::async_trait;
572    use bytes::Bytes;
573
574    use super::*;
575    use crate::{
576        WindowsCallbackRange, WindowsCancelFetchDataFlags, WindowsConnectionKey,
577        WindowsFetchDataFlags, WindowsFileIdentity, WindowsRequestKey, WindowsSyncRootIdentity,
578        WindowsTransferKey,
579    };
580
581    struct ImmediateBackend;
582
583    #[async_trait]
584    impl CloudContentBackend for ImmediateBackend {
585        async fn read_content(
586            &self,
587            request: &ContentReadRequest,
588        ) -> BackendResult<ContentReadResponse> {
589            let ContentReadRange::Range(range) = request.read_range() else {
590                return Err(CloudBackendError::new(
591                    CloudBackendErrorKind::InvalidRequest,
592                ));
593            };
594            ContentReadResponse::new(
595                request.revision().clone(),
596                range.offset(),
597                Bytes::from(vec![
598                    0x5a;
599                    usize::try_from(range.length())
600                        .expect("test range length should fit usize")
601                ]),
602                request.expected_size(),
603            )
604            .map_err(|_| CloudBackendError::new(CloudBackendErrorKind::InvalidResponse))
605        }
606    }
607
608    fn generation() -> SessionGeneration {
609        SessionGeneration::new(7).expect("generation fixture should be non-zero")
610    }
611
612    fn item_key() -> CloudItemKey {
613        CloudItemKey::new(
614            CloudScope::new(
615                CloudNamespaceId::new("namespace").expect("namespace fixture should be valid"),
616                CloudRootId::new("root").expect("root fixture should be valid"),
617            ),
618            CloudItemId::new("item").expect("item fixture should be valid"),
619        )
620    }
621
622    fn fetch_snapshot() -> WindowsFetchDataSnapshot {
623        let key = item_key();
624        let info = WindowsCallbackInfoSnapshot::new(
625            generation(),
626            WindowsConnectionKey::new(1),
627            WindowsTransferKey::new(2),
628            WindowsRequestKey::new(3),
629            None,
630            None,
631            0,
632            4,
633            WindowsSyncRootIdentity::encode(key.scope())
634                .expect("sync-root identity fixture should encode"),
635            5,
636            4096,
637            WindowsFileIdentity::encode(&key).expect("file identity fixture should encode"),
638            None,
639            0,
640            None,
641        )
642        .expect("callback info fixture should construct");
643        WindowsFetchDataSnapshot::new(
644            info,
645            WindowsFetchDataFlags::default(),
646            WindowsCallbackRange::exact(0, 4096).expect("range fixture should construct"),
647            None,
648            0,
649            0,
650        )
651    }
652
653    #[tokio::test]
654    async fn cancellation_records_completion_that_won_before_registry_matching() {
655        let snapshot = fetch_snapshot();
656        let range = ByteRange::new(0, 4096).expect("range fixture should be valid");
657        let revision =
658            ContentRevision::from_slice(b"revision").expect("revision fixture should be valid");
659        let coordinator = HydrationCoordinator::new(Arc::new(ImmediateBackend));
660        let waiter = coordinator
661            .request(HydrationRequest::range(
662                item_key(),
663                revision,
664                4096,
665                range,
666                Alignment::ONE,
667                generation(),
668            ))
669            .expect("waiter fixture should register");
670        let registry = WindowsFetchDataWaiterRegistry::new();
671        let registration = registry
672            .register(
673                &snapshot,
674                range,
675                waiter.cancellation_handle(),
676                FetchTerminalGate::default(),
677            )
678            .expect("registry fixture should register");
679        waiter
680            .wait()
681            .await
682            .expect("immediate waiter should complete before cancellation");
683
684        let cancel = WindowsCancelFetchDataSnapshot::new(
685            snapshot.info().clone(),
686            WindowsCancelFetchDataFlags::IO_ABORTED,
687            WindowsCallbackRange::exact(0, 4096).expect("range fixture should construct"),
688        );
689        let outcome = registry
690            .cancel(&cancel)
691            .expect("cancellation fixture should validate");
692        assert_eq!(outcome.already_completed(), 1);
693        assert_eq!(registry.metrics().completion_races(), 1);
694        drop(registration);
695        assert_eq!(registry.metrics().active_waiters(), 0);
696    }
697
698    #[test]
699    fn cancellation_publishes_shared_terminal_gate_before_waiter_resumes() {
700        let snapshot = fetch_snapshot();
701        let range = ByteRange::new(0, 4096).expect("range fixture should be valid");
702        let revision =
703            ContentRevision::from_slice(b"revision").expect("revision fixture should be valid");
704        let coordinator = HydrationCoordinator::new(Arc::new(ImmediateBackend));
705        let waiter = coordinator
706            .request(HydrationRequest::range(
707                item_key(),
708                revision,
709                4096,
710                range,
711                Alignment::ONE,
712                generation(),
713            ))
714            .expect("waiter fixture should register");
715        let terminal = FetchTerminalGate::default();
716        let registry = WindowsFetchDataWaiterRegistry::new();
717        let _registration = registry
718            .register(
719                &snapshot,
720                range,
721                waiter.cancellation_handle(),
722                terminal.clone(),
723            )
724            .expect("registry fixture should register");
725        let cancel = WindowsCancelFetchDataSnapshot::new(
726            snapshot.info().clone(),
727            WindowsCancelFetchDataFlags::IO_ABORTED,
728            WindowsCallbackRange::exact(0, 4096).expect("range fixture should construct"),
729        );
730
731        registry
732            .cancel(&cancel)
733            .expect("cancellation fixture should validate");
734
735        assert!(terminal.platform_cancelled());
736        assert!(!terminal.begin_native());
737    }
738
739    #[test]
740    fn registration_overflow_errors_do_not_insert_partial_state() {
741        let snapshot = fetch_snapshot();
742        let range = ByteRange::new(0, 4096).expect("range fixture should be valid");
743        let revision =
744            ContentRevision::from_slice(b"revision").expect("revision fixture should be valid");
745        let coordinator = HydrationCoordinator::new(Arc::new(ImmediateBackend));
746        let waiter = coordinator
747            .request(HydrationRequest::range(
748                item_key(),
749                revision,
750                4096,
751                range,
752                Alignment::ONE,
753                generation(),
754            ))
755            .expect("waiter fixture should register");
756        let cancellation = waiter.cancellation_handle();
757
758        let identity_overflow = WindowsFetchDataWaiterRegistry::new();
759        lock(&identity_overflow.state).next_id = u64::MAX;
760        assert!(matches!(
761            identity_overflow.register(
762                &snapshot,
763                range,
764                cancellation.clone(),
765                FetchTerminalGate::default(),
766            ),
767            Err(WindowsCloudFilesError::ActiveFetchWaiterIdOverflow)
768        ));
769        assert_eq!(identity_overflow.metrics().active_waiters(), 0);
770
771        let count_overflow = WindowsFetchDataWaiterRegistry::new();
772        lock(&count_overflow.state).metrics.active_waiters = usize::MAX;
773        assert!(matches!(
774            count_overflow.register(&snapshot, range, cancellation, FetchTerminalGate::default(),),
775            Err(WindowsCloudFilesError::ActiveFetchWaiterCountOverflow)
776        ));
777        assert_eq!(lock(&count_overflow.state).next_id, 0);
778        assert!(lock(&count_overflow.state).waiters.is_empty());
779    }
780
781    #[tokio::test]
782    async fn watchdog_poll_cancels_pending_waiter_and_progress_refreshes_deadline() {
783        let snapshot = fetch_snapshot();
784        let range = ByteRange::new(0, 4096).expect("range fixture should be valid");
785        let revision =
786            ContentRevision::from_slice(b"revision").expect("revision fixture should be valid");
787        let coordinator = HydrationCoordinator::new(Arc::new(ImmediateBackend));
788        let waiter = coordinator
789            .request(HydrationRequest::range(
790                item_key(),
791                revision,
792                4096,
793                range,
794                Alignment::ONE,
795                generation(),
796            ))
797            .expect("waiter fixture should register");
798        let config = WindowsFetchDataWatchdogConfig::new(std::time::Duration::from_secs(5))
799            .expect("watchdog fixture should be valid");
800        let registry = WindowsFetchDataWaiterRegistry::with_watchdog_config(config);
801        let start = std::time::Instant::now();
802        let registration = registry
803            .register_at(
804                &snapshot,
805                range,
806                waiter.cancellation_handle(),
807                FetchTerminalGate::default(),
808                start,
809            )
810            .expect("registry fixture should register");
811        let progress = WindowsFetchDataProgress::new(4096, 1).expect("progress should be valid");
812        assert_eq!(
813            registry
814                .report_progress(
815                    snapshot.info(),
816                    progress,
817                    start + std::time::Duration::from_secs(4)
818                )
819                .expect("progress should refresh watchdog"),
820            1
821        );
822        assert_eq!(
823            registry
824                .poll_watchdog(start + std::time::Duration::from_secs(8))
825                .cancelled(),
826            0
827        );
828        let outcome = registry.poll_watchdog(start + std::time::Duration::from_secs(9));
829        assert_eq!(outcome.cancelled(), 1);
830        assert!(registration.watchdog_timed_out());
831        assert_eq!(registry.metrics().watchdog_timeouts(), 1);
832        assert!(matches!(
833            waiter.wait().await,
834            Err(HydrationError::Cancelled)
835        ));
836        drop(registration);
837    }
838
839    #[test]
840    fn progress_regression_is_rejected_before_waiter_state_changes() {
841        let snapshot = fetch_snapshot();
842        let range = ByteRange::new(0, 4096).expect("range fixture should be valid");
843        let revision =
844            ContentRevision::from_slice(b"revision").expect("revision fixture should be valid");
845        let coordinator = HydrationCoordinator::new(Arc::new(ImmediateBackend));
846        let waiter = coordinator
847            .request(HydrationRequest::range(
848                item_key(),
849                revision,
850                4096,
851                range,
852                Alignment::ONE,
853                generation(),
854            ))
855            .expect("waiter fixture should register");
856        let registry = WindowsFetchDataWaiterRegistry::new();
857        let start = Instant::now();
858        let registration = registry
859            .register_at(
860                &snapshot,
861                range,
862                waiter.cancellation_handle(),
863                FetchTerminalGate::default(),
864                start,
865            )
866            .expect("registry fixture should register");
867        registry
868            .report_progress(
869                snapshot.info(),
870                WindowsFetchDataProgress::new(4096, 2048).expect("progress should be valid"),
871                start,
872            )
873            .expect("first progress should be accepted");
874        assert!(matches!(
875            registry.report_progress(
876                snapshot.info(),
877                WindowsFetchDataProgress::new(4096, 1024).expect("sample should be valid"),
878                start + std::time::Duration::from_secs(1),
879            ),
880            Err(WindowsCloudFilesError::InvalidProviderProgress { .. })
881        ));
882        assert_eq!(registry.metrics().progress_callbacks(), 2);
883        assert_eq!(registry.metrics().progress_updates(), 1);
884        drop(registration);
885    }
886}