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:
parent
8fcc79b3d5
commit
5e354d9969
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -387,6 +387,43 @@ void RetransmissionQueue::HandleT3RtxTimerExpiry() {
|
||||
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,25 +433,8 @@ std::vector<std::pair<TSN, Data>> RetransmissionQueue::GetChunksToSend(
|
||||
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
|
||||
|
||||
// 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.)
|
||||
@ -422,8 +442,8 @@ std::vector<std::pair<TSN, Data>> RetransmissionQueue::GetChunksToSend(
|
||||
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) {
|
||||
max_bytes -= absl::c_accumulate(to_be_sent, 0,
|
||||
[&](size_t r, const std::pair<TSN, Data>& d) {
|
||||
return r + GetSerializedChunkSize(d.second);
|
||||
});
|
||||
|
||||
@ -451,7 +471,6 @@ std::vector<std::pair<TSN, Data>> RetransmissionQueue::GetChunksToSend(
|
||||
to_be_sent.emplace_back(tsn->Wrap(), std::move(chunk_opt->data));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!to_be_sent.empty()) {
|
||||
// https://tools.ietf.org/html/rfc4960#section-6.3.2
|
||||
|
||||
@ -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<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
|
||||
|
||||
@ -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> 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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user