xref: /aosp_15_r20/external/webrtc/net/dcsctp/tx/outstanding_data.cc (revision d9f758449e529ab9291ac668be2861e7a55c2422)
1 /*
2  *  Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 #include "net/dcsctp/tx/outstanding_data.h"
11 
12 #include <algorithm>
13 #include <set>
14 #include <utility>
15 #include <vector>
16 
17 #include "net/dcsctp/common/math.h"
18 #include "net/dcsctp/common/sequence_numbers.h"
19 #include "net/dcsctp/public/types.h"
20 #include "rtc_base/logging.h"
21 
22 namespace dcsctp {
23 
24 // The number of times a packet must be NACKed before it's retransmitted.
25 // See https://tools.ietf.org/html/rfc4960#section-7.2.4
26 constexpr uint8_t kNumberOfNacksForRetransmission = 3;
27 
28 // Returns how large a chunk will be, serialized, carrying the data
GetSerializedChunkSize(const Data & data) const29 size_t OutstandingData::GetSerializedChunkSize(const Data& data) const {
30   return RoundUpTo4(data_chunk_header_size_ + data.size());
31 }
32 
Ack()33 void OutstandingData::Item::Ack() {
34   if (lifecycle_ != Lifecycle::kAbandoned) {
35     lifecycle_ = Lifecycle::kActive;
36   }
37   ack_state_ = AckState::kAcked;
38 }
39 
Nack(bool retransmit_now)40 OutstandingData::Item::NackAction OutstandingData::Item::Nack(
41     bool retransmit_now) {
42   ack_state_ = AckState::kNacked;
43   ++nack_count_;
44   if (!should_be_retransmitted() && !is_abandoned() &&
45       (retransmit_now || nack_count_ >= kNumberOfNacksForRetransmission)) {
46     // Nacked enough times - it's considered lost.
47     if (num_retransmissions_ < *max_retransmissions_) {
48       lifecycle_ = Lifecycle::kToBeRetransmitted;
49       return NackAction::kRetransmit;
50     }
51     Abandon();
52     return NackAction::kAbandon;
53   }
54   return NackAction::kNothing;
55 }
56 
MarkAsRetransmitted()57 void OutstandingData::Item::MarkAsRetransmitted() {
58   lifecycle_ = Lifecycle::kActive;
59   ack_state_ = AckState::kUnacked;
60 
61   nack_count_ = 0;
62   ++num_retransmissions_;
63 }
64 
Abandon()65 void OutstandingData::Item::Abandon() {
66   lifecycle_ = Lifecycle::kAbandoned;
67 }
68 
has_expired(TimeMs now) const69 bool OutstandingData::Item::has_expired(TimeMs now) const {
70   return expires_at_ <= now;
71 }
72 
IsConsistent() const73 bool OutstandingData::IsConsistent() const {
74   size_t actual_outstanding_bytes = 0;
75   size_t actual_outstanding_items = 0;
76 
77   std::set<UnwrappedTSN> combined_to_be_retransmitted;
78   combined_to_be_retransmitted.insert(to_be_retransmitted_.begin(),
79                                       to_be_retransmitted_.end());
80   combined_to_be_retransmitted.insert(to_be_fast_retransmitted_.begin(),
81                                       to_be_fast_retransmitted_.end());
82 
83   std::set<UnwrappedTSN> actual_combined_to_be_retransmitted;
84   for (const auto& [tsn, item] : outstanding_data_) {
85     if (item.is_outstanding()) {
86       actual_outstanding_bytes += GetSerializedChunkSize(item.data());
87       ++actual_outstanding_items;
88     }
89 
90     if (item.should_be_retransmitted()) {
91       actual_combined_to_be_retransmitted.insert(tsn);
92     }
93   }
94 
95   if (outstanding_data_.empty() &&
96       next_tsn_ != last_cumulative_tsn_ack_.next_value()) {
97     return false;
98   }
99 
100   return actual_outstanding_bytes == outstanding_bytes_ &&
101          actual_outstanding_items == outstanding_items_ &&
102          actual_combined_to_be_retransmitted == combined_to_be_retransmitted;
103 }
104 
AckChunk(AckInfo & ack_info,std::map<UnwrappedTSN,Item>::iterator iter)105 void OutstandingData::AckChunk(AckInfo& ack_info,
106                                std::map<UnwrappedTSN, Item>::iterator iter) {
107   if (!iter->second.is_acked()) {
108     size_t serialized_size = GetSerializedChunkSize(iter->second.data());
109     ack_info.bytes_acked += serialized_size;
110     if (iter->second.is_outstanding()) {
111       outstanding_bytes_ -= serialized_size;
112       --outstanding_items_;
113     }
114     if (iter->second.should_be_retransmitted()) {
115       RTC_DCHECK(to_be_fast_retransmitted_.find(iter->first) ==
116                  to_be_fast_retransmitted_.end());
117       to_be_retransmitted_.erase(iter->first);
118     }
119     iter->second.Ack();
120     ack_info.highest_tsn_acked =
121         std::max(ack_info.highest_tsn_acked, iter->first);
122   }
123 }
124 
HandleSack(UnwrappedTSN cumulative_tsn_ack,rtc::ArrayView<const SackChunk::GapAckBlock> gap_ack_blocks,bool is_in_fast_recovery)125 OutstandingData::AckInfo OutstandingData::HandleSack(
126     UnwrappedTSN cumulative_tsn_ack,
127     rtc::ArrayView<const SackChunk::GapAckBlock> gap_ack_blocks,
128     bool is_in_fast_recovery) {
129   OutstandingData::AckInfo ack_info(cumulative_tsn_ack);
130   // Erase all items up to cumulative_tsn_ack.
131   RemoveAcked(cumulative_tsn_ack, ack_info);
132 
133   // ACK packets reported in the gap ack blocks
134   AckGapBlocks(cumulative_tsn_ack, gap_ack_blocks, ack_info);
135 
136   // NACK and possibly mark for retransmit chunks that weren't acked.
137   NackBetweenAckBlocks(cumulative_tsn_ack, gap_ack_blocks, is_in_fast_recovery,
138                        ack_info);
139 
140   RTC_DCHECK(IsConsistent());
141   return ack_info;
142 }
143 
RemoveAcked(UnwrappedTSN cumulative_tsn_ack,AckInfo & ack_info)144 void OutstandingData::RemoveAcked(UnwrappedTSN cumulative_tsn_ack,
145                                   AckInfo& ack_info) {
146   auto first_unacked = outstanding_data_.upper_bound(cumulative_tsn_ack);
147 
148   for (auto iter = outstanding_data_.begin(); iter != first_unacked; ++iter) {
149     AckChunk(ack_info, iter);
150     if (iter->second.lifecycle_id().IsSet()) {
151       RTC_DCHECK(iter->second.data().is_end);
152       if (iter->second.is_abandoned()) {
153         ack_info.abandoned_lifecycle_ids.push_back(iter->second.lifecycle_id());
154       } else {
155         ack_info.acked_lifecycle_ids.push_back(iter->second.lifecycle_id());
156       }
157     }
158   }
159 
160   outstanding_data_.erase(outstanding_data_.begin(), first_unacked);
161   last_cumulative_tsn_ack_ = cumulative_tsn_ack;
162 }
163 
AckGapBlocks(UnwrappedTSN cumulative_tsn_ack,rtc::ArrayView<const SackChunk::GapAckBlock> gap_ack_blocks,AckInfo & ack_info)164 void OutstandingData::AckGapBlocks(
165     UnwrappedTSN cumulative_tsn_ack,
166     rtc::ArrayView<const SackChunk::GapAckBlock> gap_ack_blocks,
167     AckInfo& ack_info) {
168   // Mark all non-gaps as ACKED (but they can't be removed) as (from RFC)
169   // "SCTP considers the information carried in the Gap Ack Blocks in the
170   // SACK chunk as advisory.". Note that when NR-SACK is supported, this can be
171   // handled differently.
172 
173   for (auto& block : gap_ack_blocks) {
174     auto start = outstanding_data_.lower_bound(
175         UnwrappedTSN::AddTo(cumulative_tsn_ack, block.start));
176     auto end = outstanding_data_.upper_bound(
177         UnwrappedTSN::AddTo(cumulative_tsn_ack, block.end));
178     for (auto iter = start; iter != end; ++iter) {
179       AckChunk(ack_info, iter);
180     }
181   }
182 }
183 
NackBetweenAckBlocks(UnwrappedTSN cumulative_tsn_ack,rtc::ArrayView<const SackChunk::GapAckBlock> gap_ack_blocks,bool is_in_fast_recovery,OutstandingData::AckInfo & ack_info)184 void OutstandingData::NackBetweenAckBlocks(
185     UnwrappedTSN cumulative_tsn_ack,
186     rtc::ArrayView<const SackChunk::GapAckBlock> gap_ack_blocks,
187     bool is_in_fast_recovery,
188     OutstandingData::AckInfo& ack_info) {
189   // Mark everything between the blocks as NACKED/TO_BE_RETRANSMITTED.
190   // https://tools.ietf.org/html/rfc4960#section-7.2.4
191   // "Mark the DATA chunk(s) with three miss indications for retransmission."
192   // "For each incoming SACK, miss indications are incremented only for
193   // missing TSNs prior to the highest TSN newly acknowledged in the SACK."
194   //
195   // What this means is that only when there is a increasing stream of data
196   // received and there are new packets seen (since last time), packets that are
197   // in-flight and between gaps should be nacked. This means that SCTP relies on
198   // the T3-RTX-timer to re-send packets otherwise.
199   UnwrappedTSN max_tsn_to_nack = ack_info.highest_tsn_acked;
200   if (is_in_fast_recovery && cumulative_tsn_ack > last_cumulative_tsn_ack_) {
201     // https://tools.ietf.org/html/rfc4960#section-7.2.4
202     // "If an endpoint is in Fast Recovery and a SACK arrives that advances
203     // the Cumulative TSN Ack Point, the miss indications are incremented for
204     // all TSNs reported missing in the SACK."
205     max_tsn_to_nack = UnwrappedTSN::AddTo(
206         cumulative_tsn_ack,
207         gap_ack_blocks.empty() ? 0 : gap_ack_blocks.rbegin()->end);
208   }
209 
210   UnwrappedTSN prev_block_last_acked = cumulative_tsn_ack;
211   for (auto& block : gap_ack_blocks) {
212     UnwrappedTSN cur_block_first_acked =
213         UnwrappedTSN::AddTo(cumulative_tsn_ack, block.start);
214     for (auto iter = outstanding_data_.upper_bound(prev_block_last_acked);
215          iter != outstanding_data_.lower_bound(cur_block_first_acked); ++iter) {
216       if (iter->first <= max_tsn_to_nack) {
217         ack_info.has_packet_loss |=
218             NackItem(iter->first, iter->second, /*retransmit_now=*/false,
219                      /*do_fast_retransmit=*/!is_in_fast_recovery);
220       }
221     }
222     prev_block_last_acked = UnwrappedTSN::AddTo(cumulative_tsn_ack, block.end);
223   }
224 
225   // Note that packets are not NACKED which are above the highest gap-ack-block
226   // (or above the cumulative ack TSN if no gap-ack-blocks) as only packets
227   // up until the highest_tsn_acked (see above) should be considered when
228   // NACKing.
229 }
230 
NackItem(UnwrappedTSN tsn,Item & item,bool retransmit_now,bool do_fast_retransmit)231 bool OutstandingData::NackItem(UnwrappedTSN tsn,
232                                Item& item,
233                                bool retransmit_now,
234                                bool do_fast_retransmit) {
235   if (item.is_outstanding()) {
236     outstanding_bytes_ -= GetSerializedChunkSize(item.data());
237     --outstanding_items_;
238   }
239 
240   switch (item.Nack(retransmit_now)) {
241     case Item::NackAction::kNothing:
242       return false;
243     case Item::NackAction::kRetransmit:
244       if (do_fast_retransmit) {
245         to_be_fast_retransmitted_.insert(tsn);
246       } else {
247         to_be_retransmitted_.insert(tsn);
248       }
249       RTC_DLOG(LS_VERBOSE) << *tsn.Wrap() << " marked for retransmission";
250       break;
251     case Item::NackAction::kAbandon:
252       AbandonAllFor(item);
253       break;
254   }
255   return true;
256 }
257 
AbandonAllFor(const Item & item)258 void OutstandingData::AbandonAllFor(const Item& item) {
259   // Erase all remaining chunks from the producer, if any.
260   if (discard_from_send_queue_(item.data().is_unordered, item.data().stream_id,
261                                item.data().message_id)) {
262     // There were remaining chunks to be produced for this message. Since the
263     // receiver may have already received all chunks (up till now) for this
264     // message, we can't just FORWARD-TSN to the last fragment in this
265     // (abandoned) message and start sending a new message, as the receiver will
266     // then see a new message before the end of the previous one was seen (or
267     // skipped over). So create a new fragment, representing the end, that the
268     // received will never see as it is abandoned immediately and used as cum
269     // TSN in the sent FORWARD-TSN.
270     UnwrappedTSN tsn = next_tsn_;
271     next_tsn_.Increment();
272     Data message_end(item.data().stream_id, item.data().ssn,
273                      item.data().message_id, item.data().fsn, item.data().ppid,
274                      std::vector<uint8_t>(), Data::IsBeginning(false),
275                      Data::IsEnd(true), item.data().is_unordered);
276     Item& added_item =
277         outstanding_data_
278             .emplace(std::piecewise_construct, std::forward_as_tuple(tsn),
279                      std::forward_as_tuple(std::move(message_end), TimeMs(0),
280                                            MaxRetransmits::NoLimit(),
281                                            TimeMs::InfiniteFuture(),
282                                            LifecycleId::NotSet()))
283             .first->second;
284     // The added chunk shouldn't be included in `outstanding_bytes`, so set it
285     // as acked.
286     added_item.Ack();
287     RTC_DLOG(LS_VERBOSE) << "Adding unsent end placeholder for message at tsn="
288                          << *tsn.Wrap();
289   }
290 
291   for (auto& [tsn, other] : outstanding_data_) {
292     if (!other.is_abandoned() &&
293         other.data().stream_id == item.data().stream_id &&
294         other.data().is_unordered == item.data().is_unordered &&
295         other.data().message_id == item.data().message_id) {
296       RTC_DLOG(LS_VERBOSE) << "Marking chunk " << *tsn.Wrap()
297                            << " as abandoned";
298       if (other.should_be_retransmitted()) {
299         to_be_fast_retransmitted_.erase(tsn);
300         to_be_retransmitted_.erase(tsn);
301       }
302       other.Abandon();
303     }
304   }
305 }
306 
ExtractChunksThatCanFit(std::set<UnwrappedTSN> & chunks,size_t max_size)307 std::vector<std::pair<TSN, Data>> OutstandingData::ExtractChunksThatCanFit(
308     std::set<UnwrappedTSN>& chunks,
309     size_t max_size) {
310   std::vector<std::pair<TSN, Data>> result;
311 
312   for (auto it = chunks.begin(); it != chunks.end();) {
313     UnwrappedTSN tsn = *it;
314     auto elem = outstanding_data_.find(tsn);
315     RTC_DCHECK(elem != outstanding_data_.end());
316     Item& item = elem->second;
317     RTC_DCHECK(item.should_be_retransmitted());
318     RTC_DCHECK(!item.is_outstanding());
319     RTC_DCHECK(!item.is_abandoned());
320     RTC_DCHECK(!item.is_acked());
321 
322     size_t serialized_size = GetSerializedChunkSize(item.data());
323     if (serialized_size <= max_size) {
324       item.MarkAsRetransmitted();
325       result.emplace_back(tsn.Wrap(), item.data().Clone());
326       max_size -= serialized_size;
327       outstanding_bytes_ += serialized_size;
328       ++outstanding_items_;
329       it = chunks.erase(it);
330     } else {
331       ++it;
332     }
333     // No point in continuing if the packet is full.
334     if (max_size <= data_chunk_header_size_) {
335       break;
336     }
337   }
338   return result;
339 }
340 
341 std::vector<std::pair<TSN, Data>>
GetChunksToBeFastRetransmitted(size_t max_size)342 OutstandingData::GetChunksToBeFastRetransmitted(size_t max_size) {
343   std::vector<std::pair<TSN, Data>> result =
344       ExtractChunksThatCanFit(to_be_fast_retransmitted_, max_size);
345 
346   // https://datatracker.ietf.org/doc/html/rfc4960#section-7.2.4
347   // "Those TSNs marked for retransmission due to the Fast-Retransmit algorithm
348   // that did not fit in the sent datagram carrying K other TSNs are also marked
349   // as ineligible for a subsequent Fast Retransmit.  However, as they are
350   // marked for retransmission they will be retransmitted later on as soon as
351   // cwnd allows."
352   if (!to_be_fast_retransmitted_.empty()) {
353     to_be_retransmitted_.insert(to_be_fast_retransmitted_.begin(),
354                                 to_be_fast_retransmitted_.end());
355     to_be_fast_retransmitted_.clear();
356   }
357 
358   RTC_DCHECK(IsConsistent());
359   return result;
360 }
361 
GetChunksToBeRetransmitted(size_t max_size)362 std::vector<std::pair<TSN, Data>> OutstandingData::GetChunksToBeRetransmitted(
363     size_t max_size) {
364   // Chunks scheduled for fast retransmission must be sent first.
365   RTC_DCHECK(to_be_fast_retransmitted_.empty());
366   return ExtractChunksThatCanFit(to_be_retransmitted_, max_size);
367 }
368 
ExpireOutstandingChunks(TimeMs now)369 void OutstandingData::ExpireOutstandingChunks(TimeMs now) {
370   for (const auto& [tsn, item] : outstanding_data_) {
371     // Chunks that are nacked can be expired. Care should be taken not to expire
372     // unacked (in-flight) chunks as they might have been received, but the SACK
373     // is either delayed or in-flight and may be received later.
374     if (item.is_abandoned()) {
375       // Already abandoned.
376     } else if (item.is_nacked() && item.has_expired(now)) {
377       RTC_DLOG(LS_VERBOSE) << "Marking nacked chunk " << *tsn.Wrap()
378                            << " and message " << *item.data().message_id
379                            << " as expired";
380       AbandonAllFor(item);
381     } else {
382       // A non-expired chunk. No need to iterate any further.
383       break;
384     }
385   }
386   RTC_DCHECK(IsConsistent());
387 }
388 
highest_outstanding_tsn() const389 UnwrappedTSN OutstandingData::highest_outstanding_tsn() const {
390   return outstanding_data_.empty() ? last_cumulative_tsn_ack_
391                                    : outstanding_data_.rbegin()->first;
392 }
393 
Insert(const Data & data,TimeMs time_sent,MaxRetransmits max_retransmissions,TimeMs expires_at,LifecycleId lifecycle_id)394 absl::optional<UnwrappedTSN> OutstandingData::Insert(
395     const Data& data,
396     TimeMs time_sent,
397     MaxRetransmits max_retransmissions,
398     TimeMs expires_at,
399     LifecycleId lifecycle_id) {
400   UnwrappedTSN tsn = next_tsn_;
401   next_tsn_.Increment();
402 
403   // All chunks are always padded to be even divisible by 4.
404   size_t chunk_size = GetSerializedChunkSize(data);
405   outstanding_bytes_ += chunk_size;
406   ++outstanding_items_;
407   auto it = outstanding_data_
408                 .emplace(std::piecewise_construct, std::forward_as_tuple(tsn),
409                          std::forward_as_tuple(data.Clone(), time_sent,
410                                                max_retransmissions, expires_at,
411                                                lifecycle_id))
412                 .first;
413 
414   if (it->second.has_expired(time_sent)) {
415     // No need to send it - it was expired when it was in the send
416     // queue.
417     RTC_DLOG(LS_VERBOSE) << "Marking freshly produced chunk "
418                          << *it->first.Wrap() << " and message "
419                          << *it->second.data().message_id << " as expired";
420     AbandonAllFor(it->second);
421     RTC_DCHECK(IsConsistent());
422     return absl::nullopt;
423   }
424 
425   RTC_DCHECK(IsConsistent());
426   return tsn;
427 }
428 
NackAll()429 void OutstandingData::NackAll() {
430   for (auto& [tsn, item] : outstanding_data_) {
431     if (!item.is_acked()) {
432       NackItem(tsn, item, /*retransmit_now=*/true,
433                /*do_fast_retransmit=*/false);
434     }
435   }
436   RTC_DCHECK(IsConsistent());
437 }
438 
MeasureRTT(TimeMs now,UnwrappedTSN tsn) const439 absl::optional<DurationMs> OutstandingData::MeasureRTT(TimeMs now,
440                                                        UnwrappedTSN tsn) const {
441   auto it = outstanding_data_.find(tsn);
442   if (it != outstanding_data_.end() && !it->second.has_been_retransmitted()) {
443     // https://tools.ietf.org/html/rfc4960#section-6.3.1
444     // "Karn's algorithm: RTT measurements MUST NOT be made using
445     // packets that were retransmitted (and thus for which it is ambiguous
446     // whether the reply was for the first instance of the chunk or for a
447     // later instance)"
448     return now - it->second.time_sent();
449   }
450   return absl::nullopt;
451 }
452 
453 std::vector<std::pair<TSN, OutstandingData::State>>
GetChunkStatesForTesting() const454 OutstandingData::GetChunkStatesForTesting() const {
455   std::vector<std::pair<TSN, State>> states;
456   states.emplace_back(last_cumulative_tsn_ack_.Wrap(), State::kAcked);
457   for (const auto& [tsn, item] : outstanding_data_) {
458     State state;
459     if (item.is_abandoned()) {
460       state = State::kAbandoned;
461     } else if (item.should_be_retransmitted()) {
462       state = State::kToBeRetransmitted;
463     } else if (item.is_acked()) {
464       state = State::kAcked;
465     } else if (item.is_outstanding()) {
466       state = State::kInFlight;
467     } else {
468       state = State::kNacked;
469     }
470 
471     states.emplace_back(tsn.Wrap(), state);
472   }
473   return states;
474 }
475 
ShouldSendForwardTsn() const476 bool OutstandingData::ShouldSendForwardTsn() const {
477   if (!outstanding_data_.empty()) {
478     auto it = outstanding_data_.begin();
479     return it->first == last_cumulative_tsn_ack_.next_value() &&
480            it->second.is_abandoned();
481   }
482   return false;
483 }
484 
CreateForwardTsn() const485 ForwardTsnChunk OutstandingData::CreateForwardTsn() const {
486   std::map<StreamID, SSN> skipped_per_ordered_stream;
487   UnwrappedTSN new_cumulative_ack = last_cumulative_tsn_ack_;
488 
489   for (const auto& [tsn, item] : outstanding_data_) {
490     if ((tsn != new_cumulative_ack.next_value()) || !item.is_abandoned()) {
491       break;
492     }
493     new_cumulative_ack = tsn;
494     if (!item.data().is_unordered &&
495         item.data().ssn > skipped_per_ordered_stream[item.data().stream_id]) {
496       skipped_per_ordered_stream[item.data().stream_id] = item.data().ssn;
497     }
498   }
499 
500   std::vector<ForwardTsnChunk::SkippedStream> skipped_streams;
501   skipped_streams.reserve(skipped_per_ordered_stream.size());
502   for (const auto& [stream_id, ssn] : skipped_per_ordered_stream) {
503     skipped_streams.emplace_back(stream_id, ssn);
504   }
505   return ForwardTsnChunk(new_cumulative_ack.Wrap(), std::move(skipped_streams));
506 }
507 
CreateIForwardTsn() const508 IForwardTsnChunk OutstandingData::CreateIForwardTsn() const {
509   std::map<std::pair<IsUnordered, StreamID>, MID> skipped_per_stream;
510   UnwrappedTSN new_cumulative_ack = last_cumulative_tsn_ack_;
511 
512   for (const auto& [tsn, item] : outstanding_data_) {
513     if ((tsn != new_cumulative_ack.next_value()) || !item.is_abandoned()) {
514       break;
515     }
516     new_cumulative_ack = tsn;
517     std::pair<IsUnordered, StreamID> stream_id =
518         std::make_pair(item.data().is_unordered, item.data().stream_id);
519 
520     if (item.data().message_id > skipped_per_stream[stream_id]) {
521       skipped_per_stream[stream_id] = item.data().message_id;
522     }
523   }
524 
525   std::vector<IForwardTsnChunk::SkippedStream> skipped_streams;
526   skipped_streams.reserve(skipped_per_stream.size());
527   for (const auto& [stream, message_id] : skipped_per_stream) {
528     skipped_streams.emplace_back(stream.first, stream.second, message_id);
529   }
530 
531   return IForwardTsnChunk(new_cumulative_ack.Wrap(),
532                           std::move(skipped_streams));
533 }
534 
ResetSequenceNumbers(UnwrappedTSN next_tsn,UnwrappedTSN last_cumulative_tsn)535 void OutstandingData::ResetSequenceNumbers(UnwrappedTSN next_tsn,
536                                            UnwrappedTSN last_cumulative_tsn) {
537   RTC_DCHECK(outstanding_data_.empty());
538   RTC_DCHECK(next_tsn_ == last_cumulative_tsn_ack_.next_value());
539   RTC_DCHECK(next_tsn == last_cumulative_tsn.next_value());
540   next_tsn_ = next_tsn;
541   last_cumulative_tsn_ack_ = last_cumulative_tsn;
542 }
543 }  // namespace dcsctp
544