dcsctp: Improve fast retransmission support
Before this CL, fast retransmission didn't follow the SHOULDs:
https://datatracker.ietf.org/doc/html/rfc4960#section-7.2.4
* "the sender SHOULD ignore the value of cwnd (...)"
* "(...) and SHOULD NOT delay retransmission for this single
packet."
With this CL, chunks that are eligible for fast retransmission (limited
to what can fit in a single packet) will be sent just after having
received the SACK that reported them missing and transitioned the socket
into fast recovery, and they will be sent even if the congestion window
is full.
Bug: webrtc:13969
Change-Id: I12c7e191a8ffd67973db7f083bad8a6061549fa2
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/259866
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Commit-Queue: Victor Boivie <boivie@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#36724}
diff --git a/net/dcsctp/socket/dcsctp_socket.cc b/net/dcsctp/socket/dcsctp_socket.cc
index 7ccca5c..cc9a71e 100644
--- a/net/dcsctp/socket/dcsctp_socket.cc
+++ b/net/dcsctp/socket/dcsctp_socket.cc
@@ -1391,6 +1391,17 @@
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 539fde7..78331d5 100644
--- a/net/dcsctp/socket/transmission_control_block.cc
+++ b/net/dcsctp/socket/transmission_control_block.cc
@@ -102,6 +102,33 @@
}
}
+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 8cefbc6..8c240f1 100644
--- a/net/dcsctp/socket/transmission_control_block.h
+++ b/net/dcsctp/socket/transmission_control_block.h
@@ -183,6 +183,8 @@
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 6929e28..37bba69 100644
--- a/net/dcsctp/tx/outstanding_data.cc
+++ b/net/dcsctp/tx/outstanding_data.cc
@@ -110,8 +110,9 @@
--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 209323a..4afc01b 100644
--- a/net/dcsctp/tx/retransmission_queue.cc
+++ b/net/dcsctp/tx/retransmission_queue.cc
@@ -387,6 +387,43 @@
RTC_DCHECK(IsConsistent());
}
+std::vector<std::pair<TSN, Data>>
+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<std::pair<TSN, Data>> 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<TSN, Data>& c) {
+ sb << *c.first;
+ })
+ << " - "
+ << absl::c_accumulate(
+ to_be_sent, 0,
+ [&](size_t r, const std::pair<TSN, Data>& d) {
+ return r + GetSerializedChunkSize(d.second);
+ })
+ << " bytes. outstanding_bytes=" << outstanding_bytes()
+ << " (" << old_outstanding_bytes << ")";
+
+ RTC_DCHECK(IsConsistent());
+ return to_be_sent;
+}
+
std::vector<std::pair<TSN, Data>> RetransmissionQueue::GetChunksToSend(
TimeMs now,
size_t bytes_remaining_in_packet) {
@@ -396,60 +433,42 @@
std::vector<std::pair<TSN, Data>> 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<TSN, Data>& 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<TSN, Data>& 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<SendQueue::DataToSend> 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<TSN, Data>& 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<SendQueue::DataToSend> chunk_opt =
+ send_queue_.Produce(now, max_bytes - data_chunk_header_size_);
+ if (!chunk_opt.has_value()) {
+ break;
+ }
- absl::optional<UnwrappedTSN> 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<UnwrappedTSN> 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 c27fa7a..1e866b3 100644
--- a/net/dcsctp/tx/retransmission_queue.h
+++ b/net/dcsctp/tx/retransmission_queue.h
@@ -74,6 +74,16 @@
// 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<std::pair<TSN, Data>> 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 8294a31..b232bb2 100644
--- a/net/dcsctp/tx/retransmission_queue_test.cc
+++ b/net/dcsctp/tx/retransmission_queue_test.cc
@@ -78,6 +78,14 @@
};
}
+ std::vector<TSN> GetTSNsForFastRetransmit(RetransmissionQueue& queue) {
+ std::vector<TSN> tsns;
+ for (const auto& elem : queue.GetChunksForFastRetransmit(10000)) {
+ tsns.push_back(elem.first);
+ }
+ return tsns;
+ }
+
std::vector<TSN> GetSentPacketTSNs(RetransmissionQueue& queue) {
std::vector<TSN> tsns;
for (const auto& elem : queue.GetChunksToSend(now_, 10000)) {
@@ -279,7 +287,8 @@
// 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 @@
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 @@
// 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()