Loading debian/changelog +0 −10 Original line number Diff line number Diff line libnetplus (20260503+4) unstable; urgency=high * QUIC: CRITICAL FIX — flushBatch() was unconditionally clearing all queued packets even when sendmmsg() returned a partial batch count, causing silent packet loss (dropped STORE_BLOCK responses, timeouts on write-heavy traffic). Now only successfully sent packets are removed; unsent packets remain queued for retry. -- Jan Koester <jan.koester@tuxist.de> Sat, 03 May 2026 18:30:00 +0200 libnetplus (20260502+1) unstable; urgency=medium * QUIC: add udp::recvBatchAddr() — batch receive with peer addresses Loading src/quic.cpp +25 −4 Original line number Diff line number Diff line Loading @@ -3701,9 +3701,28 @@ void quic::checkLossAndRetransmit() { // ============================================================================ void quic::sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, bool fin) { // Delegate to the fast-path variant which uses batching, CC gating, // and ACK piggybacking for much better throughput. sendStreamData(stream_id, data.data(), data.size(), fin); // Retry until all data is sent or connection is lost. // The size_t variant may return early when congestion control blocks; // we must loop to ensure the full message (including FIN) reaches the peer. size_t offset = 0; size_t remaining = data.size(); int stall_count = 0; while (remaining > 0) { size_t sent = sendStreamData(stream_id, data.data() + offset, remaining, fin); if (sent == 0) { if (++stall_count > 50) break; // give up after ~5s of total CC stall // Pump incoming to process ACKs and open congestion window pumpIncoming(); continue; } stall_count = 0; offset += sent; remaining -= sent; } // Handle FIN-only (empty data with fin flag) if (data.empty() && fin) { sendStreamData(stream_id, data.data(), 0, true); } } // ============================================================================ Loading Loading @@ -4808,7 +4827,9 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, flushBatch(); } if (fin) { // Only mark FIN if we actually sent all requested data (FIN is on the last chunk). // If CC broke the loop early, the FIN was never put on the wire. if (fin && sent_total >= send_len) { stream.send_fin = true; // Auto-cleanup: both sides FIN'd → erase stream & replenish peer's stream budget if (stream.recv_fin) { Loading Loading
debian/changelog +0 −10 Original line number Diff line number Diff line libnetplus (20260503+4) unstable; urgency=high * QUIC: CRITICAL FIX — flushBatch() was unconditionally clearing all queued packets even when sendmmsg() returned a partial batch count, causing silent packet loss (dropped STORE_BLOCK responses, timeouts on write-heavy traffic). Now only successfully sent packets are removed; unsent packets remain queued for retry. -- Jan Koester <jan.koester@tuxist.de> Sat, 03 May 2026 18:30:00 +0200 libnetplus (20260502+1) unstable; urgency=medium * QUIC: add udp::recvBatchAddr() — batch receive with peer addresses Loading
src/quic.cpp +25 −4 Original line number Diff line number Diff line Loading @@ -3701,9 +3701,28 @@ void quic::checkLossAndRetransmit() { // ============================================================================ void quic::sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, bool fin) { // Delegate to the fast-path variant which uses batching, CC gating, // and ACK piggybacking for much better throughput. sendStreamData(stream_id, data.data(), data.size(), fin); // Retry until all data is sent or connection is lost. // The size_t variant may return early when congestion control blocks; // we must loop to ensure the full message (including FIN) reaches the peer. size_t offset = 0; size_t remaining = data.size(); int stall_count = 0; while (remaining > 0) { size_t sent = sendStreamData(stream_id, data.data() + offset, remaining, fin); if (sent == 0) { if (++stall_count > 50) break; // give up after ~5s of total CC stall // Pump incoming to process ACKs and open congestion window pumpIncoming(); continue; } stall_count = 0; offset += sent; remaining -= sent; } // Handle FIN-only (empty data with fin flag) if (data.empty() && fin) { sendStreamData(stream_id, data.data(), 0, true); } } // ============================================================================ Loading Loading @@ -4808,7 +4827,9 @@ size_t quic::sendStreamData(uint64_t stream_id, const uint8_t* data, size_t len, flushBatch(); } if (fin) { // Only mark FIN if we actually sent all requested data (FIN is on the last chunk). // If CC broke the loop early, the FIN was never put on the wire. if (fin && sent_total >= send_len) { stream.send_fin = true; // Auto-cleanup: both sides FIN'd → erase stream & replenish peer's stream budget if (stream.recv_fin) { Loading