Loading src/quic.cpp +28 −7 Original line number Diff line number Diff line Loading @@ -6315,25 +6315,46 @@ void quic::sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, // 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. // // A zero return is not only a cwnd stall (which the size_t variant // already waits out itself) but also an exhausted peer flow-control // window (MAX_DATA / MAX_STREAM_DATA), which returns 0 immediately. // Lifting that needs a full round trip to the peer, so the stall is // bounded by time without progress, not by a retry count: 50 back-to-back // non-blocking pumpIncoming() calls finish in microseconds, far below // any real network RTT, and used to abandon the stream after ~32KB (the // peer's initial stream window) -- leaving it neither finished nor // reset, so the client hung on it forever. static constexpr auto kStallTimeout = std::chrono::seconds(10); static constexpr int kStallWaitSliceMs = 20; // Per thread, not a member: several threads may be sending on different // streams of this connection at once (see _pump_socketwait's comment for // why one socketwait must not be shared across concurrent waiters). thread_local std::unique_ptr<socketwait> stall_wait; size_t offset = 0; size_t remaining = data.size(); int stall_count = 0; auto last_progress = std::chrono::steady_clock::now(); while (remaining > 0) { size_t sent = sendStreamData(stream_id, data.data() + offset, remaining, fin); if (sent == 0) { if (++stall_count > 50) { // Congestion control stall exceeded — data was not fully sent. // Throw so the caller knows the stream is incomplete. ConnectionState st = _conn_state.load(); if (st == ConnectionState::Closed || st == ConnectionState::Draining || std::chrono::steady_clock::now() - last_progress > kStallTimeout) { // Data was not fully sent. Throw so the caller knows the // stream is incomplete. NetException exception; exception[NetException::Error] << "sendStreamData: congestion stall, " exception[NetException::Error] << "sendStreamData: send stall, " << remaining << " of " << data.size() << " bytes unsent on stream " << stream_id; throw exception; } // Pump incoming to process ACKs and open congestion window // Wait (bounded) for ACK / MAX_DATA / MAX_STREAM_DATA to arrive, // then process it. if (!stall_wait) stall_wait = std::make_unique<socketwait>(); stall_wait->waitRead(*this, kStallWaitSliceMs); pumpIncoming(); continue; } stall_count = 0; last_progress = std::chrono::steady_clock::now(); offset += sent; remaining -= sent; } Loading Loading
src/quic.cpp +28 −7 Original line number Diff line number Diff line Loading @@ -6315,25 +6315,46 @@ void quic::sendStreamData(uint64_t stream_id, const std::vector<uint8_t>& data, // 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. // // A zero return is not only a cwnd stall (which the size_t variant // already waits out itself) but also an exhausted peer flow-control // window (MAX_DATA / MAX_STREAM_DATA), which returns 0 immediately. // Lifting that needs a full round trip to the peer, so the stall is // bounded by time without progress, not by a retry count: 50 back-to-back // non-blocking pumpIncoming() calls finish in microseconds, far below // any real network RTT, and used to abandon the stream after ~32KB (the // peer's initial stream window) -- leaving it neither finished nor // reset, so the client hung on it forever. static constexpr auto kStallTimeout = std::chrono::seconds(10); static constexpr int kStallWaitSliceMs = 20; // Per thread, not a member: several threads may be sending on different // streams of this connection at once (see _pump_socketwait's comment for // why one socketwait must not be shared across concurrent waiters). thread_local std::unique_ptr<socketwait> stall_wait; size_t offset = 0; size_t remaining = data.size(); int stall_count = 0; auto last_progress = std::chrono::steady_clock::now(); while (remaining > 0) { size_t sent = sendStreamData(stream_id, data.data() + offset, remaining, fin); if (sent == 0) { if (++stall_count > 50) { // Congestion control stall exceeded — data was not fully sent. // Throw so the caller knows the stream is incomplete. ConnectionState st = _conn_state.load(); if (st == ConnectionState::Closed || st == ConnectionState::Draining || std::chrono::steady_clock::now() - last_progress > kStallTimeout) { // Data was not fully sent. Throw so the caller knows the // stream is incomplete. NetException exception; exception[NetException::Error] << "sendStreamData: congestion stall, " exception[NetException::Error] << "sendStreamData: send stall, " << remaining << " of " << data.size() << " bytes unsent on stream " << stream_id; throw exception; } // Pump incoming to process ACKs and open congestion window // Wait (bounded) for ACK / MAX_DATA / MAX_STREAM_DATA to arrive, // then process it. if (!stall_wait) stall_wait = std::make_unique<socketwait>(); stall_wait->waitRead(*this, kStallWaitSliceMs); pumpIncoming(); continue; } stall_count = 0; last_progress = std::chrono::steady_clock::now(); offset += sent; remaining -= sent; } Loading