diff --git a/CHANGELOG.md b/CHANGELOG.md index eb2e7627..6bd1973c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to virtkit will be documented in this file. ## [Unreleased] +### Fixed + +- **vk-registry no longer stalls about 40 ms on each small read.** A client reading many + small objects over one connection, such as a build cache fetching its entries, gets each + one back without waiting for its own delayed acknowledgement. + ## [0.105.0] - 2026-11-05 ### Added diff --git a/vk-registry/src/lib.rs b/vk-registry/src/lib.rs index 06bda56e..7906c0c5 100644 --- a/vk-registry/src/lib.rs +++ b/vk-registry/src/lib.rs @@ -4078,6 +4078,11 @@ async fn serve_refusing( continue; } }; + // Small responses go out as a header write then a body write; with Nagle on, the + // second waits for the client's delayed ACK, about 40 ms on Linux. Best effort: a + // socket that refuses the option is still served correctly, only slower, and one + // the client already reset fails on its first read anyway. + let _ = stream.set_nodelay(true); let Some(peer_slot) = PeerSlot::acquire(&peers, peer.ip(), limits.per_peer) else { drop(slot); let key = peer_key(peer.ip()); diff --git a/vk-registry/tests/dav_e2e.rs b/vk-registry/tests/dav_e2e.rs index 45743f36..7de767b2 100644 --- a/vk-registry/tests/dav_e2e.rs +++ b/vk-registry/tests/dav_e2e.rs @@ -1831,3 +1831,34 @@ async fn a_directory_delete_waits_for_the_store_lock_without_holding_up_writes() assert!(!dir.join("store/repos/files/gone").exists()); let _ = std::fs::remove_dir_all(&dir); } + +/// A small object read back again and again on one connection comes back in about a +/// millisecond, not after the client's delayed ACK: the server sends a response's head and +/// body as two writes, and with Nagle's algorithm on, the second waits for that ACK (about +/// 40 ms on Linux), which made a cache of many small entries slow to read. +#[tokio::test] +async fn small_objects_are_answered_without_waiting_for_a_delayed_ack() { + let _ = rustls::crypto::ring::default_provider().install_default(); + let dir = tmp("nodelay"); + let url = spawn(open_state(&dir)); + let c = client(); + let object = format!("{url}/dav/files/cache/ab/small"); + let put = c.put(&object).body(vec![7u8; 86]).send().await.unwrap(); + assert_eq!(put.status(), 201); + + let mut took = Vec::new(); + for _ in 0..21 { + let start = std::time::Instant::now(); + let resp = c.get(&object).send().await.unwrap(); + assert_eq!(resp.status(), 200); + assert_eq!(resp.bytes().await.unwrap().len(), 86); + took.push(start.elapsed()); + } + took.sort(); + let median = took[took.len() / 2]; + assert!( + median < std::time::Duration::from_millis(20), + "median GET of a small object took {median:?} on one connection" + ); + let _ = std::fs::remove_dir_all(&dir); +}