Skip to content

Commit 4765d43

Browse files
committed
chore: add test to assert retries
1 parent 41f4db9 commit 4765d43

1 file changed

Lines changed: 93 additions & 0 deletions

File tree

src/upload/uploader.rs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,4 +378,97 @@ mod tests {
378378
)
379379
.await;
380380
}
381+
382+
/// Spawns a local TCP server that always answers `503` (a retryable status) and
383+
/// counts how many connections (= upload attempts) it receives.
384+
fn spawn_mock_returning_503() -> (String, std::sync::Arc<std::sync::atomic::AtomicUsize>) {
385+
use std::io::{Read, Write};
386+
use std::net::TcpListener;
387+
use std::sync::Arc;
388+
use std::sync::atomic::{AtomicUsize, Ordering};
389+
390+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
391+
let url = format!("http://{}/upload", listener.local_addr().unwrap());
392+
let hits = Arc::new(AtomicUsize::new(0));
393+
394+
let hits_loop = hits.clone();
395+
std::thread::spawn(move || {
396+
for stream in listener.incoming() {
397+
let Ok(mut stream) = stream else { continue };
398+
hits_loop.fetch_add(1, Ordering::SeqCst);
399+
std::thread::spawn(move || {
400+
let mut buf = [0u8; 2048];
401+
let _ = stream.read(&mut buf);
402+
let body = "transient";
403+
let resp = format!(
404+
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
405+
body.len(),
406+
body
407+
);
408+
let _ = stream.write_all(resp.as_bytes());
409+
});
410+
}
411+
});
412+
413+
(url, hits)
414+
}
415+
416+
fn upload_data_for(url: String) -> UploadData {
417+
UploadData {
418+
status: "success".to_string(),
419+
upload_url: url,
420+
run_id: "test-run".to_string(),
421+
}
422+
}
423+
424+
/// WallTime/Memory path: an on-disk archive is streamed via `STREAMING_CLIENT`,
425+
/// which has no retry middleware, so `send_streamed_with_retry` rebuilds the
426+
/// stream on each attempt. A transient `503` must be retried 3 times (4 attempts
427+
/// total). Slow (~7s) because of the default exponential backoff (1s, 2s, 4s).
428+
#[tokio::test]
429+
async fn streamed_upload_is_retried() {
430+
use std::sync::atomic::Ordering;
431+
432+
let (url, hits) = spawn_mock_returning_503();
433+
434+
let path = tempfile::NamedTempFile::new()
435+
.unwrap()
436+
.into_temp_path()
437+
.keep()
438+
.unwrap();
439+
std::fs::write(&path, b"profile-archive").unwrap();
440+
let archive = ProfileArchive::new_uncompressed_on_disk(path).unwrap();
441+
442+
let result = upload_profile_archive(&upload_data_for(url), archive).await;
443+
444+
assert!(result.is_err(), "a 503 should surface as an error after retries");
445+
assert_eq!(
446+
hits.load(Ordering::SeqCst),
447+
4,
448+
"streamed upload should be attempted 4 times (1 + 3 retries)"
449+
);
450+
}
451+
452+
/// Valgrind path: an in-memory archive goes through `REQUEST_CLIENT`, which retries
453+
/// transient failures 3 times. Confirms the same `503` mock is genuinely retryable,
454+
/// so the single attempt above is due to the client, not the status code.
455+
///
456+
/// Slow (~7s) because of the default exponential backoff (1s, 2s, 4s).
457+
#[tokio::test]
458+
async fn in_memory_upload_is_retried() {
459+
use std::sync::atomic::Ordering;
460+
461+
let (url, hits) = spawn_mock_returning_503();
462+
463+
let archive = ProfileArchive::new_compressed_in_memory(b"profile-archive".to_vec());
464+
465+
let result = upload_profile_archive(&upload_data_for(url), archive).await;
466+
467+
assert!(result.is_err(), "a 503 should surface as an error");
468+
assert_eq!(
469+
hits.load(Ordering::SeqCst),
470+
4,
471+
"in-memory upload should be attempted 4 times (1 + 3 retries)"
472+
);
473+
}
381474
}

0 commit comments

Comments
 (0)