1use 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#[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 #[must_use]
66 pub const fn active_waiters(self) -> usize {
67 self.active_waiters
68 }
69
70 #[must_use]
72 pub const fn registered_waiters(self) -> u64 {
73 self.registered_waiters
74 }
75
76 #[must_use]
78 pub const fn cancellation_callbacks(self) -> u64 {
79 self.cancellation_callbacks
80 }
81
82 #[must_use]
84 pub const fn unmatched_cancellations(self) -> u64 {
85 self.unmatched_cancellations
86 }
87
88 #[must_use]
90 pub const fn fully_matched_waiters(self) -> u64 {
91 self.fully_matched_waiters
92 }
93
94 #[must_use]
96 pub const fn partially_matched_waiters(self) -> u64 {
97 self.partially_matched_waiters
98 }
99
100 #[must_use]
102 pub const fn cancelled_waiters(self) -> u64 {
103 self.cancelled_waiters
104 }
105
106 #[must_use]
108 pub const fn already_cancelled_waiters(self) -> u64 {
109 self.already_cancelled_waiters
110 }
111
112 #[must_use]
114 pub const fn completion_races(self) -> u64 {
115 self.completion_races
116 }
117
118 #[must_use]
120 pub const fn watchdog_timeouts(self) -> u64 {
121 self.watchdog_timeouts
122 }
123
124 #[must_use]
126 pub const fn progress_callbacks(self) -> u64 {
127 self.progress_callbacks
128 }
129
130 #[must_use]
132 pub const fn progress_updates(self) -> u64 {
133 self.progress_updates
134 }
135}
136
137#[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#[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 #[must_use]
158 pub const fn cancelled(self) -> usize {
159 self.cancelled
160 }
161
162 #[must_use]
164 pub const fn already_cancelled(self) -> usize {
165 self.already_cancelled
166 }
167
168 #[must_use]
170 pub const fn already_completed(self) -> usize {
171 self.already_completed
172 }
173}
174
175impl WindowsFetchDataCancellationOutcome {
176 #[must_use]
178 pub const fn fully_matched(self) -> usize {
179 self.fully_matched
180 }
181
182 #[must_use]
184 pub const fn partially_matched(self) -> usize {
185 self.partially_matched
186 }
187
188 #[must_use]
190 pub const fn cancelled(self) -> usize {
191 self.cancelled
192 }
193
194 #[must_use]
196 pub const fn already_cancelled(self) -> usize {
197 self.already_cancelled
198 }
199
200 #[must_use]
202 pub const fn already_completed(self) -> usize {
203 self.already_completed
204 }
205
206 #[must_use]
208 pub const fn matched(self) -> bool {
209 self.fully_matched != 0 || self.partially_matched != 0
210 }
211}
212
213#[derive(Clone, Default)]
220pub struct WindowsFetchDataWaiterRegistry {
221 state: Arc<Mutex<RegistryState>>,
222}
223
224impl WindowsFetchDataWaiterRegistry {
225 #[must_use]
227 pub fn new() -> Self {
228 Self::default()
229 }
230
231 #[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 #[must_use]
244 pub fn metrics(&self) -> WindowsFetchDataWaiterMetrics {
245 lock(&self.state).metrics
246 }
247
248 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 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 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 #[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}