diff --git a/net/dcsctp/socket/dcsctp_socket.cc b/net/dcsctp/socket/dcsctp_socket.cc index 7ccca5cbda..cc9a71eae8 100644 --- a/net/dcsctp/socket/dcsctp_socket.cc +++ b/net/dcsctp/socket/dcsctp_socket.cc @@ -1391,6 +1391,17 @@ void DcSctpSocket::HandleSack(const CommonHeader& header, if (tcb_->retransmission_queue().HandleSack(now, sack)) { MaybeSendShutdownOrAck(); + // Receiving an ACK may make the socket go into fast recovery mode. + // https://datatracker.ietf.org/doc/html/rfc4960#section-7.2.4 + // "Determine how many of the earliest (i.e., lowest TSN) DATA chunks + // marked for retransmission will fit into a single packet, subject to + // constraint of the path MTU of the destination transport address to + // which the packet is being sent. Call this value K. Retransmit those K + // DATA chunks in a single packet. When a Fast Retransmit is being + // performed, the sender SHOULD ignore the value of cwnd and SHOULD NOT + // delay retransmission for this single packet." + tcb_->MaybeSendFastRetransmit(); + // Receiving an ACK will decrease outstanding bytes (maybe now below // cwnd?) or indicate packet loss that may result in sending FORWARD-TSN. tcb_->SendBufferedPackets(now); diff --git a/net/dcsctp/socket/transmission_control_block.cc b/net/dcsctp/socket/transmission_control_block.cc index 539fde7300..78331d5e96 100644 --- a/net/dcsctp/socket/transmission_control_block.cc +++ b/net/dcsctp/socket/transmission_control_block.cc @@ -102,6 +102,33 @@ void TransmissionControlBlock::MaybeSendForwardTsn(SctpPacket::Builder& builder, } } +void TransmissionControlBlock::MaybeSendFastRetransmit() { + if (!retransmission_queue_.has_data_to_be_fast_retransmitted()) { + return; + } + + // https://datatracker.ietf.org/doc/html/rfc4960#section-7.2.4 + // "Determine how many of the earliest (i.e., lowest TSN) DATA chunks marked + // for retransmission will fit into a single packet, subject to constraint of + // the path MTU of the destination transport address to which the packet is + // being sent. Call this value K. Retransmit those K DATA chunks in a single + // packet. When a Fast Retransmit is being performed, the sender SHOULD + // ignore the value of cwnd and SHOULD NOT delay retransmission for this + // single packet." + + SctpPacket::Builder builder(peer_verification_tag_, options_); + auto chunks = retransmission_queue_.GetChunksForFastRetransmit( + builder.bytes_remaining()); + for (auto& [tsn, data] : chunks) { + if (capabilities_.message_interleaving) { + builder.Add(IDataChunk(tsn, std::move(data), false)); + } else { + builder.Add(DataChunk(tsn, std::move(data), false)); + } + } + packet_sender_.Send(builder); +} + void TransmissionControlBlock::SendBufferedPackets(SctpPacket::Builder& builder, TimeMs now) { for (int packet_idx = 0; diff --git a/net/dcsctp/socket/transmission_control_block.h b/net/dcsctp/socket/transmission_control_block.h index 8cefbc65f4..8c240f1043 100644 --- a/net/dcsctp/socket/transmission_control_block.h +++ b/net/dcsctp/socket/transmission_control_block.h @@ -183,6 +183,8 @@ class TransmissionControlBlock : public Context { bool has_cookie_echo_chunk() const { return cookie_echo_chunk_.has_value(); } + void MaybeSendFastRetransmit(); + // Fills `builder` (which may already be filled with control chunks) with // other control and data chunks, and sends packets as much as can be // allowed by the congestion control algorithm. diff --git a/net/dcsctp/tx/outstanding_data.cc b/net/dcsctp/tx/outstanding_data.cc index 6929e286a4..37bba69253 100644 --- a/net/dcsctp/tx/outstanding_data.cc +++ b/net/dcsctp/tx/outstanding_data.cc @@ -110,8 +110,9 @@ void OutstandingData::AckChunk(AckInfo& ack_info, --outstanding_items_; } if (iter->second.should_be_retransmitted()) { + RTC_DCHECK(to_be_fast_retransmitted_.find(iter->first) == + to_be_fast_retransmitted_.end()); to_be_retransmitted_.erase(iter->first); - to_be_fast_retransmitted_.erase(iter->first); } iter->second.Ack(); ack_info.highest_tsn_acked = diff --git a/net/dcsctp/tx/retransmission_queue.cc b/net/dcsctp/tx/retransmission_queue.cc index 209323af9f..4afc01bf6e 100644 --- a/net/dcsctp/tx/retransmission_queue.cc +++ b/net/dcsctp/tx/retransmission_queue.cc @@ -387,6 +387,43 @@ void RetransmissionQueue::HandleT3RtxTimerExpiry() { RTC_DCHECK(IsConsistent()); } +std::vector> +RetransmissionQueue::GetChunksForFastRetransmit(size_t bytes_in_packet) { + RTC_DCHECK(outstanding_data_.has_data_to_be_fast_retransmitted()); + RTC_DCHECK(IsDivisibleBy4(bytes_in_packet)); + std::vector> to_be_sent; + size_t old_outstanding_bytes = outstanding_bytes(); + + to_be_sent = + outstanding_data_.GetChunksToBeFastRetransmitted(bytes_in_packet); + RTC_DCHECK(!to_be_sent.empty()); + + // https://tools.ietf.org/html/rfc4960#section-6.3.2 + // "Every time a DATA chunk is sent to any address (including a + // retransmission), if the T3-rtx timer of that address is not running, + // start it running so that it will expire after the RTO of that address." + if (!t3_rtx_.is_running()) { + t3_rtx_.Start(); + } + RTC_DLOG(LS_VERBOSE) << log_prefix_ << "Fast-retransmitting TSN " + << StrJoin(to_be_sent, ",", + [&](rtc::StringBuilder& sb, + const std::pair& c) { + sb << *c.first; + }) + << " - " + << absl::c_accumulate( + to_be_sent, 0, + [&](size_t r, const std::pair& d) { + return r + GetSerializedChunkSize(d.second); + }) + << " bytes. outstanding_bytes=" << outstanding_bytes() + << " (" << old_outstanding_bytes << ")"; + + RTC_DCHECK(IsConsistent()); + return to_be_sent; +} + std::vector> RetransmissionQueue::GetChunksToSend( TimeMs now, size_t bytes_remaining_in_packet) { @@ -396,60 +433,42 @@ std::vector> RetransmissionQueue::GetChunksToSend( std::vector> to_be_sent; size_t old_outstanding_bytes = outstanding_bytes(); size_t old_rwnd = rwnd_; - if (outstanding_data_.has_data_to_be_fast_retransmitted()) { - // https://tools.ietf.org/html/rfc4960#section-7.2.4 - // "Determine how many of the earliest (i.e., lowest TSN) DATA chunks - // marked for retransmission will fit into a single packet ... Retransmit - // those K DATA chunks in a single packet. When a Fast Retransmit is being - // performed, the sender SHOULD ignore the value of cwnd and SHOULD NOT - // delay retransmission for this single packet." - to_be_sent = outstanding_data_.GetChunksToBeFastRetransmitted( - bytes_remaining_in_packet); - size_t to_be_sent_bytes = absl::c_accumulate( - to_be_sent, 0, [&](size_t r, const std::pair& d) { - return r + GetSerializedChunkSize(d.second); - }); - RTC_DLOG(LS_VERBOSE) << log_prefix_ << "fast-retransmit: sending " - << to_be_sent.size() << " chunks, " << to_be_sent_bytes - << " bytes"; - } - if (to_be_sent.empty()) { - // Normal sending. Calculate the bandwidth budget (how many bytes that is - // allowed to be sent), and fill that up first with chunks that are - // scheduled to be retransmitted. If there is still budget, send new chunks - // (which will have their TSN assigned here.) - size_t max_bytes = - RoundDownTo4(std::min(max_bytes_to_send(), bytes_remaining_in_packet)); - to_be_sent = outstanding_data_.GetChunksToBeRetransmitted(max_bytes); - max_bytes -= absl::c_accumulate( - to_be_sent, 0, [&](size_t r, const std::pair& d) { - return r + GetSerializedChunkSize(d.second); - }); + // Calculate the bandwidth budget (how many bytes that is + // allowed to be sent), and fill that up first with chunks that are + // scheduled to be retransmitted. If there is still budget, send new chunks + // (which will have their TSN assigned here.) + size_t max_bytes = + RoundDownTo4(std::min(max_bytes_to_send(), bytes_remaining_in_packet)); - while (max_bytes > data_chunk_header_size_) { - RTC_DCHECK(IsDivisibleBy4(max_bytes)); - absl::optional chunk_opt = - send_queue_.Produce(now, max_bytes - data_chunk_header_size_); - if (!chunk_opt.has_value()) { - break; - } + to_be_sent = outstanding_data_.GetChunksToBeRetransmitted(max_bytes); + max_bytes -= absl::c_accumulate(to_be_sent, 0, + [&](size_t r, const std::pair& d) { + return r + GetSerializedChunkSize(d.second); + }); - size_t chunk_size = GetSerializedChunkSize(chunk_opt->data); - max_bytes -= chunk_size; - rwnd_ -= chunk_size; + while (max_bytes > data_chunk_header_size_) { + RTC_DCHECK(IsDivisibleBy4(max_bytes)); + absl::optional chunk_opt = + send_queue_.Produce(now, max_bytes - data_chunk_header_size_); + if (!chunk_opt.has_value()) { + break; + } - absl::optional tsn = outstanding_data_.Insert( - chunk_opt->data, - partial_reliability_ ? chunk_opt->max_retransmissions - : MaxRetransmits::NoLimit(), - now, - partial_reliability_ ? chunk_opt->expires_at - : TimeMs::InfiniteFuture()); + size_t chunk_size = GetSerializedChunkSize(chunk_opt->data); + max_bytes -= chunk_size; + rwnd_ -= chunk_size; - if (tsn.has_value()) { - to_be_sent.emplace_back(tsn->Wrap(), std::move(chunk_opt->data)); - } + absl::optional tsn = outstanding_data_.Insert( + chunk_opt->data, + partial_reliability_ ? chunk_opt->max_retransmissions + : MaxRetransmits::NoLimit(), + now, + partial_reliability_ ? chunk_opt->expires_at + : TimeMs::InfiniteFuture()); + + if (tsn.has_value()) { + to_be_sent.emplace_back(tsn->Wrap(), std::move(chunk_opt->data)); } } diff --git a/net/dcsctp/tx/retransmission_queue.h b/net/dcsctp/tx/retransmission_queue.h index c27fa7a3cf..1e866b30b5 100644 --- a/net/dcsctp/tx/retransmission_queue.h +++ b/net/dcsctp/tx/retransmission_queue.h @@ -74,6 +74,16 @@ class RetransmissionQueue { // Handles an expired retransmission timer. void HandleT3RtxTimerExpiry(); + bool has_data_to_be_fast_retransmitted() const { + return outstanding_data_.has_data_to_be_fast_retransmitted(); + } + + // Returns a list of chunks to "fast retransmit" that would fit in one SCTP + // packet with `bytes_in_packet` bytes available. The current value + // of `cwnd` is ignored. + std::vector> GetChunksForFastRetransmit( + size_t bytes_in_packet); + // Returns a list of chunks to send that would fit in one SCTP packet with // `bytes_remaining_in_packet` bytes available. This may be further limited by // the congestion control windows. Note that `ShouldSendForwardTSN` must be diff --git a/net/dcsctp/tx/retransmission_queue_test.cc b/net/dcsctp/tx/retransmission_queue_test.cc index 8294a3111d..b232bb2d82 100644 --- a/net/dcsctp/tx/retransmission_queue_test.cc +++ b/net/dcsctp/tx/retransmission_queue_test.cc @@ -78,6 +78,14 @@ class RetransmissionQueueTest : public testing::Test { }; } + std::vector GetTSNsForFastRetransmit(RetransmissionQueue& queue) { + std::vector tsns; + for (const auto& elem : queue.GetChunksForFastRetransmit(10000)) { + tsns.push_back(elem.first); + } + return tsns; + } + std::vector GetSentPacketTSNs(RetransmissionQueue& queue) { std::vector tsns; for (const auto& elem : queue.GetChunksToSend(now_, 10000)) { @@ -279,7 +287,8 @@ TEST_F(RetransmissionQueueTest, ResendPacketsWhenNackedThreeTimes) { // resent right now. The send queue will not even be queried. EXPECT_CALL(producer_, Produce).Times(0); - EXPECT_THAT(GetSentPacketTSNs(queue), testing::ElementsAre(TSN(13), TSN(16))); + EXPECT_THAT(GetTSNsForFastRetransmit(queue), + testing::ElementsAre(TSN(13), TSN(16))); EXPECT_THAT(queue.GetChunkStatesForTesting(), ElementsAre(Pair(TSN(12), State::kAcked), // @@ -1140,7 +1149,8 @@ TEST_F(RetransmissionQueueTest, AbandonsRtxLimit2WhenNackedNineTimes) { Pair(TSN(18), State::kInFlight), // Pair(TSN(19), State::kInFlight))); - EXPECT_THAT(queue.GetChunksToSend(now_, 1000), ElementsAre(Pair(TSN(10), _))); + EXPECT_THAT(queue.GetChunksForFastRetransmit(1000), + ElementsAre(Pair(TSN(10), _))); // Ack TSN [14 to 16] - three more nacks - second and last retransmission. for (int tsn = 14; tsn <= 16; ++tsn) { @@ -1375,7 +1385,7 @@ TEST_F(RetransmissionQueueTest, ReadyForHandoverWhenNothingToRetransmit) { // Send "fast retransmit" mode chunks EXPECT_CALL(producer_, Produce).Times(0); - EXPECT_THAT(GetSentPacketTSNs(queue), SizeIs(2)); + EXPECT_THAT(GetTSNsForFastRetransmit(queue), SizeIs(2)); EXPECT_EQ( queue.GetHandoverReadiness(), HandoverReadinessStatus()