From 9e5e78ebef4e370b424e7829c66ef22e3af55815 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 10 Oct 2026 10:42:12 +0800 Subject: [PATCH] fix(runtime): preserve typed fetch timeout for late inner results --- moli-core/src/runtime/fetch_deadline.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/moli-core/src/runtime/fetch_deadline.rs b/moli-core/src/runtime/fetch_deadline.rs index 628142308c..556b98dd90 100644 --- a/moli-core/src/runtime/fetch_deadline.rs +++ b/moli-core/src/runtime/fetch_deadline.rs @@ -137,9 +137,11 @@ impl FetchDeadline { where F: Future>, { + // Tokio polls the inner future first. A result queued after the deadline + // must not replace the typed timeout when both are ready on the next poll. match tokio::time::timeout_at(self.at, future).await { - Ok(result) => result, - Err(_) => Err(anyhow::Error::new(FetchReadinessTimeout::new( + Ok(result) if tokio::time::Instant::now() < self.at => result, + _ => Err(anyhow::Error::new(FetchReadinessTimeout::new( self.timeout, phase, ))), @@ -193,6 +195,24 @@ mod tests { Ok(()) } + #[tokio::test] + async fn expired_deadline_wins_over_a_ready_inner_result() -> Result<()> { + for result in [Ok(()), Err(anyhow::anyhow!("inner operation timed out"))] { + let deadline = FetchDeadline::new(Duration::ZERO)?; + let error = deadline + .wait( + FetchTimeoutPhase::WaitingForScript, + std::future::ready(result), + ) + .await + .unwrap_err(); + let timeout = error.downcast_ref::().unwrap(); + assert_eq!(timeout.timeout(), Duration::ZERO); + assert_eq!(timeout.phase(), FetchTimeoutPhase::WaitingForScript); + } + Ok(()) + } + #[test] fn impossible_deadline_range_is_an_anyhow_error() { let error = FetchDeadline::new(Duration::MAX).unwrap_err();