@@ -2,9 +2,19 @@ use std::collections::HashMap;
22use std:: collections:: hash_map:: Entry ;
33use std:: sync:: { Arc , Mutex } ;
44
5+ use thiserror:: Error ;
56use tokio:: sync:: oneshot;
67
7- use crate :: fetcher:: { DedupError , Fetcher } ;
8+ use crate :: fetcher:: Fetcher ;
9+
10+ #[ derive( Debug , Clone , Error ) ]
11+ #[ non_exhaustive]
12+ pub enum DedupError < E > {
13+ #[ error( "fetch failed: {0}" ) ]
14+ Load ( #[ source] E ) ,
15+ #[ error( "the fetcher panicked while loading" ) ]
16+ Panic ,
17+ }
818
919// Aliased to keep the nested generics under clippy's type-complexity lint.
1020type Responder < F > =
@@ -58,14 +68,15 @@ impl<F: Fetcher> Deduplicator<F> {
5868
5969 match reply. await {
6070 Ok ( result) => result,
61- // Sender gone without a reply: run_fetch died abnormally, report don't hang .
71+ // Sender gone without a reply: run_fetch died abnormally, report instead of hanging .
6272 Err ( _) => Err ( DedupError :: Panic ) ,
6373 }
6474 }
6575}
6676
67- // A load panic becomes a JoinError (never a hung waiter); assumes the input's
68- // Clone/Hash/Eq do not panic.
77+ // A load panic becomes a JoinError (never a hung waiter).
78+ // The Fetcher and Input trait impls (Clone, Hash, Eq) run outside that isolation:
79+ // they must not panic, or the entry orphans and this input's later callers hang.
6980async fn run_fetch < F : Fetcher > ( fetcher : F , in_flight : InFlight < F > , input : F :: Input ) {
7081 let key = input. clone ( ) ;
7182 let outcome = tokio:: spawn ( async move { fetcher. load ( input) . await } ) . await ;
@@ -254,8 +265,8 @@ mod tests {
254265 }
255266 }
256267
257- // A fetch panic reaches all callers as DedupError::Panic and frees the input,
258- // so the next call refetches. Looped on virtual time for the cleanup race;
268+ // A fetch panic reaches all callers as DedupError::Panic and frees the input,
269+ // so the next call refetches. Looped on virtual time for the cleanup race;
259270 // the printed panic is expected.
260271 #[ tokio:: test( start_paused = true ) ]
261272 async fn a_fetch_panic_reaches_callers_and_frees_the_input ( ) {
@@ -324,7 +335,7 @@ mod tests {
324335 ( fetcher, calls, completed, entered, finished)
325336 }
326337
327- // The initiator's wait times out, but the started fetch is not cancelled:
338+ // The initiator's wait times out, but the started fetch is not cancelled:
328339 // the follower still gets the value and the fetch ran once. Auto-advance drives the clock.
329340 #[ tokio:: test( start_paused = true ) ]
330341 async fn abandoning_the_initiator_does_not_cancel_the_fetch ( ) {
@@ -348,7 +359,7 @@ mod tests {
348359 }
349360 }
350361
351- // All waiters leave after the fetch started: it still runs to completion,
362+ // All waiters leave after the fetch started: it still runs to completion,
352363 // and delivery into the closed receiver does not panic.
353364 #[ tokio:: test( start_paused = true ) ]
354365 async fn all_waiters_leaving_does_not_cancel_the_started_fetch ( ) {
@@ -364,7 +375,7 @@ mod tests {
364375 entered. notified ( ) . await ;
365376
366377 caller. abort ( ) ;
367- let _ = caller. await ;
378+ assert ! ( caller. await . unwrap_err ( ) . is_cancelled ( ) ) ;
368379
369380 tokio:: time:: advance ( HOLD ) . await ;
370381 finished. notified ( ) . await ;
@@ -377,4 +388,27 @@ mod tests {
377388 ) ;
378389 }
379390 }
391+
392+ // One of several waiters for an input departs; the rest still get the value
393+ // from the single shared fetch, and delivery into the departed receiver does
394+ // not panic. Auto-advance drives the virtual clock.
395+ #[ tokio:: test( start_paused = true ) ]
396+ async fn a_departed_waiter_does_not_starve_the_others ( ) {
397+ const HOLD : Duration = Duration :: from_secs ( 10 ) ;
398+ const SHORT : Duration = Duration :: from_secs ( 1 ) ;
399+ for _ in 0 ..50 {
400+ let ( fetcher, calls, _completed, _entered, _finished) = slow ( HOLD ) ;
401+ let d = Deduplicator :: new ( fetcher) ;
402+
403+ let departing = tokio:: time:: timeout ( SHORT , d. call ( 1 ) ) ;
404+ let survivor_a = d. call ( 1 ) ;
405+ let survivor_b = d. call ( 1 ) ;
406+ let ( gone, a, b) = tokio:: join!( departing, survivor_a, survivor_b) ;
407+
408+ assert ! ( gone. is_err( ) , "one waiter timed out and departed" ) ;
409+ assert_eq ! ( a. unwrap( ) , 1 , "a surviving waiter still gets the value" ) ;
410+ assert_eq ! ( b. unwrap( ) , 1 , "the other survivor too" ) ;
411+ assert_eq ! ( calls. load( Ordering :: SeqCst ) , 1 , "one shared fetch for all" ) ;
412+ }
413+ }
380414}
0 commit comments