1use crate::http::{
10 FileId, GetResponse, HttpClient, HttpError, HttpHeaders, HttpRequestBuilder, HttpResponse,
11};
12use async_stream::stream;
13use fast_pull::{ProgressEntry, PullResult, PullStream, Puller};
14use parking_lot::Mutex;
15use std::{fmt::Debug, future::Future, sync::Arc};
16use url::Url;
17
18pub struct HttpPuller<Client: HttpClient> {
24 client: Client,
25 url: Arc<Url>,
26 resp: Option<Arc<Mutex<Option<GetResponse<Client>>>>>,
27 file_id: FileId,
28}
29impl<C: HttpClient> Clone for HttpPuller<C> {
30 fn clone(&self) -> Self {
31 Self {
32 client: self.client.clone(),
33 url: self.url.clone(),
34 resp: self.resp.clone(),
35 file_id: self.file_id.clone(),
36 }
37 }
38}
39impl<Client: HttpClient> HttpPuller<Client> {
40 pub const fn new(
49 url: Arc<Url>,
50 client: Client,
51 resp: Option<Arc<Mutex<Option<GetResponse<Client>>>>>,
52 file_id: FileId,
53 ) -> Self {
54 Self {
55 client,
56 url,
57 resp,
58 file_id,
59 }
60 }
61}
62impl<Client: HttpClient> Debug for HttpPuller<Client> {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("HttpPuller")
65 .field("url", &self.url)
66 .field("file_id", &self.file_id)
67 .field("client", &"...")
68 .field("resp", &"...")
69 .finish()
70 }
71}
72
73impl<Client: HttpClient> Puller for HttpPuller<Client> {
74 type Error = HttpError<Client>;
75 fn pull(
76 &mut self,
77 range: Option<&ProgressEntry>,
78 ) -> impl Future<Output = PullResult<impl PullStream<Self::Error>, Self::Error>> + Send {
79 let client = self.client.clone();
80 let url = self.url.clone();
81 let file_id = self.file_id.clone();
82 let mut reused = if range.is_none_or(|r| r.start == 0) {
90 self.resp.as_ref().and_then(|m| m.lock().take())
91 } else {
92 None
93 };
94 let mut range = range.cloned();
99
100 std::future::ready(Ok(Box::pin(stream! {
101 loop {
111 let mut current: GetResponse<Client>;
118 if let Some(resp) = reused.take() {
119 current = resp;
120 } else {
121 match client.get((*url).clone(), range.clone()).send().await {
122 Ok(resp) => current = resp,
123 Err((e, retry)) => {
124 yield Err((HttpError::Request(e), retry));
128 loop {
129 yield Err((HttpError::Irrecoverable, None));
130 }
131 }
132 }
133 }
134
135 let new_file_id = FileId::new(
138 current.headers().get("etag").ok().as_deref(),
139 current.headers().get("last-modified").ok().as_deref(),
140 );
141 if new_file_id != file_id {
142 yield Err((HttpError::MismatchedBody(new_file_id, current), None));
146 loop {
147 yield Err((HttpError::Irrecoverable, None));
148 }
149 }
150
151 loop {
152 match current.chunk().await {
153 Ok(Some(chunk)) => {
154 if let Some(r) = &mut range {
155 r.start += chunk.len() as u64;
156 }
157 yield Ok(chunk);
158 }
159 Ok(None) => return,
160 Err(e) => {
161 yield Err((HttpError::Chunk(e, current), None));
164 match &range {
165 None => loop {
172 yield Err((HttpError::Irrecoverable, None));
173 },
174 Some(_) => break,
177 }
178 }
179 }
180 }
181 }
182 })))
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
189 use std::borrow::Cow;
190 use std::pin::Pin;
191 use std::sync::atomic::{AtomicU64, Ordering};
192 use std::task::{Context, Poll};
193 use std::time::Duration;
194
195 use bytes::Bytes;
196
197 use super::*;
198 use futures::TryStreamExt;
199
200 #[derive(Clone, Debug)]
201 struct MockClient;
202 impl HttpClient for MockClient {
203 type RequestBuilder = MockRequestBuilder;
204 fn get(&self, _url: Url, _range: Option<ProgressEntry>) -> Self::RequestBuilder {
205 MockRequestBuilder
206 }
207 }
208 struct MockRequestBuilder;
209 impl HttpRequestBuilder for MockRequestBuilder {
210 type Response = MockResponse;
211 type RequestError = MockError;
212 fn send(self) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> {
213 std::future::ready(Ok(MockResponse::new()))
214 }
215 }
216 #[derive(Debug)]
217 struct MockResponse {
218 headers: MockHeaders,
219 url: Url,
220 }
221 impl MockResponse {
222 fn new() -> Self {
223 Self {
224 headers: MockHeaders,
225 url: Url::parse("http://mock-url").unwrap(),
226 }
227 }
228 }
229 impl HttpResponse for MockResponse {
230 type Headers = MockHeaders;
231 type ChunkError = MockError;
232 fn headers(&self) -> &Self::Headers {
233 &self.headers
234 }
235 fn url(&self) -> &Url {
236 &self.url
237 }
238 async fn chunk(&mut self) -> Result<Option<Bytes>, Self::ChunkError> {
239 DelayChunk::new().await
240 }
241 }
242 #[derive(Debug)]
243 struct MockHeaders;
244 impl HttpHeaders for MockHeaders {
245 type GetHeaderError = MockError;
246 fn get(&self, _header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError> {
247 Err(MockError)
248 }
249 }
250 #[derive(Debug, thiserror::Error)]
251 #[error("MockError")]
252 struct MockError;
253
254 struct DelayChunk {
255 polled_once: bool,
256 }
257 impl DelayChunk {
258 fn new() -> Self {
259 Self { polled_once: false }
260 }
261 }
262 impl Future for DelayChunk {
263 type Output = Result<Option<Bytes>, MockError>;
264 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
265 if !self.polled_once {
266 println!("Wait... [Mock: simulating network delay with Pending]");
267 self.polled_once = true;
268 cx.waker().wake_by_ref();
269 return Poll::Pending;
270 }
271 println!("Done! [Mock: data arrived, Ready]");
272 Poll::Ready(Ok(Some(Bytes::from_static(b"success"))))
273 }
274 }
275
276 #[tokio::test]
277 async fn test_http_puller_infinite_loop_fix() {
278 let url = Url::parse("http://localhost").unwrap();
279 let client = MockClient;
280 let file_id = FileId::new(None, None);
281 let mut puller = HttpPuller::new(Arc::new(url), client, None, file_id);
282 let range = 0..7;
283 let mut stream = Puller::pull(&mut puller, Some(&range))
284 .await
285 .expect("Failed to create stream");
286 println!("--- Starting HttpPuller test ---");
287 let result =
288 tokio::time::timeout(Duration::from_secs(1), async { stream.try_next().await }).await;
289 match result {
290 Ok(Ok(Some(bytes))) => {
291 println!("Received data: {bytes:?}");
292 assert_eq!(bytes, Bytes::from_static(b"success"));
293 println!("Test passed: HttpPuller correctly handled Pending state!");
294 }
295 e => {
296 panic!(
297 "Test failed: timeout! This indicates HttpPuller may have lost the Future state after receiving Pending and entered an infinite loop. {e:?}"
298 );
299 }
300 }
301 }
302
303 #[derive(Debug)]
304 struct MismatchHeaders;
305 impl HttpHeaders for MismatchHeaders {
306 type GetHeaderError = MockError;
307 fn get(&self, header: &str) -> Result<Cow<'_, str>, Self::GetHeaderError> {
308 if header == "etag" {
309 Ok(Cow::Borrowed("etag-x"))
310 } else {
311 Err(MockError)
312 }
313 }
314 }
315
316 #[derive(Clone, Debug)]
317 struct MismatchClient;
318 impl HttpClient for MismatchClient {
319 type RequestBuilder = MismatchRequestBuilder;
320 fn get(&self, _url: Url, _range: Option<ProgressEntry>) -> Self::RequestBuilder {
321 MismatchRequestBuilder
322 }
323 }
324 struct MismatchRequestBuilder;
325 impl HttpRequestBuilder for MismatchRequestBuilder {
326 type Response = MismatchResponse;
327 type RequestError = MockError;
328 fn send(
329 self,
330 ) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send {
331 std::future::ready(Ok(MismatchResponse::new()))
332 }
333 }
334 #[derive(Debug)]
335 struct MismatchResponse {
336 url: Url,
337 }
338 impl MismatchResponse {
339 fn new() -> Self {
340 Self {
341 url: Url::parse("http://mock-url").unwrap(),
342 }
343 }
344 }
345 impl HttpResponse for MismatchResponse {
346 type Headers = MismatchHeaders;
347 type ChunkError = MockError;
348 fn headers(&self) -> &Self::Headers {
349 &MismatchHeaders
350 }
351 fn url(&self) -> &Url {
352 &self.url
353 }
354 fn chunk(
355 &mut self,
356 ) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send {
357 std::future::ready(Ok(None))
358 }
359 }
360
361 #[derive(Clone, Debug)]
362 struct ReqErrClient;
363 impl HttpClient for ReqErrClient {
364 type RequestBuilder = ReqErrRequestBuilder;
365 fn get(&self, _url: Url, _range: Option<ProgressEntry>) -> Self::RequestBuilder {
366 ReqErrRequestBuilder
367 }
368 }
369 struct ReqErrRequestBuilder;
370 impl HttpRequestBuilder for ReqErrRequestBuilder {
371 type Response = MockResponse;
372 type RequestError = MockError;
373 fn send(
374 self,
375 ) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send {
376 std::future::ready(Err((MockError, None)))
377 }
378 }
379
380 #[derive(Clone, Debug)]
381 struct ChunkErrClient;
382 impl HttpClient for ChunkErrClient {
383 type RequestBuilder = ChunkErrRequestBuilder;
384 fn get(&self, _url: Url, _range: Option<ProgressEntry>) -> Self::RequestBuilder {
385 ChunkErrRequestBuilder
386 }
387 }
388 struct ChunkErrRequestBuilder;
389 impl HttpRequestBuilder for ChunkErrRequestBuilder {
390 type Response = ChunkErrResponse;
391 type RequestError = MockError;
392 fn send(
393 self,
394 ) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send {
395 std::future::ready(Ok(ChunkErrResponse::new()))
396 }
397 }
398 #[derive(Debug)]
399 struct ChunkErrResponse {
400 url: Url,
401 }
402 impl ChunkErrResponse {
403 fn new() -> Self {
404 Self {
405 url: Url::parse("http://mock-url").unwrap(),
406 }
407 }
408 }
409 impl HttpResponse for ChunkErrResponse {
410 type Headers = MockHeaders;
411 type ChunkError = MockError;
412 fn headers(&self) -> &Self::Headers {
413 &MockHeaders
414 }
415 fn url(&self) -> &Url {
416 &self.url
417 }
418 fn chunk(
419 &mut self,
420 ) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send {
421 std::future::ready(Err(MockError))
422 }
423 }
424
425 #[test]
426 fn http_puller_debug_formats() {
427 let url = Url::parse("http://localhost").unwrap();
428 let puller = HttpPuller::new(Arc::new(url), MockClient, None, FileId::new(None, None));
429 let s = format!("{puller:?}");
430 assert!(s.contains("HttpPuller"));
431 assert!(s.contains("url"));
432 }
433
434 #[test]
435 fn mock_response_url_is_accessible() {
436 let r = MockResponse::new();
437 assert_eq!(r.url().as_str(), "http://mock-url/");
438 }
439
440 #[tokio::test]
441 async fn test_http_puller_mismatched_body() {
442 let url = Url::parse("http://localhost").unwrap();
443 let client = MismatchClient;
444 let file_id = FileId::new(None, None);
445 let mut puller = HttpPuller::new(Arc::new(url), client, None, file_id);
446 let mut stream = Puller::pull(&mut puller, None).await.unwrap();
447 let result = stream.try_next().await;
448 assert!(matches!(
449 result,
450 Err((HttpError::MismatchedBody(_, _), None))
451 ));
452 }
453
454 #[tokio::test]
455 async fn test_http_puller_request_error() {
456 let url = Url::parse("http://localhost").unwrap();
457 let client = ReqErrClient;
458 let file_id = FileId::new(None, None);
459 let mut puller = HttpPuller::new(Arc::new(url), client, None, file_id);
460 let mut stream = Puller::pull(&mut puller, None).await.unwrap();
461 let result = stream.try_next().await;
462 assert!(matches!(result, Err((HttpError::Request(_), None))));
463 }
464
465 #[tokio::test]
466 async fn test_http_puller_chunk_error_then_irrecoverable() {
467 let url = Url::parse("http://localhost").unwrap();
468 let client = ChunkErrClient;
469 let file_id = FileId::new(None, None);
470 let mut puller = HttpPuller::new(Arc::new(url), client, None, file_id);
471 let mut stream = Puller::pull(&mut puller, None).await.unwrap();
472 let first = stream.try_next().await;
475 assert!(matches!(first, Err((HttpError::Chunk(_, _), None))));
476 let second = stream.try_next().await;
479 assert!(matches!(second, Err((HttpError::Irrecoverable, None))));
480 }
481
482 #[tokio::test]
483 async fn test_http_puller_error_stream_never_terminates() {
484 let url = Url::parse("http://localhost").unwrap();
488 let client = ChunkErrClient;
489 let file_id = FileId::new(None, None);
490 let mut puller = HttpPuller::new(Arc::new(url), client, None, file_id);
491 let mut stream = Puller::pull(&mut puller, None).await.unwrap();
492 assert!(matches!(
494 stream.try_next().await,
495 Err((HttpError::Chunk(_, _), None))
496 ));
497 for _ in 0..5 {
499 assert!(matches!(
500 stream.try_next().await,
501 Err((HttpError::Irrecoverable, None))
502 ));
503 }
504 }
505
506 #[tokio::test]
507 async fn test_http_puller_partial_range_request() {
508 let url = Url::parse("http://localhost").unwrap();
509 let client = MockClient;
510 let file_id = FileId::new(None, None);
511 let mut puller = HttpPuller::new(Arc::new(url), client, None, file_id);
512 let range = 10..100;
513 let mut stream = Puller::pull(&mut puller, Some(&range)).await.unwrap();
514 let result = tokio::time::timeout(Duration::from_secs(1), stream.try_next()).await;
515 match result {
516 Ok(Ok(Some(bytes))) => assert_eq!(bytes, Bytes::from_static(b"success")),
517 e => panic!("expected a successful chunk, got {e:?}"),
518 }
519 }
520
521 #[tokio::test]
522 async fn test_http_puller_reuses_response() {
523 let url = Url::parse("http://localhost").unwrap();
524 let resp = Some(Arc::new(parking_lot::Mutex::new(Some(MockResponse::new()))));
527 let file_id = FileId::new(None, None);
528 let mut puller = HttpPuller::new(Arc::new(url), MockClient, resp, file_id);
529 let mut stream = Puller::pull(&mut puller, None).await.unwrap();
530 let result = tokio::time::timeout(Duration::from_secs(1), stream.try_next()).await;
531 match result {
532 Ok(Ok(Some(bytes))) => assert_eq!(bytes, Bytes::from_static(b"success")),
533 e => panic!("expected the reused response to yield data, got {e:?}"),
534 }
535 }
536
537 static RESUME_RANGE_START: AtomicU64 = AtomicU64::new(0);
540
541 #[derive(Clone, Debug)]
542 struct ResumeClient;
543 impl HttpClient for ResumeClient {
544 type RequestBuilder = ResumeRequestBuilder;
545 fn get(&self, _url: Url, range: Option<ProgressEntry>) -> Self::RequestBuilder {
546 if let Some(r) = range {
547 RESUME_RANGE_START.store(r.start, Ordering::SeqCst);
548 }
549 ResumeRequestBuilder
550 }
551 }
552 struct ResumeRequestBuilder;
553 impl HttpRequestBuilder for ResumeRequestBuilder {
554 type Response = ResumeResponse;
555 type RequestError = MockError;
556 fn send(
557 self,
558 ) -> impl Future<Output = PullResult<Self::Response, Self::RequestError>> + Send {
559 std::future::ready(Ok(ResumeResponse::new()))
560 }
561 }
562 #[derive(Debug)]
563 struct ResumeResponse {
564 url: Url,
565 calls: u32,
566 }
567 impl ResumeResponse {
568 fn new() -> Self {
569 Self {
570 url: Url::parse("http://mock-url").unwrap(),
571 calls: 0,
572 }
573 }
574 }
575 impl HttpResponse for ResumeResponse {
576 type Headers = MockHeaders;
577 type ChunkError = MockError;
578 fn headers(&self) -> &Self::Headers {
579 &MockHeaders
580 }
581 fn url(&self) -> &Url {
582 &self.url
583 }
584 fn chunk(
585 &mut self,
586 ) -> impl Future<Output = Result<Option<Bytes>, Self::ChunkError>> + Send {
587 self.calls += 1;
588 let first = self.calls == 1;
589 let fut = async move {
590 if first {
591 Ok(Some(Bytes::from_static(b"success")))
592 } else {
593 Err(MockError)
594 }
595 };
596 Box::pin(fut) as Pin<Box<dyn Future<Output = Result<Option<Bytes>, MockError>> + Send>>
597 }
598 }
599
600 #[tokio::test]
601 async fn test_resume_range_advances_after_chunk_error() {
602 let url = Url::parse("http://localhost").unwrap();
603 let file_id = FileId::new(None, None);
604 let mut puller = HttpPuller::new(Arc::new(url), ResumeClient, None, file_id);
605 let range = 0..100;
606 let mut stream = Puller::pull(&mut puller, Some(&range)).await.unwrap();
607 let first = stream.try_next().await;
609 assert!(matches!(first, Ok(Some(_))));
610 let second = stream.try_next().await;
612 assert!(matches!(second, Err((HttpError::Chunk(_, _), None))));
613 let third = stream.try_next().await;
615 assert!(matches!(third, Ok(Some(_))));
616 assert_eq!(RESUME_RANGE_START.load(Ordering::SeqCst), 7);
617 }
618}