dcsctp: Add Data Tracker
The Data Tracker's purpose is to keep track of all received DATA chunks and to ACK/NACK that data, by generating SACK chunks reflecting its view of what has been received and what has been lost. It also contains logic for _when_ to send the SACKs, as that's different depending on e.g. packet loss. Generally, SACKs are sent every second packet on a connection with no packet loss, and can also be sent on a delayed timer. In case partial reliability is used, and the transmitter has decided that some data shouldn't be retransmitted, it will send a FORWARD-TSN chunk, which this class also handles, by "forgetting" about those chunks. Bug: webrtc:12614 Change-Id: Ifafb0c211f6a47872e81830165ab5fc43ee7f366 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/213664 Commit-Queue: Victor Boivie <boivie@webrtc.org> Reviewed-by: Tommi <tommi@webrtc.org> Cr-Commit-Position: refs/heads/master@{#33676}
This commit is contained in:

committed by
Commit Bot

parent
50fc1dfbcc
commit
b2d539be6b
@ -16,6 +16,7 @@ if (rtc_include_tests) {
|
||||
"common:dcsctp_common_unittests",
|
||||
"packet:dcsctp_packet_unittests",
|
||||
"public:dcsctp_public_unittests",
|
||||
"rx:dcsctp_rx_unittests",
|
||||
"timer:dcsctp_timer_unittests",
|
||||
]
|
||||
}
|
||||
|
38
net/dcsctp/rx/BUILD.gn
Normal file
38
net/dcsctp/rx/BUILD.gn
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
|
||||
#
|
||||
# Use of this source code is governed by a BSD-style license
|
||||
# that can be found in the LICENSE file in the root of the source
|
||||
# tree. An additional intellectual property rights grant can be found
|
||||
# in the file PATENTS. All contributing project authors may
|
||||
# be found in the AUTHORS file in the root of the source tree.
|
||||
|
||||
import("../../../webrtc.gni")
|
||||
|
||||
rtc_library("data_tracker") {
|
||||
deps = [
|
||||
"../../../api:array_view",
|
||||
"../../../rtc_base",
|
||||
"../../../rtc_base:checks",
|
||||
"../../../rtc_base:rtc_base_approved",
|
||||
]
|
||||
sources = [
|
||||
"data_tracker.cc",
|
||||
"data_tracker.h",
|
||||
]
|
||||
}
|
||||
|
||||
if (rtc_include_tests) {
|
||||
rtc_library("dcsctp_rx_unittests") {
|
||||
testonly = true
|
||||
|
||||
deps = [
|
||||
":data_tracker",
|
||||
"../../../api:array_view",
|
||||
"../../../rtc_base:checks",
|
||||
"../../../rtc_base:gunit_helpers",
|
||||
"../../../rtc_base:rtc_base_approved",
|
||||
"../../../test:test_support",
|
||||
]
|
||||
sources = [ "data_tracker_test.cc" ]
|
||||
}
|
||||
}
|
264
net/dcsctp/rx/data_tracker.cc
Normal file
264
net/dcsctp/rx/data_tracker.cc
Normal file
@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
#include "net/dcsctp/rx/data_tracker.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "absl/types/optional.h"
|
||||
#include "net/dcsctp/common/sequence_numbers.h"
|
||||
#include "net/dcsctp/packet/chunk/sack_chunk.h"
|
||||
#include "net/dcsctp/timer/timer.h"
|
||||
#include "rtc_base/logging.h"
|
||||
#include "rtc_base/strings/string_builder.h"
|
||||
|
||||
namespace dcsctp {
|
||||
|
||||
void DataTracker::Observe(TSN tsn,
|
||||
AnyDataChunk::ImmediateAckFlag immediate_ack) {
|
||||
UnwrappedTSN unwrapped_tsn = tsn_unwrapper_.Unwrap(tsn);
|
||||
// Old chunk already seen before?
|
||||
if (unwrapped_tsn <= last_cumulative_acked_tsn_) {
|
||||
// TODO(boivie) Set duplicate TSN, even if it's not used in SCTP yet.
|
||||
return;
|
||||
}
|
||||
|
||||
if (unwrapped_tsn == last_cumulative_acked_tsn_.next_value()) {
|
||||
last_cumulative_acked_tsn_ = unwrapped_tsn;
|
||||
// The cumulative acked tsn may be moved even further, if a gap was filled.
|
||||
while (!additional_tsns.empty() &&
|
||||
*additional_tsns.begin() ==
|
||||
last_cumulative_acked_tsn_.next_value()) {
|
||||
last_cumulative_acked_tsn_.Increment();
|
||||
additional_tsns.erase(additional_tsns.begin());
|
||||
}
|
||||
} else {
|
||||
additional_tsns.insert(unwrapped_tsn);
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc4960#section-6.7
|
||||
// "Upon the reception of a new DATA chunk, an endpoint shall examine the
|
||||
// continuity of the TSNs received. If the endpoint detects a gap in
|
||||
// the received DATA chunk sequence, it SHOULD send a SACK with Gap Ack
|
||||
// Blocks immediately. The data receiver continues sending a SACK after
|
||||
// receipt of each SCTP packet that doesn't fill the gap."
|
||||
if (!additional_tsns.empty()) {
|
||||
UpdateAckState(AckState::kImmediate, "packet loss");
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7053#section-5.2
|
||||
// "Upon receipt of an SCTP packet containing a DATA chunk with the I
|
||||
// bit set, the receiver SHOULD NOT delay the sending of the corresponding
|
||||
// SACK chunk, i.e., the receiver SHOULD immediately respond with the
|
||||
// corresponding SACK chunk."
|
||||
if (*immediate_ack) {
|
||||
UpdateAckState(AckState::kImmediate, "immediate-ack bit set");
|
||||
}
|
||||
|
||||
if (!seen_packet_) {
|
||||
// https://tools.ietf.org/html/rfc4960#section-5.1
|
||||
// "After the reception of the first DATA chunk in an association the
|
||||
// endpoint MUST immediately respond with a SACK to acknowledge the DATA
|
||||
// chunk."
|
||||
seen_packet_ = true;
|
||||
UpdateAckState(AckState::kImmediate, "first DATA chunk");
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc4960#section-6.2
|
||||
// "Specifically, an acknowledgement SHOULD be generated for at least
|
||||
// every second packet (not every second DATA chunk) received, and SHOULD be
|
||||
// generated within 200 ms of the arrival of any unacknowledged DATA chunk."
|
||||
if (ack_state_ == AckState::kIdle) {
|
||||
UpdateAckState(AckState::kBecomingDelayed, "received DATA when idle");
|
||||
} else if (ack_state_ == AckState::kDelayed) {
|
||||
UpdateAckState(AckState::kImmediate, "received DATA when already delayed");
|
||||
}
|
||||
}
|
||||
|
||||
void DataTracker::HandleForwardTsn(TSN new_cumulative_ack) {
|
||||
// ForwardTSN is sent to make the receiver (this socket) "forget" about partly
|
||||
// received (or not received at all) data, up until `new_cumulative_ack`.
|
||||
|
||||
UnwrappedTSN unwrapped_tsn = tsn_unwrapper_.Unwrap(new_cumulative_ack);
|
||||
UnwrappedTSN prev_last_cum_ack_tsn = last_cumulative_acked_tsn_;
|
||||
|
||||
// Old chunk already seen before?
|
||||
if (unwrapped_tsn <= last_cumulative_acked_tsn_) {
|
||||
// https://tools.ietf.org/html/rfc3758#section-3.6
|
||||
// "Note, if the "New Cumulative TSN" value carried in the arrived
|
||||
// FORWARD TSN chunk is found to be behind or at the current cumulative TSN
|
||||
// point, the data receiver MUST treat this FORWARD TSN as out-of-date and
|
||||
// MUST NOT update its Cumulative TSN. The receiver SHOULD send a SACK to
|
||||
// its peer (the sender of the FORWARD TSN) since such a duplicate may
|
||||
// indicate the previous SACK was lost in the network."
|
||||
UpdateAckState(AckState::kImmediate,
|
||||
"FORWARD_TSN new_cumulative_tsn was behind");
|
||||
return;
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc3758#section-3.6
|
||||
// "When a FORWARD TSN chunk arrives, the data receiver MUST first update
|
||||
// its cumulative TSN point to the value carried in the FORWARD TSN chunk, and
|
||||
// then MUST further advance its cumulative TSN point locally if possible, as
|
||||
// shown by the following example..."
|
||||
|
||||
// The `new_cumulative_ack` will become the current
|
||||
// `last_cumulative_acked_tsn_`, and if there have been prior "gaps" that are
|
||||
// now overlapping with the new value, remove them.
|
||||
last_cumulative_acked_tsn_ = unwrapped_tsn;
|
||||
int erased_additional_tsns = std::distance(
|
||||
additional_tsns.begin(), additional_tsns.upper_bound(unwrapped_tsn));
|
||||
additional_tsns.erase(additional_tsns.begin(),
|
||||
additional_tsns.upper_bound(unwrapped_tsn));
|
||||
|
||||
// See if the `last_cumulative_acked_tsn_` can be moved even further:
|
||||
while (!additional_tsns.empty() &&
|
||||
*additional_tsns.begin() == last_cumulative_acked_tsn_.next_value()) {
|
||||
last_cumulative_acked_tsn_.Increment();
|
||||
additional_tsns.erase(additional_tsns.begin());
|
||||
++erased_additional_tsns;
|
||||
}
|
||||
|
||||
RTC_DLOG(LS_VERBOSE) << log_prefix_ << "FORWARD_TSN, cum_ack_tsn="
|
||||
<< *prev_last_cum_ack_tsn.Wrap() << "->"
|
||||
<< *new_cumulative_ack << "->"
|
||||
<< *last_cumulative_acked_tsn_.Wrap() << ", removed "
|
||||
<< erased_additional_tsns << " additional TSNs";
|
||||
|
||||
// https://tools.ietf.org/html/rfc3758#section-3.6
|
||||
// "Any time a FORWARD TSN chunk arrives, for the purposes of sending a
|
||||
// SACK, the receiver MUST follow the same rules as if a DATA chunk had been
|
||||
// received (i.e., follow the delayed sack rules specified in ..."
|
||||
if (ack_state_ == AckState::kIdle) {
|
||||
UpdateAckState(AckState::kBecomingDelayed,
|
||||
"received FORWARD_TSN when idle");
|
||||
} else if (ack_state_ == AckState::kDelayed) {
|
||||
UpdateAckState(AckState::kImmediate,
|
||||
"received FORWARD_TSN when already delayed");
|
||||
}
|
||||
}
|
||||
|
||||
SackChunk DataTracker::CreateSelectiveAck(size_t a_rwnd) {
|
||||
// Note that in SCTP, the receiver side is allowed to discard received data
|
||||
// and signal that to the sender, but only chunks that have previously been
|
||||
// reported in the gap-ack-blocks. However, this implementation will never do
|
||||
// that. So this SACK produced is more like a NR-SACK as explained in
|
||||
// https://ieeexplore.ieee.org/document/4697037 and which there is an RFC
|
||||
// draft at https://tools.ietf.org/html/draft-tuexen-tsvwg-sctp-multipath-17.
|
||||
std::vector<TSN> duplicate_tsns;
|
||||
duplicate_tsns.reserve(duplicates_.size());
|
||||
for (UnwrappedTSN tsn : duplicates_) {
|
||||
duplicate_tsns.push_back(tsn.Wrap());
|
||||
}
|
||||
duplicates_.clear();
|
||||
|
||||
return SackChunk(last_cumulative_acked_tsn_.Wrap(), a_rwnd,
|
||||
CreateGapAckBlocks(), duplicate_tsns);
|
||||
}
|
||||
|
||||
std::vector<SackChunk::GapAckBlock> DataTracker::CreateGapAckBlocks() const {
|
||||
// This method will calculate the gaps between blocks of contiguous values in
|
||||
// `additional_tsns_`, in the same format as the SACK chunk expects it;
|
||||
// offsets from the "cumulative ack TSN value".
|
||||
std::vector<SackChunk::GapAckBlock> gap_ack_blocks;
|
||||
|
||||
absl::optional<UnwrappedTSN> first_tsn_in_block = absl::nullopt;
|
||||
absl::optional<UnwrappedTSN> last_tsn_in_block = absl::nullopt;
|
||||
|
||||
auto flush = [&]() {
|
||||
if (first_tsn_in_block.has_value()) {
|
||||
int start_diff =
|
||||
first_tsn_in_block->Difference(last_cumulative_acked_tsn_);
|
||||
int end_diff = last_tsn_in_block->Difference(last_cumulative_acked_tsn_);
|
||||
gap_ack_blocks.emplace_back(static_cast<uint16_t>(start_diff),
|
||||
static_cast<uint16_t>(end_diff));
|
||||
first_tsn_in_block = absl::nullopt;
|
||||
last_tsn_in_block = absl::nullopt;
|
||||
}
|
||||
};
|
||||
for (UnwrappedTSN tsn : additional_tsns) {
|
||||
if (last_tsn_in_block.has_value() &&
|
||||
last_tsn_in_block->next_value() == tsn) {
|
||||
// Continuing the same block.
|
||||
last_tsn_in_block = tsn;
|
||||
} else {
|
||||
// New block, or a gap from the old block's last value.
|
||||
flush();
|
||||
first_tsn_in_block = tsn;
|
||||
last_tsn_in_block = tsn;
|
||||
}
|
||||
}
|
||||
flush();
|
||||
return gap_ack_blocks;
|
||||
}
|
||||
|
||||
bool DataTracker::ShouldSendAck(bool also_if_delayed) {
|
||||
if (ack_state_ == AckState::kImmediate ||
|
||||
(also_if_delayed && (ack_state_ == AckState::kBecomingDelayed ||
|
||||
ack_state_ == AckState::kDelayed))) {
|
||||
UpdateAckState(AckState::kIdle, "sending SACK");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DataTracker::will_increase_cum_ack_tsn(TSN tsn) const {
|
||||
UnwrappedTSN unwrapped = tsn_unwrapper_.PeekUnwrap(tsn);
|
||||
return unwrapped == last_cumulative_acked_tsn_.next_value();
|
||||
}
|
||||
|
||||
void DataTracker::ForceImmediateSack() {
|
||||
ack_state_ = AckState::kImmediate;
|
||||
}
|
||||
|
||||
void DataTracker::HandleDelayedAckTimerExpiry() {
|
||||
UpdateAckState(AckState::kImmediate, "delayed ack timer expired");
|
||||
}
|
||||
|
||||
void DataTracker::ObservePacketEnd() {
|
||||
if (ack_state_ == AckState::kBecomingDelayed) {
|
||||
UpdateAckState(AckState::kDelayed, "packet end");
|
||||
}
|
||||
}
|
||||
|
||||
void DataTracker::UpdateAckState(AckState new_state, absl::string_view reason) {
|
||||
if (new_state != ack_state_) {
|
||||
RTC_DLOG(LS_VERBOSE) << log_prefix_ << "State changed from "
|
||||
<< ToString(ack_state_) << " to "
|
||||
<< ToString(new_state) << " due to " << reason;
|
||||
if (ack_state_ == AckState::kDelayed) {
|
||||
delayed_ack_timer_.Stop();
|
||||
} else if (new_state == AckState::kDelayed) {
|
||||
delayed_ack_timer_.Start();
|
||||
}
|
||||
ack_state_ = new_state;
|
||||
}
|
||||
}
|
||||
|
||||
absl::string_view DataTracker::ToString(AckState ack_state) {
|
||||
switch (ack_state) {
|
||||
case AckState::kIdle:
|
||||
return "IDLE";
|
||||
case AckState::kBecomingDelayed:
|
||||
return "BECOMING_DELAYED";
|
||||
case AckState::kDelayed:
|
||||
return "DELAYED";
|
||||
case AckState::kImmediate:
|
||||
return "IMMEDIATE";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace dcsctp
|
121
net/dcsctp/rx/data_tracker.h
Normal file
121
net/dcsctp/rx/data_tracker.h
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
#ifndef NET_DCSCTP_RX_DATA_TRACKER_H_
|
||||
#define NET_DCSCTP_RX_DATA_TRACKER_H_
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "absl/strings/string_view.h"
|
||||
#include "net/dcsctp/common/sequence_numbers.h"
|
||||
#include "net/dcsctp/packet/chunk/data_common.h"
|
||||
#include "net/dcsctp/packet/chunk/sack_chunk.h"
|
||||
#include "net/dcsctp/packet/data.h"
|
||||
#include "net/dcsctp/timer/timer.h"
|
||||
|
||||
namespace dcsctp {
|
||||
|
||||
// Keeps track of received DATA chunks and handles all logic for _when_ to
|
||||
// create SACKs and also _how_ to generate them.
|
||||
//
|
||||
// It only uses TSNs to track delivery and doesn't need to be aware of streams.
|
||||
//
|
||||
// SACKs are optimally sent every second packet on connections with no packet
|
||||
// loss. When packet loss is detected, it's sent for every packet. When SACKs
|
||||
// are not sent directly, a timer is used to send a SACK delayed (by RTO/2, or
|
||||
// 200ms, whatever is smallest).
|
||||
class DataTracker {
|
||||
public:
|
||||
explicit DataTracker(absl::string_view log_prefix,
|
||||
Timer* delayed_ack_timer,
|
||||
TSN peer_initial_tsn)
|
||||
: log_prefix_(std::string(log_prefix) + "dtrack: "),
|
||||
delayed_ack_timer_(*delayed_ack_timer),
|
||||
last_cumulative_acked_tsn_(
|
||||
tsn_unwrapper_.Unwrap(TSN(*peer_initial_tsn - 1))) {}
|
||||
|
||||
// Call for every incoming data chunk.
|
||||
void Observe(TSN tsn,
|
||||
AnyDataChunk::ImmediateAckFlag immediate_ack =
|
||||
AnyDataChunk::ImmediateAckFlag(false));
|
||||
// Called at the end of processing an SCTP packet.
|
||||
void ObservePacketEnd();
|
||||
|
||||
// Called for incoming FORWARD-TSN/I-FORWARD-TSN chunks
|
||||
void HandleForwardTsn(TSN new_cumulative_ack);
|
||||
|
||||
// Indicates if a SACK should be sent. There may be other reasons to send a
|
||||
// SACK, but if this function indicates so, it should be sent as soon as
|
||||
// possible. Calling this function will make it clear a flag so that if it's
|
||||
// called again, it will probably return false.
|
||||
//
|
||||
// If the delayed ack timer is running, this method will return false _unless_
|
||||
// `also_if_delayed` is set to true. Then it will return true as well.
|
||||
bool ShouldSendAck(bool also_if_delayed = false);
|
||||
|
||||
// Returns the last cumulative ack TSN - the last seen data chunk's TSN
|
||||
// value before any packet loss was detected.
|
||||
TSN last_cumulative_acked_tsn() const {
|
||||
return TSN(last_cumulative_acked_tsn_.Wrap());
|
||||
}
|
||||
|
||||
// Returns true if the received `tsn` would increase the cumulative ack TSN.
|
||||
bool will_increase_cum_ack_tsn(TSN tsn) const;
|
||||
|
||||
// Forces `ShouldSendSack` to return true.
|
||||
void ForceImmediateSack();
|
||||
|
||||
// Note that this will clear `duplicates_`, so every SackChunk that is
|
||||
// consumed must be sent.
|
||||
SackChunk CreateSelectiveAck(size_t a_rwnd);
|
||||
|
||||
void HandleDelayedAckTimerExpiry();
|
||||
|
||||
private:
|
||||
enum class AckState {
|
||||
// No need to send an ACK.
|
||||
kIdle,
|
||||
|
||||
// Has received data chunks (but not yet end of packet).
|
||||
kBecomingDelayed,
|
||||
|
||||
// Has received data chunks and the end of a packet. Delayed ack timer is
|
||||
// running and a SACK will be sent on expiry, or if DATA is sent, or after
|
||||
// next packet with data.
|
||||
kDelayed,
|
||||
|
||||
// Send a SACK immediately after handling this packet.
|
||||
kImmediate,
|
||||
};
|
||||
std::vector<SackChunk::GapAckBlock> CreateGapAckBlocks() const;
|
||||
void UpdateAckState(AckState new_state, absl::string_view reason);
|
||||
static absl::string_view ToString(AckState ack_state);
|
||||
|
||||
const std::string log_prefix_;
|
||||
// If a packet has ever been seen.
|
||||
bool seen_packet_ = false;
|
||||
Timer& delayed_ack_timer_;
|
||||
AckState ack_state_ = AckState::kIdle;
|
||||
UnwrappedTSN::Unwrapper tsn_unwrapper_;
|
||||
|
||||
// All TSNs up until (and including) this value have been seen.
|
||||
UnwrappedTSN last_cumulative_acked_tsn_;
|
||||
// Received TSNs that are not directly following `last_cumulative_acked_tsn_`.
|
||||
std::set<UnwrappedTSN> additional_tsns;
|
||||
std::set<UnwrappedTSN> duplicates_;
|
||||
};
|
||||
} // namespace dcsctp
|
||||
|
||||
#endif // NET_DCSCTP_RX_DATA_TRACKER_H_
|
209
net/dcsctp/rx/data_tracker_test.cc
Normal file
209
net/dcsctp/rx/data_tracker_test.cc
Normal file
@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
#include "net/dcsctp/rx/data_tracker.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
|
||||
#include "absl/types/optional.h"
|
||||
#include "api/array_view.h"
|
||||
#include "net/dcsctp/packet/chunk/sack_chunk.h"
|
||||
#include "net/dcsctp/timer/fake_timeout.h"
|
||||
#include "net/dcsctp/timer/timer.h"
|
||||
#include "rtc_base/gunit.h"
|
||||
#include "test/gmock.h"
|
||||
|
||||
namespace dcsctp {
|
||||
namespace {
|
||||
using ::testing::ElementsAre;
|
||||
using ::testing::IsEmpty;
|
||||
|
||||
constexpr size_t kArwnd = 10000;
|
||||
|
||||
class DataTrackerTest : public testing::Test {
|
||||
protected:
|
||||
DataTrackerTest()
|
||||
: timeout_manager_([this]() { return now_; }),
|
||||
timer_manager_([this]() { return timeout_manager_.CreateTimeout(); }),
|
||||
timer_(timer_manager_.CreateTimer(
|
||||
"test/delayed_ack",
|
||||
[]() { return absl::nullopt; },
|
||||
TimerOptions(DurationMs(0)))),
|
||||
buf_("log: ", timer_.get(), /*peer_initial_tsn=*/TSN(11)) {}
|
||||
|
||||
void Observer(std::initializer_list<uint32_t> tsns) {
|
||||
for (const uint32_t tsn : tsns) {
|
||||
buf_.Observe(TSN(tsn), AnyDataChunk::ImmediateAckFlag(false));
|
||||
}
|
||||
}
|
||||
|
||||
TimeMs now_ = TimeMs(0);
|
||||
FakeTimeoutManager timeout_manager_;
|
||||
TimerManager timer_manager_;
|
||||
std::unique_ptr<Timer> timer_;
|
||||
DataTracker buf_;
|
||||
};
|
||||
|
||||
TEST_F(DataTrackerTest, Empty) {
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(10));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), IsEmpty());
|
||||
EXPECT_THAT(sack.duplicate_tsns(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ObserverSingleInOrderPacket) {
|
||||
Observer({11});
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(11));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), IsEmpty());
|
||||
EXPECT_THAT(sack.duplicate_tsns(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ObserverManyInOrderMovesCumulativeTsnAck) {
|
||||
Observer({11, 12, 13});
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(13));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), IsEmpty());
|
||||
EXPECT_THAT(sack.duplicate_tsns(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ObserveOutOfOrderMovesCumulativeTsnAck) {
|
||||
Observer({12, 13, 14, 11});
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(14));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), IsEmpty());
|
||||
EXPECT_THAT(sack.duplicate_tsns(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, SingleGap) {
|
||||
Observer({12});
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(10));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), ElementsAre(SackChunk::GapAckBlock(2, 2)));
|
||||
EXPECT_THAT(sack.duplicate_tsns(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ExampleFromRFC4960Section334) {
|
||||
Observer({11, 12, 14, 15, 17});
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(12));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), ElementsAre(SackChunk::GapAckBlock(2, 3),
|
||||
SackChunk::GapAckBlock(5, 5)));
|
||||
EXPECT_THAT(sack.duplicate_tsns(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, AckAlreadyReceivedChunk) {
|
||||
Observer({11});
|
||||
SackChunk sack1 = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack1.cumulative_tsn_ack(), TSN(11));
|
||||
EXPECT_THAT(sack1.gap_ack_blocks(), IsEmpty());
|
||||
|
||||
// Receive old chunk
|
||||
Observer({8});
|
||||
SackChunk sack2 = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack2.cumulative_tsn_ack(), TSN(11));
|
||||
EXPECT_THAT(sack2.gap_ack_blocks(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, DoubleSendRetransmittedChunk) {
|
||||
Observer({11, 13, 14, 15});
|
||||
SackChunk sack1 = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack1.cumulative_tsn_ack(), TSN(11));
|
||||
EXPECT_THAT(sack1.gap_ack_blocks(),
|
||||
ElementsAre(SackChunk::GapAckBlock(2, 4)));
|
||||
|
||||
// Fill in the hole.
|
||||
Observer({12, 16, 17, 18});
|
||||
SackChunk sack2 = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack2.cumulative_tsn_ack(), TSN(18));
|
||||
EXPECT_THAT(sack2.gap_ack_blocks(), IsEmpty());
|
||||
|
||||
// Receive chunk 12 again.
|
||||
Observer({12, 19, 20, 21});
|
||||
SackChunk sack3 = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack3.cumulative_tsn_ack(), TSN(21));
|
||||
EXPECT_THAT(sack3.gap_ack_blocks(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ForwardTsnSimple) {
|
||||
// Messages (11, 12, 13), (14, 15) - first message expires.
|
||||
Observer({11, 12, 15});
|
||||
|
||||
buf_.HandleForwardTsn(TSN(13));
|
||||
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(13));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), ElementsAre(SackChunk::GapAckBlock(2, 2)));
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ForwardTsnSkipsFromGapBlock) {
|
||||
// Messages (11, 12, 13), (14, 15) - first message expires.
|
||||
Observer({11, 12, 14});
|
||||
|
||||
buf_.HandleForwardTsn(TSN(13));
|
||||
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(14));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ExampleFromRFC3758) {
|
||||
buf_.HandleForwardTsn(TSN(102));
|
||||
|
||||
Observer({102, 104, 105, 107});
|
||||
|
||||
buf_.HandleForwardTsn(TSN(103));
|
||||
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(105));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), ElementsAre(SackChunk::GapAckBlock(2, 2)));
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, EmptyAllAcks) {
|
||||
Observer({11, 13, 14, 15});
|
||||
|
||||
buf_.HandleForwardTsn(TSN(100));
|
||||
|
||||
SackChunk sack = buf_.CreateSelectiveAck(kArwnd);
|
||||
EXPECT_EQ(sack.cumulative_tsn_ack(), TSN(100));
|
||||
EXPECT_THAT(sack.gap_ack_blocks(), IsEmpty());
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, SetsArwndCorrectly) {
|
||||
SackChunk sack1 = buf_.CreateSelectiveAck(/*a_rwnd=*/100);
|
||||
EXPECT_EQ(sack1.a_rwnd(), 100u);
|
||||
|
||||
SackChunk sack2 = buf_.CreateSelectiveAck(/*a_rwnd=*/101);
|
||||
EXPECT_EQ(sack2.a_rwnd(), 101u);
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, WillIncreaseCumAckTsn) {
|
||||
EXPECT_EQ(buf_.last_cumulative_acked_tsn(), TSN(10));
|
||||
EXPECT_FALSE(buf_.will_increase_cum_ack_tsn(TSN(10)));
|
||||
EXPECT_TRUE(buf_.will_increase_cum_ack_tsn(TSN(11)));
|
||||
EXPECT_FALSE(buf_.will_increase_cum_ack_tsn(TSN(12)));
|
||||
|
||||
Observer({11, 12, 13, 14, 15});
|
||||
EXPECT_EQ(buf_.last_cumulative_acked_tsn(), TSN(15));
|
||||
EXPECT_FALSE(buf_.will_increase_cum_ack_tsn(TSN(15)));
|
||||
EXPECT_TRUE(buf_.will_increase_cum_ack_tsn(TSN(16)));
|
||||
EXPECT_FALSE(buf_.will_increase_cum_ack_tsn(TSN(17)));
|
||||
}
|
||||
|
||||
TEST_F(DataTrackerTest, ForceShouldSendSackImmediately) {
|
||||
EXPECT_FALSE(buf_.ShouldSendAck());
|
||||
|
||||
buf_.ForceImmediateSack();
|
||||
|
||||
EXPECT_TRUE(buf_.ShouldSendAck());
|
||||
}
|
||||
} // namespace
|
||||
} // namespace dcsctp
|
Reference in New Issue
Block a user