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}
This commit is contained in:
Victor Boivie 2022-04-22 16:28:33 +02:00 committed by WebRTC LUCI CQ
parent 8fcc79b3d5
commit 5e354d9969
7 changed files with 133 additions and 53 deletions

View File

@ -1391,6 +1391,17 @@ void DcSctpSocket::HandleSack(const CommonHeader& header,
if (tcb_->retransmission_queue().HandleSack(now, sack)) { if (tcb_->retransmission_queue().HandleSack(now, sack)) {
MaybeSendShutdownOrAck(); 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 // Receiving an ACK will decrease outstanding bytes (maybe now below
// cwnd?) or indicate packet loss that may result in sending FORWARD-TSN. // cwnd?) or indicate packet loss that may result in sending FORWARD-TSN.
tcb_->SendBufferedPackets(now); tcb_->SendBufferedPackets(now);

View File

@ -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, void TransmissionControlBlock::SendBufferedPackets(SctpPacket::Builder& builder,
TimeMs now) { TimeMs now) {
for (int packet_idx = 0; for (int packet_idx = 0;

View File

@ -183,6 +183,8 @@ class TransmissionControlBlock : public Context {
bool has_cookie_echo_chunk() const { return cookie_echo_chunk_.has_value(); } 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 // Fills `builder` (which may already be filled with control chunks) with
// other control and data chunks, and sends packets as much as can be // other control and data chunks, and sends packets as much as can be
// allowed by the congestion control algorithm. // allowed by the congestion control algorithm.

View File

@ -110,8 +110,9 @@ void OutstandingData::AckChunk(AckInfo& ack_info,
--outstanding_items_; --outstanding_items_;
} }
if (iter->second.should_be_retransmitted()) { 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_retransmitted_.erase(iter->first);
to_be_fast_retransmitted_.erase(iter->first);
} }
iter->second.Ack(); iter->second.Ack();
ack_info.highest_tsn_acked = ack_info.highest_tsn_acked =

View File

@ -387,6 +387,43 @@ void RetransmissionQueue::HandleT3RtxTimerExpiry() {
RTC_DCHECK(IsConsistent()); 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( std::vector<std::pair<TSN, Data>> RetransmissionQueue::GetChunksToSend(
TimeMs now, TimeMs now,
size_t bytes_remaining_in_packet) { size_t bytes_remaining_in_packet) {
@ -396,60 +433,42 @@ std::vector<std::pair<TSN, Data>> RetransmissionQueue::GetChunksToSend(
std::vector<std::pair<TSN, Data>> to_be_sent; std::vector<std::pair<TSN, Data>> to_be_sent;
size_t old_outstanding_bytes = outstanding_bytes(); size_t old_outstanding_bytes = outstanding_bytes();
size_t old_rwnd = rwnd_; 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); // Calculate the bandwidth budget (how many bytes that is
max_bytes -= absl::c_accumulate( // allowed to be sent), and fill that up first with chunks that are
to_be_sent, 0, [&](size_t r, const std::pair<TSN, Data>& d) { // scheduled to be retransmitted. If there is still budget, send new chunks
return r + GetSerializedChunkSize(d.second); // (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_) { to_be_sent = outstanding_data_.GetChunksToBeRetransmitted(max_bytes);
RTC_DCHECK(IsDivisibleBy4(max_bytes)); max_bytes -= absl::c_accumulate(to_be_sent, 0,
absl::optional<SendQueue::DataToSend> chunk_opt = [&](size_t r, const std::pair<TSN, Data>& d) {
send_queue_.Produce(now, max_bytes - data_chunk_header_size_); return r + GetSerializedChunkSize(d.second);
if (!chunk_opt.has_value()) { });
break;
}
size_t chunk_size = GetSerializedChunkSize(chunk_opt->data); while (max_bytes > data_chunk_header_size_) {
max_bytes -= chunk_size; RTC_DCHECK(IsDivisibleBy4(max_bytes));
rwnd_ -= chunk_size; 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( size_t chunk_size = GetSerializedChunkSize(chunk_opt->data);
chunk_opt->data, max_bytes -= chunk_size;
partial_reliability_ ? chunk_opt->max_retransmissions rwnd_ -= chunk_size;
: MaxRetransmits::NoLimit(),
now,
partial_reliability_ ? chunk_opt->expires_at
: TimeMs::InfiniteFuture());
if (tsn.has_value()) { absl::optional<UnwrappedTSN> tsn = outstanding_data_.Insert(
to_be_sent.emplace_back(tsn->Wrap(), std::move(chunk_opt->data)); 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));
} }
} }

View File

@ -74,6 +74,16 @@ class RetransmissionQueue {
// Handles an expired retransmission timer. // Handles an expired retransmission timer.
void HandleT3RtxTimerExpiry(); 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 // 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 // `bytes_remaining_in_packet` bytes available. This may be further limited by
// the congestion control windows. Note that `ShouldSendForwardTSN` must be // the congestion control windows. Note that `ShouldSendForwardTSN` must be

View File

@ -78,6 +78,14 @@ class RetransmissionQueueTest : public testing::Test {
}; };
} }
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> GetSentPacketTSNs(RetransmissionQueue& queue) {
std::vector<TSN> tsns; std::vector<TSN> tsns;
for (const auto& elem : queue.GetChunksToSend(now_, 10000)) { 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. // resent right now. The send queue will not even be queried.
EXPECT_CALL(producer_, Produce).Times(0); 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(), EXPECT_THAT(queue.GetChunkStatesForTesting(),
ElementsAre(Pair(TSN(12), State::kAcked), // ElementsAre(Pair(TSN(12), State::kAcked), //
@ -1140,7 +1149,8 @@ TEST_F(RetransmissionQueueTest, AbandonsRtxLimit2WhenNackedNineTimes) {
Pair(TSN(18), State::kInFlight), // Pair(TSN(18), State::kInFlight), //
Pair(TSN(19), 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. // Ack TSN [14 to 16] - three more nacks - second and last retransmission.
for (int tsn = 14; tsn <= 16; ++tsn) { for (int tsn = 14; tsn <= 16; ++tsn) {
@ -1375,7 +1385,7 @@ TEST_F(RetransmissionQueueTest, ReadyForHandoverWhenNothingToRetransmit) {
// Send "fast retransmit" mode chunks // Send "fast retransmit" mode chunks
EXPECT_CALL(producer_, Produce).Times(0); EXPECT_CALL(producer_, Produce).Times(0);
EXPECT_THAT(GetSentPacketTSNs(queue), SizeIs(2)); EXPECT_THAT(GetTSNsForFastRetransmit(queue), SizeIs(2));
EXPECT_EQ( EXPECT_EQ(
queue.GetHandoverReadiness(), queue.GetHandoverReadiness(),
HandoverReadinessStatus() HandoverReadinessStatus()