diff --git a/apps/socketoptions.hpp b/apps/socketoptions.hpp index 12f23da2d..319499816 100644 --- a/apps/socketoptions.hpp +++ b/apps/socketoptions.hpp @@ -286,7 +286,8 @@ const SocketOption srt_options [] { { "bindtodevice", 0, SRTO_BINDTODEVICE, SocketOption::PRE, SocketOption::STRING, nullptr}, { "retransmitalgo", 0, SRTO_RETRANSMITALGO, SocketOption::PRE, SocketOption::INT, nullptr }, { "cryptomode", 0, SRTO_CRYPTOMODE, SocketOption::PRE, SocketOption::INT, nullptr }, - { "maxrexmitbw", 0, SRTO_MAXREXMITBW, SocketOption::POST, SocketOption::INT64, nullptr } + { "maxrexmitbw", 0, SRTO_MAXREXMITBW, SocketOption::POST, SocketOption::INT64, nullptr }, + { "sendmode", 0, SRTO_SENDMODE, SocketOption::PRE, SocketOption::INT, nullptr } }; } diff --git a/docs/API/API-functions.md b/docs/API/API-functions.md index 686e1610b..0eb0d110e 100644 --- a/docs/API/API-functions.md +++ b/docs/API/API-functions.md @@ -2061,7 +2061,8 @@ the [`SRT_MSGCTRL::msgttl`](#SRT_MSGCTRL) field. * `inorder`: Required to be received in the order of sending. See [`SRT_MSGCTRL::inorder`](#SRT_MSGCTRL). * `mctrl`: An object of [`SRT_MSGCTRL`](#SRT_MSGCTRL) type that contains extra -parameters, including `ttl` and `inorder`. +parameters, including `ttl` and `inorder`. See details of this structure for +additional important details concerning these parameters. The way this function works is determined by the mode set in options, and it has specific requirements: @@ -2811,10 +2812,18 @@ The sender can pass the timestamp in `MSGCTRL::srctime` of the `srt_sendmsg2(..) function together with the packet being submitted to SRT. If the `srctime` value is not provided (the default value of 0 is set), SRT will use the internal clock and assign packet submission time as the packet timestamp. If the sender wants to -explicitly assign a timestamp to a certain packet this timestamp MUST be taken -from SRT Time Access functions. The time value provided MUST equal or exceed the -connection start time (`srt_connection_time(..)`) of the SRT socket passed -to `srt_sendmsg2(..)`. +explicitly specify a timestamp for a packet, this value must be monotonic against +the time used by SRT, that is, this time value must be later (greater) than: + +* the value obtained by `srt_connection_time()` for the same socket + +* the value specified in any earlier call of `srt_sendmsg2` for the same socket + +otherwise the call to `srt_sendmsg2` will fail. + +This time value must be in the same domain as the time values used by SRT. The +values obtained by `srt_connection_time()` and `srt_time_now()` are in this +domain. The current time value of the SRT internal clock can be retrieved using the `srt_time_now()` function. @@ -2822,44 +2831,66 @@ the `srt_time_now()` function. There are two known cases where you might want to use `srctime`: 1. SRT passthrough (for stream gateways). -You may wish to simply retrieve packets from an SRT source and pass them transparently -to an SRT output (possibly re-encrypting). In that case, every packet you read should -preserve the original value of `srctime` as obtained from [`srt_recvmsg2`](#srt_recvmsg2), -and the original `srctime` for each packet should be then passed to [`srt_sendmsg2`](#srt_sendmsg). -This mechanism could be used to avoid jitter resulting from varying differences between -the time of receiving and sending the same packet. -2. Stable timing source. -In the case of a live streaming procedure, when spreading packets evenly into the stream, -you might want to predefine times for every single packet to keep time intervals perfectly equal. -Or, if you believe that your input signal delivers packets at the exact times that should be -assigned to them, you might want to preserve these times at the SRT receiving side -to avoid jitter that may result from varying time differences between the packet arrival -and the moment when sending it over SRT. In such cases you might do the following: +You may wish to simply retrieve packets from an SRT source and pass them +transparently to an SRT output (possibly re-encrypting). In that case, every +packet you read should preserve the original value of `srctime` as obtained +from [`srt_recvmsg2`](#srt_recvmsg2), and the original `srctime` for each +packet should be then passed to [`srt_sendmsg2`](#srt_sendmsg). This mechanism +could be used to avoid jitter resulting from varying differences between the +time of receiving and sending the same packet. - - At the packet arrival time, grab the current time at that moment using `srt_time_now()`. +2. Stable timing source. - - When you want a pre-calculated packet time, use a private relative time counter - set at the moment when the connection was made. From the moment when your first packet - is ready, start pre-calculating packet times relative to the connection start time obtained - from `srt_connection_time()`. Although you still have to synchronize sending times with these - predefined times, by explicitly specifying the source time you avoid the jitter - resulting from a lost accuracy due to waiting time and unfortunate thread scheduling. +In the case of a live streaming procedure, when spreading packets evenly into +the stream, you might want to predefine times for every single packet to keep +time intervals perfectly equal. Or, if you believe that your input signal +delivers packets at the exact times that should be assigned to them, you might +want to preserve these times at the SRT receiving side to avoid jitter that may +result from varying time differences between the packet arrival +and the moment when sending it over SRT. In such cases you might do the following: -Note that `srctime` uses an internally defined clock that is intended to be monotonic -(the definition depends on the build flags, see below). Because of that **the application -should not define this time basing on values obtained from the system functions for getting -the current system time** (such as `time`, `ftime` or `gettimeofday`). To avoid problems and -misunderstanding you should rely exclusively on time values provided by the -`srt_time_now()` and `srt_connection_time()` functions. +* At the packet arrival time, grab the current time at that moment using +`srt_time_now()`. + +* When you want a pre-calculated packet time, use a private relative time +counter set at the moment when the connection was made. From the moment when +your first packet is ready, start pre-calculating packet times relative to the +connection start time obtained from `srt_connection_time()`. Although you still +have to synchronize sending times with these predefined times, by explicitly +specifying the source time you avoid the jitter resulting from a lost accuracy +due to waiting time and unfortunate thread scheduling. + +3. STOW mode + +In the STOW mode with eager sending the application is allowed to supply +packets faster than it is legitimate for the live mode. In this case the +packets can remain in the sender buffer until the time comes for them to +be sent, while the timestamp still defines the rhythm in which they should +be sent. This timestamp then defines the relative time between packets to +be kept when then receiver application will receive them, as well as it will +be the ultimate time to keep in the receiver buffer (to wait for recovery) +and the ultimate time to send in the eager mode. + +XXX NOT IMPLEMENTED YET: the time may be supplied automatically in STOW mode +basing on the bandwidth specified by `SRTO_INPUTBW`. Until this is implemented +supplying the timestamps in STOW mode is obligatory. + +Note that `srctime` uses an internally defined clock that is intended to be +monotonic (the definition depends on the build flags, see below). Because of +that **the application should not define this time basing on values obtained +from the system functions for getting the current system time** (such as +`time`, `ftime` or `gettimeofday`). To avoid problems and misunderstanding you +should rely exclusively on time values provided by the `srt_time_now()` and +`srt_connection_time()` functions. The clock used by the SRT internal clock is determined by the following build flags: - `ENABLE_MONOTONIC` makes use of `CLOCK_MONOTONIC` with `clock_gettime` function. - `ENABLE_STDXXX_SYNC` makes use of `std::chrono::steady_clock`. The default is currently to use the system clock as the internal SRT clock, -although it's highly recommended to use one of the above monotonic clocks, -as system clock is vulnerable to time modifications during transmission. +although it's highly recommended to use one of the above monotonic clocks, as +system clock is vulnerable to time modifications during transmission. ### srt_time_now diff --git a/docs/features/encryption.md b/docs/features/encryption.md index 67a44bfbf..956163d0c 100644 --- a/docs/features/encryption.md +++ b/docs/features/encryption.md @@ -162,6 +162,14 @@ To avoid this, each direction uses its own SEK and Salt: Two older versions connected together still share the same key in both directions. +With scheduled sending, application data enters the preschedule queue while the +initial refresh is pending. A regular packet receives its send time only after +the encryption gate opens and flight-window capacity is available. The +`CUDT::checkSndTimers` key-management state machine drives the refresh and retries; +acknowledging the refreshed key wakes the multiplexer scheduler. Invalid responses +or delayed acknowledgments of the original handshake key do not open the gate. +Blocked sockets do not prevent other sockets or retransmissions from progressing. + The cryptographic usage limit of the KEK is 248 wraps (AESKW) which means virtual infinity at the expected SEK rekeying rate (90000 years to rekey 100 keys every second). [figure1]: images/srt-encryption-1.png diff --git a/srtcore/buffer_snd.cpp b/srtcore/buffer_snd.cpp index ef3114d3b..97566309b 100644 --- a/srtcore/buffer_snd.cpp +++ b/srtcore/buffer_snd.cpp @@ -268,6 +268,52 @@ EncryptionKeySpec CSndBuffer::encryptPacket(CSndPacket& w_packet, CCryptoControl return f; } +int CSndBuffer::extractPacket(int32_t seqno, CSndPacket& w_packet, time_point& w_srctime, time_point& w_nextuniquets) +{ + // This one is simpler - you just extract packet with given seqno. + // Should be unique, but just trust the seqno. Reject returning anything + // only if this seqno is not found in the buffer. + ScopedLock bufferguard(m_BufLock); + + w_nextuniquets = time_point(); // default: none + + // 1. Empty buffer + if (m_Packets.empty()) + { + HLOGC(bslog.Debug, log << "extractPacket(%" << seqno << "): sender buffer empty"); + return READ_NONE; + } + + // XXX unused? time_point entertime = steady_clock::now(); + + int offset = CSeqNo::seqoff(m_iSndLastDataAck, seqno); + if (offset < 0 || offset >= int(m_Packets.size())) + { + HLOGC(bslog.Debug, log << "extractPacket(%" << seqno << "): not in the buffer"); + return READ_NONE; + } + + SndPktArray::Packet& p = m_Packets[offset]; + w_srctime = p.m_tsOriginTime; + + w_packet.pkt.m_pcData = p.m_pcData; + w_packet.pkt.setLength(p.m_iLength, m_iBlockLen); + w_packet.acquire((p), this); + + w_packet.pkt.set_seqno(p.m_iSeqNo); + w_packet.pkt.set_msgflags(p.m_iMsgNoBitset); + + HLOGC(bslog.Debug, log << CONID() << "CSndBuffer: SCHEDULED packet to send: size=" << p.m_iLength + << " #" << w_packet.pkt.getMsgSeq() + << " %" << w_packet.pkt.seqno() + << " !" << BufferStamp(w_packet.pkt.m_pcData, w_packet.pkt.getLength())); + + if (offset + 1 < int(m_Packets.size())) // WE DO HAVE a next packet after this one + w_nextuniquets = m_Packets[offset+1].m_tsOriginTime; + + return p.m_iLength; +} + int CSndBuffer::extractUniquePacket(CSndPacket& w_packet, time_point& w_srctime, int32_t& w_lastseqno, time_point& w_nextuniquets) { @@ -588,6 +634,75 @@ int CSndBuffer::popLostSeq() return seq; } +// This function should return the first loss +pair CSndBuffer::peekRexmit(const duration& min_rexmit_interval, time_point& w_send_time) +{ + ScopedLock bufferguard(m_BufLock); + + int seq = SRT_SEQNO_NONE; + + HLOGC(qslog.Debug, log << "REXMIT: looking for loss report since %" << m_iSndLastDataAck << "..."); + + time_point now = steady_clock::now(); + + pair offs = m_Packets.peekLoss(min_rexmit_interval, now, (w_send_time)); + + // No loss found - return 0: no lost packets extracted. + if (offs.first == -1) + { + HLOGC(qslog.Debug, log << "REXMIT: no loss found"); + return make_pair(seq, seq); + } + + seq = CSeqNo::incseq(m_iSndLastDataAck, offs.first); + return make_pair(seq, offs.second == -1 ? SRT_SEQNO_NONE : CSeqNo::incseq(m_iSndLastDataAck, offs.second)); +} + +int CSndBuffer::extractRexmitPacket(int32_t seqno, CSndPacket& w_sndpkt, sync::steady_clock::time_point& w_tsOrigin) +{ + ScopedLock bufferguard(m_BufLock); + + int offset = CSeqNo::seqoff(m_iSndLastDataAck, seqno); + if (offset < 0 || offset >= int(m_Packets.size())) + { + // If a release attempt is done, it usually means that this sequence + // IS PRESENT in the buffer; such a situation shouldn't happen. + LOGC(bslog.Fatal, log << "IPE: CSndBuffer::releasePacket:%" << seqno << " with in-buffer %" + << m_iSndLastDataAck << "+" << m_Packets.size() << " - OUT OF RANGE REQUEST"); + return 0; + } + + // Ok, now we know that offset is valid; now check if it is eligible + // for loss retransmission; if not, return 0. + if (!m_Packets.extractLoss(offset)) + { + return 0; + } + + // Ok, the packet at this position was revoked from loss, + // return it. We don't use readPacketInternal because it hadles the + // stale state. In scheduled mode stale state should be handled in peekLoss(). + SndPktArray::Packet* p = &m_Packets[offset]; + + w_sndpkt.pkt.m_pcData = p->m_pcData; + const int readlen = p->m_iLength; + w_sndpkt.pkt.setLength(readlen, m_iBlockLen); + + // We state that the requested seqno refers to a historical (not unique) + // packet, hence the encryption action has encrypted the data and updated + // the flags. + w_sndpkt.pkt.set_msgflags(p->m_iMsgNoBitset); + w_tsOrigin = p->m_tsOriginTime; + + // This function is called when packet retransmission is triggered. + // Therefore we are setting the rexmit time. + p->m_tsRexmitTime = steady_clock::now(); + + ++p->m_iBusy; + w_sndpkt.acquire_busy(p->m_iSeqNo, this); + + return readlen; +} void CSndBuffer::releasePacket(int32_t seqno) { @@ -1772,6 +1887,298 @@ int SndPktArray::extractFirstLoss(const duration& miniv, const time_point& now) return -1; } +explicit_t SndPktArray::extractLoss(int offset) +{ + // No loss at all + if (m_iFirstRexmit == -1) + { + SRT_ASSERT(m_iLastRexmit == -1); + return false; + } + + Packet& p = m_PktQueue[offset]; + + // This packet is not scheduled for retransmission. + // (if it's zombie, simply leave it there). + if (sync::is_zero(p.m_tsNextRexmitTime)) + return false; + + // Clear this as retransmitted. + p.m_tsNextRexmitTime = time_point(); + + // Ok, first find it among the lists. + int recbegin = -1; + int recprev = -1; + int recoff = 0; + + if (offset < m_iFirstRexmit) + { + // XXX not sure if this really is IPE. Could be that it disappeared in the meantime. + // LOGC(bslog.Error, log << "IPE: extractLoss: offset=" << offset << " < first=" << m_iFirstRexmit); + return false; + } + + // Identify the record that contains this one. + if (offset >= m_iLastRexmit) + { + recbegin = m_iLastRexmit; + recoff = offset - recbegin; + if (recoff >= m_PktQueue[recbegin].m_iLossLength) + return false; + } + else + { + Tie(recbegin, recprev) = find_loss_rec(offset); + if (recbegin == -1) + { + // Not found even after past the last one. + return false; + } + recoff = offset - recbegin; + } + + // Alright, we have then our record. Now we remove the packet. + + // Now check if we want to just modify a single record, or + // eliminate the record entirely. The second one is only if + // you have position 0. + + int updated_next_record = -1; + Packet& recp = m_PktQueue[recbegin]; + if (recoff == 0 && recp.m_iLossLength == 1) + { + if (offset == m_iLastRexmit) + { + if (offset == m_iFirstRexmit) + { + // Doing this now because this branch does quick return. + recp.m_iLossLength = 0; + recp.m_iNextLossRecOff = 0; + + // Simplest: remove eveything. + recp.m_iLossLength = 0; + recp.m_iNextLossRecOff = 0; + m_iFirstRexmit = -1; + m_iLastRexmit = -1; + m_iLossLengthCache = 0; + + return true; // nothing more to do + } + // updated_next_record = -1; keep this value. + } + else + { + // Still could be first, but that we will know from recorev. + updated_next_record = recbegin + recp.m_iNextLossRecOff; + } + // Must have been delayed because previous m_iNextLossRecOff is used above. + recp.m_iLossLength = 0; + recp.m_iNextLossRecOff = 0; + } + else + { + // MINIMUM 2 packets. + // Check edge cases + + int len_before = offset - recbegin; + int len_after = recp.m_iLossLength - recoff; + + // These two conditions cannot be satisfied simultaneously + // when loss lenght == 1 is ruled out. + if (len_before == 0) + { + // Simple: shift record by one + int newrec = recbegin + 1; + m_PktQueue[newrec].m_iLossLength = recp.m_iLossLength - 1; + if (offset != m_iLastRexmit) // IS NOT last => update next ptr + { + m_PktQueue[newrec].m_iNextLossRecOff = recp.m_iNextLossRecOff - 1; + } + else + { + m_iLastRexmit = newrec; + } + + // The cell previously starting the record is now free. + recp.m_iLossLength = 0; + recp.m_iNextLossRecOff = 0; + + // Set it because it was shifted + updated_next_record = newrec; + } + else if (len_after == 1) // length = last + 1 + { + // No change in the position, just cut the tail. + --recp.m_iLossLength; + } + else + { + // This means we have at least 3 packets in the record. + // (next record is still where it was before). + + int newrec = offset + 1; + int nextrecoff; + if (recbegin == m_iLastRexmit) + { + nextrecoff = 0; + m_iLastRexmit = newrec; + } + else + { + nextrecoff = recbegin + recp.m_iNextLossRecOff - newrec; + } + + m_PktQueue[newrec].m_iLossLength = len_after - 1; // except the removed one + m_PktQueue[newrec].m_iNextLossRecOff = nextrecoff; + + recp.m_iLossLength = len_before; + recp.m_iNextLossRecOff = newrec - recbegin; + } + } + + if (updated_next_record != -1) + { + // BUT: we also need to update the previous one. + + // For last record we haven't checked previous; find it now. + // NOTE: DO NOT find if recprev == -1 because it means that + // we already know that recbegin == m_iFirstRexmit. + if (recbegin == m_iLastRexmit) + { + Tie(recbegin, recprev) = find_loss_rec(offset); + if (recbegin == -1) + { + // Not found even after past the last one. + // (just in case - at this stage should be impo) + return false; + } + } + + if (recprev == -1) + { + m_iFirstRexmit = updated_next_record; + } + else + { + m_PktQueue[recprev].m_iNextLossRecOff = updated_next_record - recprev; + if (recbegin == m_iLastRexmit) + { + m_iLastRexmit = updated_next_record; + } + } + } + + --m_iLossLengthCache; + + string msg SRT_ATR_UNUSED; + SRT_ASSERT(validateLossIntegrity((msg))); + + return true; +} + + +// Special version for scheduled sending: peek the next candidate +// and the follower, if found. Still do remove all outdated loss +// entries, skip too early entries etc. +pair SndPktArray::peekLoss(const duration& miniv, const time_point& now, time_point& w_send_time) +{ + // No loss at all + if (m_iFirstRexmit == -1) + return make_pair(-1, -1); + + // Exactly one packet available (we believe that "zombies" + // can only follow at least one remaining loss record, so it's not possible + // if only one loss record is available + if (m_iLossLengthCache == 1) + { + // We have exactly one packet; report it, regardless of conditions. + return make_pair(m_iFirstRexmit, -1); + } + + // Note that records may have: + // - "zombie": marked for non-eligible by cleared time - remove if possible + // - "too new": such time that time + miniv > now() - this record must remain + // + // If during the search there was no "too new" record found yet, remove + // every "zombie" on the way, and also the extracted record. If any "too + // new" was found, do not remove anything anymore and the qualified record + // should be also marked "zombie" when retrieved (none found is also possible). + int stop_revoke = -1; + int last_cleared = -1; + bool skipped_too_new = false; + + int off_planned = -1, off_next = -1; + + // Try to find the first one with skipping all "too early" ones. + for (int loss_begin = m_iFirstRexmit; loss_begin != -1; loss_begin = next_loss_rec(loss_begin)) + { + int loss_end = loss_begin + m_PktQueue[loss_begin].m_iLossLength; + + for (int i = loss_begin; i != loss_end; ++i) + { + SndPktArray::Packet& p = m_PktQueue[i]; + if (!is_zero(p.m_tsNextRexmitTime)) + { + // Ok, so this cell will be taken, but it might be the future. + if (!p.updated_rexmit_time_passed(now, miniv)) + { + if (stop_revoke == -1 && i > 0) + stop_revoke = i - 1; + skipped_too_new = true; + HLOGC(qslog.Debug, log << "... skipped +" << i << " - too early by " + << FormatDurationAuto(now + miniv - p.m_tsNextRexmitTime)); + continue; + } + + // --- found --- + off_planned = i; + // Check the next loss + off_next = i + 1; + if (off_next == loss_end) + { + // Worst case, it will turn back to -1. + off_next = next_loss_rec(off_planned); + } + goto Break2; // break 2 loops + } + else // Zombie + { + HLOGC(qslog.Debug, log << "... skipped +" << i << " - cleared earlier"); + if (!skipped_too_new) + last_cleared = i; + } + // If it was cleared, continue searching. + } + } + +Break2: + + if (last_cleared != -1) + { + remove_loss(last_cleared); + } + + // If not found any "old enough" loss record, simply return + // the first and second loss you found. We know that there + // at least 2 loss reports. + if (off_planned == -1) + { + off_planned = m_iFirstRexmit; + if (m_PktQueue[off_planned].m_iLossLength > 1) + { + off_next = off_planned + 1; + } + else + { + off_next = next_loss_rec(m_iFirstRexmit); + } + } + + w_send_time = m_PktQueue[off_planned].m_tsOriginTime; + + return make_pair(off_planned, off_next); +} + // Debug support string SndPktArray::show_external(int32_t seqno, int32_t lastsent_seqno) const { diff --git a/srtcore/buffer_snd.h b/srtcore/buffer_snd.h index 80c92a60e..4801d9e82 100644 --- a/srtcore/buffer_snd.h +++ b/srtcore/buffer_snd.h @@ -248,6 +248,13 @@ struct SndPktArray // For spontaneous extraction (request to get the first still eligible) int extractFirstLoss(const duration& min_interval, const time_point& now); + // This tries to extract the packet with explicitly given seqno. + explicit_t extractLoss(int32_t seqno); + + // For scheduled extraction: first find out what packets are still eligible, + // then extract the exact ones. + std::pair peekLoss(const duration& miniv, const time_point& now, time_point& w_send_time); + // Lightweight version of extractFirstLoss for testing purposes only. // Finds the first packet marked as loss and removes this mark. int extractFirstLoss() @@ -439,6 +446,16 @@ class CSndBuffer int extractUniquePacket(CSndPacket& w_packet, time_point& w_origintime, int32_t& w_lastseqno, time_point& w_nextuniquets); + /// Get access to the packet at the unique position with pointed sequence number. + /// + /// @param [out] w_packet Object to keep the packet + /// @param [out] w_origintime Scheduling time of the packet + /// @param [out] w_nextuniquets Set to the time of the packet next to extracted unique or zero time if no such packet + /// @param [in] seqno Sequence number of the packet to extract + /// @return Actual length of data read. + SRT_TSA_NEEDS_NONLOCKED(m_BufLock) + int extractPacket(int32_t seqno, CSndPacket& w_packet, time_point& w_origintime, time_point& w_nextuniquets); + struct DropRange { static const size_t BEGIN = 0, END = 1; @@ -452,10 +469,20 @@ class CSndBuffer SRT_TSA_NEEDS_NONLOCKED(m_BufLock) int readOldPacket(int32_t seqno, CSndPacket& w_packet, time_point& w_origintime, DropRange& w_drop); + // Normal sending + SRT_TSA_NEEDS_NONLOCKED(m_BufLock) int extractFirstRexmitPacket(const duration& min_rexmit_interval, int32_t& w_current_seqno, CSndPacket& w_sndpkt, sync::steady_clock::time_point& w_tsOrigin, std::vector& w_drops); + // Scheduled sending + + SRT_TSA_NEEDS_NONLOCKED(m_BufLock) + std::pair peekRexmit(const duration& min_rexmit_interval, time_point& w_send_time); + + SRT_TSA_NEEDS_NONLOCKED(m_BufLock) + int extractRexmitPacket(int32_t seqno, CSndPacket& w_sndpkt, sync::steady_clock::time_point& w_tsOrigin); + private: SRT_TSA_NEEDS_LOCKED(m_BufLock) int readPacketInternal(int offset, CSndPacket& w_packet, time_point& w_origintime, DropRange& w_drop); diff --git a/srtcore/congctl.cpp b/srtcore/congctl.cpp index 054897591..41b06cdff 100644 --- a/srtcore/congctl.cpp +++ b/srtcore/congctl.cpp @@ -631,6 +631,19 @@ SrtCongestion::NamePtr SrtCongestion::congctls[N_CONTROLLERS] = {"file", Creator::Create } }; +bool SrtCongestion::select_sendmode(int sendmode) +{ + // 0 should be handled already, but just follow it. + if (sendmode > 0) + { + selector = 1; // "file" CC + } + else + { + selector = 0; // "live" (normal live mode) + } + return true; +} bool SrtCongestion::configure(CUDT* parent) { diff --git a/srtcore/congctl.h b/srtcore/congctl.h index 2bdf7a9ed..f4d9de648 100644 --- a/srtcore/congctl.h +++ b/srtcore/congctl.h @@ -80,6 +80,19 @@ class SrtCongestion return true; } + bool select(const std::string& name, int sendmode) + { + // Ok, if sendmode is 0, allow selection by name. + // Otherwise enforce "live" mode by all means, but select + // the "file" CC. + if (sendmode == 0) + return select(name); + + return select_sendmode(sendmode); + } + + bool select_sendmode(int sendmode); + std::string selected_name() { if (selector == N_CONTROLLERS) diff --git a/srtcore/core.cpp b/srtcore/core.cpp index a0132384b..784b6b58e 100644 --- a/srtcore/core.cpp +++ b/srtcore/core.cpp @@ -246,6 +246,11 @@ void SocketKeeper::acquire_socket(CUDTSocket* s) void SocketKeeper::acquire_LOCKED(CUDTSocket* s) { + SRT_ASSERT(s); + SRT_ASSERT(!socket || socket == s); + if (!s || (socket && socket != s)) + return; + socket = s; s->apiAcquire(); } @@ -1144,7 +1149,7 @@ void CUDT::open() m_tsNextACKTime.store(currtime + m_tdACKInterval); m_tsNextNAKTime.store(currtime + m_tdNAKInterval); m_tsLastRspAckTime = currtime; - m_tsLastSndTime.store(currtime); + m_LastSend.collect(currtime); #if SRT_ENABLE_BONDING m_tsUnstableSince = steady_clock::time_point(); @@ -2216,7 +2221,7 @@ CUDT::MsgHandled CUDT::processSrtMsg(const CPacket *ctrlpkt) HLOGC(cnlog.Debug, log << CONID() << "Dispatching message type=" << etype << " data length=" << (len / sizeof(int32_t))); - ScopedLock cl (m_ConnectionLock); + UniqueLock cl (m_ConnectionLock); if (!stillConnected()) { HLOGC(inlog.Debug, log << CONID() << "... NOT DISPATCHING - connection is closed"); @@ -2253,8 +2258,13 @@ CUDT::MsgHandled CUDT::processSrtMsg(const CPacket *ctrlpkt) { const bool was_gated = m_CryptoControl.isSndDataGated(); m_CryptoControl.processSrtMsg_KMRSP(srtdata, len, m_uPeerSrtVersion, false); - if (was_gated && !m_CryptoControl.isSndDataGated()) + const bool became_ready = was_gated && !m_CryptoControl.isSndDataGated(); + const bool scheduled = m_config.uSenderMode != 0; + if (became_ready && !scheduled) m_pMuxer->updateSendNormal(m_parent); + cl.unlock(); + if (became_ready && scheduled) + m_pMuxer->notifyScheduledSendReady(); return MSGH_OK; // nothing to do } @@ -2700,6 +2710,8 @@ bool CUDT::interpretSrtHandshake(CUDTSocket* lsn SRT_ATR_UNUSED, const CHandShak if (IsSet(ext_flags, CHandShake::HS_EXT_HSREQ)) { HLOGC(cnlog.Debug, log << CONID() << FUNID() << ": extracting HSREQ/RSP type extension"); + m_CryptoControl.setPeerSecDist(IsSet(m_uPeerSrtFlags, SRT_OPT_SECDIST)); + uint32_t *begin = p; uint32_t *next = 0; size_t length = size / sizeof(uint32_t); @@ -6340,7 +6352,7 @@ SRT_REJECT_REASON CUDT::setupCC() } } - bool res = m_CongCtl.select(m_config.sCongestion.str()); + bool res = m_CongCtl.select(m_config.sCongestion.str(), m_config.uSenderMode); if (!res || !m_CongCtl.configure(this)) { return SRT_REJ_CONGESTION; @@ -6391,7 +6403,7 @@ SRT_REJECT_REASON CUDT::setupCC() m_tsNextACKTime.store(currtime + m_tdACKInterval); m_tsNextNAKTime.store(currtime + m_tdNAKInterval); m_tsLastRspAckTime = currtime; - m_tsLastSndTime.store(currtime); + m_LastSend.collect(currtime); #ifdef SRT_ENABLE_RATE_MEASUREMENT HLOGC(bslog.Debug, log << CONID() << "RATE-MEASUREMENT: initializing time TS=" << FormatTime(currtime)); @@ -7181,12 +7193,28 @@ int CUDT::sendmsg2(const char *data, int len, SRT_MSGCTRL& w_mctrl) throw CUDTException(MJ_NOTSUP, MN_INVALMSGAPI); } + /* XXX Check again. When using file mode at the reception side, the + timestamp is ignored anyway, so this is likely not necessary. if ((!m_config.bMessageAPI || !m_bTsbPd)) { HLOGC(aslog.Warn, log << CONID() << "Source time can only be used with TSBPD and Message API enabled. Using default time instead."); w_mctrl.srctime = 0; } + */ + } + else + { + // SRCTIME is undefined - this is not acceptable in STOW sender modes + // XXX NOTE: it should be doable to tolerate this situation if SRT + // can simply generate the right source times, or when there's some + // method to allow extended time for recovery at the receiver side. + if (m_config.uSenderMode != 0) + { + LOGC(aslog.Error, log << CONID() << "sendmsg2: with sender mode=" << m_config.uSenderMode + << " declaring packet send time is obligatory"); + throw CUDTException(MJ_NOTSUP, MN_INVALMSGAPI); + } } const int64_t send_srctime = w_mctrl.srctime @@ -7225,9 +7253,16 @@ int CUDT::sendmsg2(const char *data, int len, SRT_MSGCTRL& w_mctrl) } } - // Insert this socket to the snd list if it is not on the list already. - // m_pMuxer->sndUList()->pop may lock CSndUList::m_ListLock and then m_RecvAckLock - m_pMuxer->updateSendNormal(m_parent); + if (m_config.uSenderMode == 0) + { + // Insert this socket to the snd list if it is not on the list already. + m_pMuxer->updateSendNormal(m_parent); + } + else + { + time_point dst_time = time_point() + microseconds_from(w_mctrl.srctime); + scheduleRegular(seqno, steady_clock::now(), dst_time); + } #ifdef SRT_ENABLE_ECN // IF there was a packet drop on the sender side, report congestion to the app. @@ -7242,6 +7277,143 @@ int CUDT::sendmsg2(const char *data, int len, SRT_MSGCTRL& w_mctrl) return size; } +// [[affinity(main)]] +void CUDT::scheduleRegular(int32_t seqno, const time_point& currtime, const time_point& exp_sendtime) +{ + m_LastSched.lazyStart(seqno, currtime); + time_point delivery = exp_sendtime + milliseconds_from(m_iPeerTsbPdDelay_ms) + slippedRTT(2); + m_pMuxer->scheduleSend(m_parent, seqno, sched::TP_REGULAR, exp_sendtime, delivery); +} + +void CUDT::scheduleRexmit(int32_t seqno, const time_point& exp_sendtime) +{ + time_point delivery = exp_sendtime + milliseconds_from(m_iPeerTsbPdDelay_ms) + slippedRTT(2); + m_pMuxer->scheduleSend(m_parent, seqno, sched::TP_REXMIT, exp_sendtime, delivery); +} + + +steady_clock::time_point CSendPipeManager::updateNext(const steady_clock::time_point& currtime, + const steady_clock::duration& send_interval) +{ + duration passed_time = currtime - m_tsTime.load(); + + int deviation = count_microseconds(send_interval - passed_time); + m_tdAverageDeviation_us = avg_iir_lazy<16>(m_tdAverageDeviation_us, deviation); + + time_point nextperiod_time = m_tsTime.load() + send_interval; + time_point forfeit_time = currtime - send_interval * 2; + + HLOGC(qslog.Debug, log << "PLAN: now=" << FormatTime(currtime) + << " LAST:" << FormatDurationAuto(m_tsTime.load() - currtime, true) + << " NEXT:" << FormatDurationAuto(nextperiod_time - currtime, true) + << " PAST-LAST: " << FormatDurationAuto(forfeit_time - nextperiod_time, true)); + + if (nextperiod_time + seconds_from(1) < currtime) + nextperiod_time = forfeit_time; + + if (nextperiod_time < currtime) + { + nextperiod_time = currtime; + } + // Remember this as the last sending time + // NOTE: We remember this as the last sending time, + // regardless if we have used the time override or not. + // Time override is used when a regular packet's expected sending + // time is earlier than nextperiod_time. In this case we send it + // faster than according to the current speed, but we still remember + // this time as the last sent in order to keep up with the speed. + m_tsTime = nextperiod_time; + + return m_tsTime; +} + +// [[using affinity(CSndQueue::m_WorkerThread)]] +// (Can't use SRT_ASSERT_AFFINITY because CUDT has no access to CMultiplexer::m_SndQueue) +bool CUDT::planSendingTime(sched::Type type, const CUDT::time_point& latest_delivery, CUDT::time_point& w_sendtime) +{ + // type: check if TP_REXMIT, otherwise it's regular + // latest_delivery: the latest possible time when it should be delivered + // w_sendtime: + // [IN] The PLANNED time for the packet (in eager mode, the latest time to send the regular packet) + // [ONT] The resulting time when the packet should be sent + // + // Return: + // * true: this scheduled packet shall be sent + // * false: please drop this send request + + // Ok, the state update rules: + // + // m_tsTime is the time when the packet was sent last time. + // This should be set to the time written into w_sendtime. + // All calculations start from this time. It should not be zero any time. + + time_point currtime = steady_clock::now(); + + // Could assert, but let's just do a quick check + if (m_LastSched.updateFirst(currtime)) + { + HLOGC(qslog.Debug, log << "PLAN: First time, set as NOW: " << FormatTime(m_LastSched.lastTime())); + + // Yes, will be in the past during pickup; doesn't matter. + w_sendtime = m_LastSched.lastTime(); + if (type == sched::TP_REXMIT && w_sendtime > latest_delivery) + return false; + return true; + } + + time_point nextperiod_time = m_LastSched.updateNext(currtime, m_tdSendInterval); + + // Ok, so nextperiod_time is now the time that should be set + // as w_sendtime; we need to check it if it's in forward. + // If so, take it as a good deal. + if (nextperiod_time <= w_sendtime) + { + HLOGC(qslog.Debug, log << "PLAN: ... PLANTIME=" << FormatTime(w_sendtime) << " - speedup by " + << FormatDurationAuto(w_sendtime - nextperiod_time)); + + if (type == sched::TP_REXMIT && nextperiod_time > latest_delivery) + return false; + w_sendtime = nextperiod_time; + return true; + } + + // Ok, now we are past the intended sending time. + // Additional rules: + + // For regular (first-time, unique) packets, we allow + // for overdrive, no matter how fast it would get. + + if (type != sched::TP_REXMIT) + { + HLOGC(qslog.Debug, log << "PLAN: ... PLANTIME=" << FormatTime(w_sendtime) << " KEPT - override NEXTSEND by " + << FormatDurationAuto(nextperiod_time - w_sendtime)); + // REGULAR + // Leave the send time as is, while keeping the last + // sending time as the alleged next sending time. + return true; + } + + // For retransmitted packets, qualify them to send at + // this predicted time. Except they are already past the + // latest possible delivery time, in which case deny them. + + if (nextperiod_time > latest_delivery) + { + HLOGC(qslog.Debug, log << "PLAN:... REXMIT nextsend=" << FormatTime(nextperiod_time) + << " exceeds latest delivery by " << FormatDurationAuto(nextperiod_time - latest_delivery) + << " - NOT PLANNING"); + return false; + } + + // Condition: next delivery time is past w_sendtime, but still before latest_delivery + // Plan regularly, without override. + + HLOGC(qslog.Debug, log << "PLAN:... REXMIT nextsend=" << FormatTime(nextperiod_time) + << " - PLANNING at this time; " << FormatDurationAuto(currtime - nextperiod_time) << " towards NOW"); + w_sendtime = nextperiod_time; + return true; +} + bool CUDT::updateCryptoOnSending(bool force_refresh) { int keyindex[2] = {-1, -1}; @@ -7663,6 +7835,12 @@ int64_t CUDT::sendfile(fstream &ifs, int64_t &offset, int64_t size, int block) throw CUDTException(MJ_SETUP, MN_SECURITY, 0); } + if (m_config.uSenderMode != 0) + { + LOGC(aslog.Error, log << CONID() << "In FILE mode the scheduled sender mode is not supported"); + throw CUDTException(MJ_NOTSUP, MN_INVALBUFFERAPI); + } + ScopedLock sendguard (m_SendLock); if (m_pSndBuffer->getCurrBufSize() == 0) @@ -8466,7 +8644,7 @@ void CUDT::sendCtrl(UDTMessageType pkttype, const int32_t* lparam, void* rparam, // Fix keepalive if (nbsent) - m_tsLastSndTime.store(steady_clock::now()); + m_LastSend.update(steady_clock::now(), nbsent); } bool CUDT::getFirstNoncontSequence(int32_t& w_seq, string& w_log_reason) @@ -8857,7 +9035,9 @@ bool CUDT::revokeACKedSequences(int32_t ackdata_seqno, int32_t& w_last_sent_seqn HLOGC(inlog.Debug, log << "ACK: kicking the send schedule/cond"); // insert this socket to snd list if it is not on the list yet - const steady_clock::time_point currtime = m_pMuxer->updateSendNormal(m_parent); + const steady_clock::time_point currtime = (m_config.uSenderMode == 0) + ? m_pMuxer->updateSendNormal(m_parent) + : steady_clock::now(); if (m_config.bSynSending) { @@ -8894,6 +9074,7 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point // Note: minimum of one 4-byte field is granted before the call. const int32_t ackdata_seqno = ackdata[ACKD_RCVLASTACK]; + bool notify_scheduled_capacity = false; // Check the value of ACK in case when it was some rogue peer if (ackdata_seqno < 0) @@ -8933,15 +9114,23 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point // Process a lite ACK if (isLiteAck) { - ScopedLock ack_lock(m_RecvAckLock); - if (CSeqNo::seqcmp(ackdata_seqno, m_iSndLastAck) >= 0) { - m_iFlowWindowSize = m_iFlowWindowSize - CSeqNo::seqoff(m_iSndLastAck, ackdata_seqno); - m_iSndLastAck = ackdata_seqno; - m_iSndMinFlightSpan = getFlightSpan(); - m_tsLastRspAckTime = currtime; - m_iReXmitCount = 1; // Reset re-transmit count since last ACK + ScopedLock ack_lock(m_RecvAckLock); + if (CSeqNo::seqcmp(ackdata_seqno, m_iSndLastAck) >= 0) + { + const int cwnd1 = std::min(m_iFlowWindowSize, m_iCongestionWindow); + const bool was_stuck = cwnd1 <= getFlightSpan(); + m_iFlowWindowSize = m_iFlowWindowSize - CSeqNo::seqoff(m_iSndLastAck, ackdata_seqno); + m_iSndLastAck = ackdata_seqno; + m_iSndMinFlightSpan = getFlightSpan(); + m_tsLastRspAckTime = currtime; + m_iReXmitCount = 1; // Reset re-transmit count since last ACK + const int cwnd = std::min(m_iFlowWindowSize, m_iCongestionWindow); + notify_scheduled_capacity = m_config.uSenderMode != 0 && was_stuck && cwnd > getFlightSpan(); + } } + if (notify_scheduled_capacity) + m_pMuxer->notifyScheduledSendReady(); return true; } @@ -9011,13 +9200,20 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point m_tsLastRspAckTime = currtime; m_iReXmitCount = 1; // Reset re-transmit count since last ACK - const int cwnd = std::min(m_iFlowWindowSize, m_iCongestionWindow); - if (bWasStuck && cwnd > getFlightSpan()) + if (m_config.uSenderMode == 0) { - m_pMuxer->updateSendNormal(m_parent); - HLOGC(gglog.Debug, - log << CONID() << "processCtrlAck: could reschedule SND. iFlowWindowSize " << m_iFlowWindowSize - << " SPAN " << getFlightSpan() << " ackdataseqno %" << ackdata_seqno); + const int cwnd = std::min(m_iFlowWindowSize, m_iCongestionWindow); + if (bWasStuck && cwnd > getFlightSpan()) + { + m_pMuxer->updateSendNormal(m_parent); + HLOGC(gglog.Debug, + log << CONID() << "processCtrlAck: could reschedule SND. iFlowWindowSize " << m_iFlowWindowSize + << " SPAN " << getFlightSpan() << " ackdataseqno %" << ackdata_seqno); + } + } + else + { + notify_scheduled_capacity = bWasStuck; } } @@ -9035,6 +9231,9 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point if (CSeqNo::seqoff(m_iSndLastFullAck, ackdata_seqno) <= 0) { // discard it if it is a repeated ACK + ack_lock.unlock(); + if (notify_scheduled_capacity && hasSendCapacity()) + m_pMuxer->notifyScheduledSendReady(); return true; } m_iSndLastFullAck = ackdata_seqno; @@ -9145,6 +9344,9 @@ bool CUDT::processCtrlAck(const CPacket &ctrlpkt, const steady_clock::time_point updateCC(TEV_ACK, EventVariant(ackdata_seqno)); + if (notify_scheduled_capacity && hasSendCapacity()) + m_pMuxer->notifyScheduledSendReady(); + m_StatsLock.lock(); m_stats.sndr.recvdAck.count(1); m_StatsLock.unlock(); @@ -9265,6 +9467,11 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt) // when logging is forcefully off. int32_t wrong_loss SRT_ATR_UNUSED = SRT_SEQNO_NONE; + // Will be used to determine rexmit packets to schedule. + // If remain with this value, there's nothing to schedule. + int32_t first_loss = SRT_SEQNO_NONE; + time_point first_send_time; + { #if SRT_ENABLE_BONDING // Keep the group from disappearing in the meantime @@ -9332,7 +9539,8 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt) { HLOGC(inlog.Debug, log << CONID() << "LOSSREPORT: adding " << losslist_lo << " - " << losslist_hi << " to loss list"); - num = m_pSndBuffer->insertLoss(losslist_lo, losslist_hi, steady_clock::now()); + num = m_pSndBuffer->insertLoss(losslist_lo, losslist_hi, steady_clock::now(), (first_send_time)); + first_loss = losslist_lo; } // ELSE losslist_lo <% m_iSndLastAck else @@ -9356,9 +9564,11 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt) { HLOGC(inlog.Debug, log << CONID() << "LOSSREPORT: adding " << m_iSndLastAck << "[ACK] - " << losslist_hi << " to loss list"); - num = m_pSndBuffer->insertLoss(m_iSndLastAck, losslist_hi, steady_clock::now()); + num = m_pSndBuffer->insertLoss(m_iSndLastAck, losslist_hi, steady_clock::now(), (first_send_time)); dropreq_hi = CSeqNo::decseq(m_iSndLastAck); IF_HEAVY_LOGGING(drop_type = "partially"); + + first_loss = m_iSndLastAck; } // In distinction to losslist, DROPREQ has always just one range, @@ -9398,7 +9608,8 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt) HLOGC(inlog.Debug, log << CONID() << "LOSSREPORT: adding %" << losslist[i] << " (1 packet) to loss list"); - num = m_pSndBuffer->insertLoss(losslist[i], losslist[i], steady_clock::now()); + num = m_pSndBuffer->insertLoss(losslist[i], losslist[i], steady_clock::now(), (first_send_time)); + first_loss = losslist[i]; m_StatsLock.lock(); m_stats.sndr.lost.count(num); @@ -9420,6 +9631,18 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt) } } + if (m_config.uSenderMode != 0 && first_loss != SRT_SEQNO_NONE) + { + if (!is_zero(first_send_time)) // theoretically can happen in buffer empty - paranoid check + { + scheduleRexmit(first_loss, first_send_time); + } + else + { + LOGC(qslog.Error, log << "LOSSREPORT: IPE: loss scheduled, but time is empty!"); + } + } + if (!secure) { LOGC(inlog.Warn, @@ -9448,7 +9671,8 @@ bool CUDT::processCtrlLossReport(const CPacket& ctrlpkt) // blind rexmit mode is laterexmit (the sender will repeat sending // unacknowledged packets only when it has reached the limit with // nothing more to withdraw from the sender buffer). - m_pMuxer->updateSendNormal(m_parent); + if (m_config.uSenderMode == 0) + m_pMuxer->updateSendNormal(m_parent); m_StatsLock.lock(); m_stats.sndr.recvdNak.count(1); @@ -9564,7 +9788,7 @@ bool CUDT::processCtrlHS(const CPacket& ctrlpkt) const int nbsent = channel()->sendto(m_PeerAddr, rsppkt, m_SourceAddr); if (nbsent) { - m_tsLastSndTime.store(steady_clock::now()); + m_LastSend.update(steady_clock::now(), nbsent); } } @@ -10059,6 +10283,49 @@ void CUDT::retransmissionConsumeLength(size_t payload_size) #endif } +int CUDT::extractPlannedLoss(SendTaskProto& proto) +{ + // Ok, first extract the first and next loss. NOTE: + // 1. It doesn't mean that any loss is eligible for sending. + // 2. Having both seqno set only means that there's more than one. + + // All we can do here is to plan sending for the losses that are reported. + + duration delivery_slip = milliseconds_from(m_iPeerTsbPdDelay_ms) + slippedRTT(2); + + time_point sendtime; + pair nextloss = m_pSndBuffer->peekRexmit(minRexmitInterval(), (sendtime)); + + // RETURN: + // 0 - There are no lost packets scheduled, including the current one. + // 1 - The current packet was filled, but there are no more losses. + // 2 - The current was filled, and next, too. + if (nextloss.first == SRT_SEQNO_NONE) + { + // LOG: no loss available + return 0; + } + + // Fill the first one + proto.m_Packet.set_socket(m_parent); + proto.m_Packet.m_iSeqNo = nextloss.first; + proto.m_Packet.m_Type = sched::TP_REXMIT; + proto.m_tsSendTime = sendtime; + proto.m_tsLatestDeliveryTime = sendtime + delivery_slip; + + if (!planSendingTime(sched::TP_REXMIT, proto.m_tsLatestDeliveryTime, (proto.m_tsSendTime))) + { + HLOGC(qslog.Debug, log << "REXMIT: dropping expired request for %" << proto.m_Packet.seqno()); + m_pSndBuffer->cancelLostSeq(proto.m_Packet.seqno()); + proto.m_Packet = SchedPacket(); + } + + if (nextloss.second == SRT_SEQNO_NONE) + return 1; + + return 2; +} + int CUDT::packLostData(CSndPacket& w_sndpkt) { #ifdef SRT_ENABLE_MAXREXMITBW @@ -10151,6 +10418,64 @@ int CUDT::packLostData(CSndPacket& w_sndpkt) return w_packet.getLength(); } +int CUDT::packLostData(int32_t seqno, CSndPacket& w_sndpkt) +{ + // Original sending time of the packet, to be stamped after filling. + steady_clock::time_point tsOrigin; + + { + // protect m_iSndLastDataAck from updating by ACK processing + ScopedLock ackguard(m_RecvAckLock); + + const int payload = m_pSndBuffer->extractRexmitPacket(seqno, (w_sndpkt), (tsOrigin)); + m_iSndCurrSeqNo = CSeqNo::maxseq(m_iSndCurrSeqNo, seqno); + + if (payload <= 0) + return 0; + } + + CPacket& w_packet = w_sndpkt.pkt; + + HLOGC(qslog.Debug, log << CONID() << "packed REXMIT packet %" << w_packet.seqno() << " size=" << w_packet.getLength() + << " - still " << m_pSndBuffer->getLossLength() << " LOSS ENTRIES left"); + + // POST-EXTRACTION length fixes. + + // The packet has been ecrypted, thus the authentication tag is expected to be stored + // in the SND buffer as well right after the payload. + if (m_CryptoControl.getCryptoMode() == CSrtConfig::CIPHER_MODE_AES_GCM) + { + w_packet.setLength(w_packet.getLength() + HAICRYPT_AUTHTAG_MAX); + } + +#ifdef SRT_ENABLE_MAXREXMITBW + retransmissionConsumeLength(w_packet.getLength()); +#endif + + m_StatsLock.lock(); + m_stats.sndr.sentRetrans.count(w_packet.getLength()); + m_StatsLock.unlock(); + + // Despite the contextual interpretation of packet.m_iMsgNo around + // CSndBuffer::readOldPacket (extractUniquePacket doesn't return -1), in this particular + // case we can be sure that this is exactly the value of PH_MSGNO as a bitset. + // So, set here the rexmit flag if the peer understands it. + if (m_bPeerRexmitFlag) + { + w_packet.set_msgflags(w_packet.msgflags() | PACKET_SND_REXMIT); + } + setDataPacketTS(w_packet, tsOrigin); + +#ifdef SRT_ENABLE_RATE_MEASUREMENT + m_SndRexmitMeasurement.dataUpdate(1, w_packet.getLength()); + HLOGC(bslog.Debug, log << "RateMeasurement: REXMIT, pkt-size=" << w_packet.getLength() + << " - COLLECTED pkts=" << m_SndRexmitMeasurement.m_nInstaPackets + << " bytes=" << m_SndRexmitMeasurement.m_nInstaBytes); +#endif + + return w_packet.getLength(); +} + #if SRT_DEBUG_TRACE_SND class snd_logger { @@ -10446,7 +10771,7 @@ bool CUDT::packData(CSndPacket& w_sndpkt, steady_clock::time_point& w_nexttime, } else { - if (!packUniqueData((w_sndpkt))) + if (packUniqueData((w_sndpkt)) != PACK_OK) { m_tsNextSendTime = steady_clock::time_point(); m_tdSendTimeDiff = steady_clock::duration::zero(); @@ -10489,7 +10814,7 @@ bool CUDT::packData(CSndPacket& w_sndpkt, steady_clock::time_point& w_nexttime, #endif // Fix keepalive - m_tsLastSndTime.store(enter_time); + m_LastSend.update(enter_time, payload); considerLegacySrtHandshake(steady_clock::time_point()); @@ -10557,17 +10882,128 @@ bool CUDT::packData(CSndPacket& w_sndpkt, steady_clock::time_point& w_nexttime, return payload >= 0; // XXX shouldn't be > 0 ? == 0 is only when buffer range exceeded. } -bool CUDT::packUniqueData(CSndPacket& w_sndpkt) +// Second version of packData: pack the exact data as specified in the specification +// reported from the schedule. +CUDT::PackResult CUDT::packData(const SchedPacket& spec, CSndPacket& w_sndpkt, CNetworkInterface& w_src_addr) +{ + int payload = 0; + bool new_packet_packed = false; + + ScopedLock connectguard(m_ConnectionLock); + // If a closing action is done simultaneously, then + // m_bOpened should already be false, and it's set + // just before releasing this lock. + // + // If this lock is caught BEFORE the closing could + // start the dissolving process, this process will + // not be started until this function is finished. + if (!m_bOpened) + return PACK_UNAVAILABLE; + + time_point enter_time = steady_clock::now(); + w_src_addr = m_SourceAddr; + + CPacket& w_packet = w_sndpkt.pkt; + + IF_HEAVY_LOGGING(const char* reason = ""); // The source of the data packet (normal/rexmit/filter) + if (spec.type() == sched::TP_REXMIT) + { + payload = packLostData(spec.seqno(), (w_sndpkt)); + if (payload <= 0) + return PACK_UNAVAILABLE; + IF_HEAVY_LOGGING(reason = "reXmit"); + } + else if (spec.type() == sched::TP_CONTROL) + { + if (m_PacketFilter.packControlPacket(spec.seqno(), m_CryptoControl.getSndCryptoFlags(), (w_sndpkt.pkt))) + { + HLOGC(qslog.Debug, log << CONID() << "filter: filter/CTL packet ready - packing instead of data."); + payload = (int) w_sndpkt.pkt.getLength(); + IF_HEAVY_LOGGING(reason = "filter"); + + // Stats + ScopedLock lg(m_StatsLock); + m_stats.sndr.sentFilterExtra.count(1); + } + else + { + LOGC(qslog.Error, log << CONID() << "filter: IPE: didn't provide control packet for %" << m_iSndCurrSeqNo + << " that has been scheduled"); + return PACK_ERROR; + } + } + else // type() == TP_REGULAR + { + const PackResult result = packUniqueData(spec.seqno(), (w_sndpkt)); + if (result != PACK_OK) + return result; +#if SRT_ENABLE_MAXREXMITBW + if (m_zSndAveragePacketSize > 0) + { + m_zSndAveragePacketSize = avg_iir<16>(m_zSndAveragePacketSize, w_packet.getLength()); + } + else + { + m_zSndAveragePacketSize = w_packet.getLength(); + } + m_zSndMaxPacketSize = std::max(m_zSndMaxPacketSize, w_packet.getLength()); +#endif + new_packet_packed = true; + + payload = (int) w_packet.getLength(); + IF_HEAVY_LOGGING(reason = "normal"); + } + + w_packet.set_id(m_PeerID); // Set the destination SRT socket ID. + + // XXX This should be done when scheduling the packet first time. + // The problem: m_PacketFilter has assigned affinity to the receiver worker thread, + // while scheduling a packet is happening in the application thread. + if (new_packet_packed && m_PacketFilter) + { + HLOGC(qslog.Debug, log << CONID() << "filter: Feeding packet for source clip"); + m_PacketFilter.feedSource((w_packet)); + } + + HLOGC(qslog.Debug, + log << CONID() << "packData: " << reason << " packet seq=" << w_packet.seqno() << " (ACK=" << m_iSndLastAck + << " ACKDATA=" << m_pSndBuffer->firstSeqNo() << " MSG/FLAGS: " << w_packet.MessageFlagStr() << ")"); + + // This means that the packet will really be sent. + if (payload >= 0) + { + // Fix keepalive + m_LastSend.update(enter_time, payload); + + considerLegacySrtHandshake(steady_clock::time_point()); + + // WARNING: TEV_SEND is the only event that is reported from + // the CSndQueue::worker thread. All others are reported from + // CRcvQueue::worker. If you connect to this signal, make sure + // that you are aware of prospective simultaneous access. + updateCC(TEV_SEND, EventVariant(&w_packet)); + + m_StatsLock.lock(); + m_stats.sndr.sent.count(payload); + if (new_packet_packed) + m_stats.sndr.sentUnique.count(payload); + m_StatsLock.unlock(); + return PACK_OK; + } + + return PACK_ERROR; +} + +CUDT::PackResult CUDT::packUniqueData(CSndPacket& w_sndpkt) { if (m_CryptoControl.isSndDataGated()) { HLOGC(qslog.Debug, log << CONID() << "packUniqueData: waiting for the peer to acknowledge the independent TX key"); - return false; + return PACK_CONGESTED; } int current_sequence_number; // reflexing variable time_point tsOrigin; - int pld_size; { ScopedLock lkrack (m_RecvAckLock); @@ -10579,14 +11015,14 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt) HLOGC(qslog.Debug, log << CONID() << "packUniqueData: CONGESTED: cwnd=min(" << m_iFlowWindowSize << "," << m_iCongestionWindow << ")=" << cwnd << " seqlen=(" << m_iSndLastAck << "-" << m_iSndCurrSeqNo << ")=" << flightspan); - return false; + return PACK_CONGESTED; } + IF_HEAVY_LOGGING(int32_t prev = m_iSndCurrSeqNo); + current_sequence_number = m_iSndCurrSeqNo; // PROXY for atomic; ALSO needed later. time_point next_unique_ts; - pld_size = m_pSndBuffer->extractUniquePacket((w_sndpkt), (tsOrigin), (current_sequence_number), - (next_unique_ts)); - IF_HEAVY_LOGGING(int32_t prev = m_iSndCurrSeqNo); + const int pld_size = m_pSndBuffer->extractUniquePacket((w_sndpkt), (tsOrigin), (current_sequence_number), (next_unique_ts)); m_iSndCurrSeqNo = current_sequence_number; m_tsSndNextUnique = next_unique_ts; @@ -10595,14 +11031,48 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt) { HLOGC(qslog.Debug, log << "packUniqueData: nothing extracted from the buffer; skipped " << CSeqNo::seqoff(prev, current_sequence_number)); - return false; + return PACK_UNAVAILABLE; + } + + HLOGC(qslog.Debug, log << "packUniqueData: extracted %" << current_sequence_number + << " after previous %" << m_iSndCurrSeqNo.load() << " (diff: " + << CSeqNo::seqoff(m_iSndCurrSeqNo, current_sequence_number) << ")"); + } + + return postExtractUniqueData(current_sequence_number, tsOrigin, w_sndpkt); +} + +CUDT::PackResult CUDT::packUniqueData(int32_t seqno, CSndPacket& w_sndpkt) +{ + int current_sequence_number; + time_point tsOrigin; + + { + ScopedLock lkrack (m_RecvAckLock); + current_sequence_number = m_iSndCurrSeqNo; + time_point next_unique_ts; + const int pld_size = m_pSndBuffer->extractPacket(seqno, (w_sndpkt), (tsOrigin), (next_unique_ts)); + m_tsSndNextUnique = next_unique_ts; + + if (pld_size == 0) + { + HLOGC(qslog.Debug, log << "packUniqueData: nothing extracted from the buffer; skipped " + << CSeqNo::seqoff(m_iSndCurrSeqNo, seqno)); + return PACK_UNAVAILABLE; } + m_iSndCurrSeqNo = CSeqNo::maxseq(m_iSndCurrSeqNo, seqno); + HLOGC(qslog.Debug, log << "packUniqueData: extracted %" << current_sequence_number << " after previous %" << m_iSndCurrSeqNo.load() << " (diff: " << CSeqNo::seqoff(m_iSndCurrSeqNo, current_sequence_number) << ")"); } + return postExtractUniqueData(current_sequence_number, tsOrigin, w_sndpkt); +} + +CUDT::PackResult CUDT::postExtractUniqueData(int current_sequence_number, const time_point& tsOrigin, CSndPacket& w_sndpkt) +{ CPacket& w_packet = w_sndpkt.pkt; #if SRT_ENABLE_BONDING @@ -10665,7 +11135,7 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt) << " is behind of EXTRACTION sequence " << current_sequence_number << ", dropping this packet: DIFF=" << packetspan << " STAMP=" << BufferStamp(w_packet.m_pcData, w_packet.getLength())); // XXX: Probably also change the socket state to broken? - return false; + return PACK_ERROR; } int32_t upd SRT_ATR_UNUSED = gk.group->updateSentSeq(m_iSndCurrSeqNo); @@ -10693,7 +11163,7 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt) { LOGC(qslog.Error, log << CONID() << "packUniqueData: encryption failed; data packet not sent"); w_sndpkt.release(); - return false; + return PACK_ERROR; } if (keyflag != EK_NOENC) updateCryptoOnSending(); @@ -10709,7 +11179,7 @@ bool CUDT::packUniqueData(CSndPacket& w_sndpkt) m_SndRegularMeasurement.dataUpdate(1, w_packet.getLength()); #endif - return true; + return PACK_OK; } // This is a close request, but called from the handler of the @@ -12672,9 +13142,9 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime) const bool is_laterexmit = m_CongCtl->rexmitMethod() == SrtCongestion::SRM_LATEREXMIT; // FileCC const bool is_fastrexmit = m_CongCtl->rexmitMethod() == SrtCongestion::SRM_FASTREXMIT; // LiveCC - // If the receiver will send periodic NAK reports, then FASTREXMIT (live) is inactive. + // FASTREXMIT (live) is inactive if the peer reports losses periodically or drops late packets. // TODO: Probably some method of "blind rexmit" MUST BE DONE, when TLPKTDROP is off. - if (is_fastrexmit && m_bPeerNakReport) + if (is_fastrexmit && (m_bPeerNakReport || m_bPeerTLPktDrop)) return; if ((!is_laterexmit || m_pSndBuffer->getLossLength() == 0)) @@ -12683,7 +13153,8 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime) if (getFlightSpan() > 0) { const int32_t csn = m_iSndCurrSeqNo; - const int num = m_pSndBuffer->insertLoss(m_iSndLastAck, csn, steady_clock::now()); + time_point first_send_time; + const int num = m_pSndBuffer->insertLoss(m_iSndLastAck, csn, steady_clock::now(), (first_send_time)); if (num > 0) { m_StatsLock.lock(); @@ -12694,6 +13165,11 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime) log << CONID() << "ENFORCED " << (is_laterexmit ? "LATEREXMIT" : "FASTREXMIT") << " by ACK-TMOUT (scheduling): " << CSeqNo::incseq(m_iSndLastAck) << "-" << csn << " (" << CSeqNo::seqoff(m_iSndLastAck, csn) << " packets)"); + + if (m_config.uSenderMode != 0) + { + scheduleRexmit(m_iSndLastAck, first_send_time); + } } } } @@ -12706,7 +13182,10 @@ void CUDT::checkBlindRexmitTimer(const steady_clock::time_point& currtime) const ECheckTimerStage stage = is_fastrexmit ? TEV_CHT_FASTREXMIT : TEV_CHT_REXMIT; updateCC(TEV_CHECKTIMER, EventVariant(stage)); - m_pMuxer->updateSendNormal(m_parent); + if (m_config.uSenderMode == 0) + { + m_pMuxer->updateSendNormal(m_parent); + } } void CUDT::checkTimers() @@ -12739,7 +13218,7 @@ void CUDT::checkTimers() checkBlindRexmitTimer(currtime); - if (currtime > m_tsLastSndTime.load() + microseconds_from(COMM_KEEPALIVE_PERIOD_US)) + if (currtime > m_LastSend.time() + microseconds_from(COMM_KEEPALIVE_PERIOD_US)) { sendCtrl(UMSG_KEEPALIVE); #if SRT_ENABLE_BONDING diff --git a/srtcore/core.h b/srtcore/core.h index 7f17d7006..1505c4fb4 100644 --- a/srtcore/core.h +++ b/srtcore/core.h @@ -69,6 +69,7 @@ modified by #include "handshake.h" #include "congctl.h" #include "packetfilter.h" +#include "schedule_snd.h" #include "socketconfig.h" #include "utilities.h" @@ -135,6 +136,124 @@ enum SeqPairItems class CCryptoControl; + +class CLastSend +{ + friend class CUDT; + sync::AtomicClock m_tsSendTime; + + // Statistical data, reset when sending ACKACK + sync::AtomicClock m_tsBeginTime; + sync::atomic m_uNumberPackets; + sync::atomic m_uNumberBytes; + uint32_t m_uBytesPerSecond; + +public: + + CLastSend(): m_uNumberPackets(0), m_uNumberBytes(0) + { + } + + sync::steady_clock::time_point time() const { return m_tsSendTime; } + + void collect(const sync::steady_clock::time_point& tm) + { + using namespace sync; + + steady_clock::time_point old = m_tsBeginTime; + m_tsBeginTime.store(m_tsSendTime.load()); + + int forespan = 0; + if (!is_zero(old)) + { + steady_clock::duration diff = m_tsBeginTime.load() - old; + uint64_t basesize = m_uNumberBytes * 1000 * 1000; + uint64_t basetime = count_microseconds(diff); + if (basetime) // prevent division by 0 + { + m_uBytesPerSecond = basesize / basetime; + forespan = 5; + } + } + else + { + m_uBytesPerSecond = 0; + } + m_tsSendTime = tm; + m_uNumberBytes = 4; + m_uNumberPackets = 1; + + steady_clock::duration diff = tm - m_tsBeginTime.load(); + uint64_t basesize = m_uNumberBytes * 1000 * 1000; + uint64_t basetime = count_microseconds(diff); + if (basetime) // prevent division by 0 + m_uBytesPerSecond = (forespan * m_uBytesPerSecond + (basesize / basetime))/(forespan + 1); + } + + void update(const sync::steady_clock::time_point& tm, uint32_t bytes, uint32_t npackets = 1) + { + m_tsSendTime = tm; + m_uNumberBytes += +bytes; + m_uNumberPackets += +npackets; + // Do not calculate speed here. Do it on reset only. + } +}; + +class CSendPipeManager +{ + friend class CUDT; + typedef sync::steady_clock::time_point time_point; + typedef sync::steady_clock::duration duration; + + //sync::Mutex m_Lock; + sync::atomic m_iSchedSeqNo; // SEQNO up to which regular packets were scheduled + int32_t m_iBufferedSeqNo; // SEQNO up to which there are packets in the sender buffer + + sync::AtomicClock m_tsTime; + + int m_tdAverageDeviation_us; + +public: + + static const int SCHEDULE_FORFEIT_LIMIT_MS = 500; + + CSendPipeManager(): + m_iSchedSeqNo(SRT_SEQNO_NONE), + m_tsTime() + { + } + + sync::steady_clock::time_point lastTime() const { return m_tsTime.load(); } + int32_t lastSchedSeq() const { return m_iSchedSeqNo; } + + bool lazyStart(int32_t seqno, const sync::steady_clock::time_point& tm) + { + if (m_iSchedSeqNo == SRT_SEQNO_NONE) + { + m_iSchedSeqNo = seqno; + m_tsTime = tm; + + return true; + } + + return false; + } + + // This should not happen, kinda sanity check + bool updateFirst(const sync::steady_clock::time_point& currtime) + { + if (is_zero(m_tsTime) || currtime - m_tsTime.load() > sync::seconds_from(1)) + { + m_tsTime = currtime; + return true; + } + return false; + } + + sync::steady_clock::time_point updateNext(const sync::steady_clock::time_point& currtime, + const sync::steady_clock::duration& send_interval); +}; + class CUDTUnited; class CUDTSocket; #if SRT_ENABLE_BONDING @@ -994,6 +1113,25 @@ class CUDT SRT_TSA_NEEDS_NONLOCKED(m_ConnectionLock) void checkSndTimers(); + // For schedule mode sending + sync::steady_clock::time_point lastSchedTime() const { return m_LastSched.lastTime(); } + //sync::steady_clock::duration lastSendInterval() const { return m_LastSched.lastInterval(); } + void scheduleRegular(int32_t seqno, const time_point& currtime, const time_point& exp_sendtime); + void scheduleRexmit(int32_t seqno, const time_point& exp_sendtime); + +public: // Used by scheduler + bool planSendingTime(sched::Type type, const CUDT::time_point& latest_delivery, CUDT::time_point& w_sendtime); + int extractPlannedLoss(SendTaskProto& proto); + bool hasSendCapacity() const + { + const int cwnd = std::min(m_iFlowWindowSize.load(), m_iCongestionWindow.load()); + const int32_t flight_span = getFlightSpan(m_iSndLastAck.load(), m_iSndCurrSeqNo.load()); + return cwnd > flight_span; + } + + bool isSendReady() const { return !m_CryptoControl.isSndDataGated(); } + +private: /// @brief Check and perform KM refresh if needed. bool checkSndKMRefresh(int* aw_keyindex); @@ -1180,7 +1318,7 @@ class CUDT SRT_TSA_GUARDED_BY(m_RecvAckLock) atomic_time_point m_tsLastRspTime; // Timestamp of last response from the peer time_point m_tsLastRspAckTime; // (SND) Timestamp of last ACK from the peer - atomic_time_point m_tsLastSndTime; // Timestamp of last data/ctrl sent (in system ticks) + CLastSend m_LastSend; // Time and stats for the last sending time_point m_tsLastWarningTime; // Last time that a warning message is sent atomic_time_point m_tsLastReqTime; // last time when a connection request is sent time_point m_tsRcvPeerStartTime; @@ -1192,7 +1330,8 @@ class CUDT int m_iPktCount; // Packet counter for ACK int m_iLightACKCount; // Light ACK counter - time_point m_tsNextSendTime; // Scheduled time of next packet sending + time_point m_tsNextSendTime; // Scheduled time of next packet sending (normal mode) + CSendPipeManager m_LastSched; // Data for packet scheduling (scheduler mode) sync::atomic m_iSndLastFullAck; // Last full ACK received SRT_TSA_GUARDED_BY(m_RecvAckLock) @@ -1428,6 +1567,14 @@ class CUDT /// @return payload size on success, <=0 on failure int packLostData(CSndPacket& packet); + /// Pack a specified packet for retransmission. In distinction to the + /// version with no number, just get the packet, if it's still eligible. + /// + /// @param seq [in] the already known sequence to extract + /// @param packet [in, out] a packet structure to fill + /// @return payload size on success, <=0 on failure + int packLostData(int32_t seqno, CSndPacket& packet); + duration minRexmitInterval() { if (m_bPeerNakReport && m_config.iRetransmitAlgo != 0) @@ -1444,8 +1591,19 @@ class CUDT /// Pack a unique data packet (never sent so far) in CPacket for sending. /// @param packet [in, out] a CPacket structure to fill. /// - /// @return true if a packet has been packets; false otherwise. - bool packUniqueData(CSndPacket& packet); + /// @return Whether packet packing succeeded and, if not, why it did not. + enum PackResult + { + PACK_OK, // Packet extracted and prepared for sending. + PACK_CONGESTED, // A new unscheduled packet is blocked by the flight window or initial key refresh. + PACK_UNAVAILABLE, // The socket or scheduled packet is no longer available. + PACK_ERROR // An invariant or packet-filter failure. + }; + + PackResult packUniqueData(CSndPacket& packet); + PackResult packUniqueData(int32_t seqno, CSndPacket& packet); + PackResult postExtractUniqueData(int current_sequence_number, const time_point& tsOrigin, CSndPacket& packet); + /// Pack in CPacket the next data to be send. If the call succeeds, the sequence number /// of the packet is reserved and guaranteed to stay in the buffer. This should be later @@ -1460,6 +1618,17 @@ class CUDT bool packData(CSndPacket& packet, time_point& nexttime, CNetworkInterface& src_addr); bool releaseSend(); void removeSndLossUpTo(int32_t seq); + PackResult packData(const SchedPacket& spec, CSndPacket& packet, CNetworkInterface& src_addr); + + // XXX + // previous: time when the previous packet was sent over THIS PIPE. + // latest: the latest possible time when the requested packert should be sent + // RETURNS: time when the now requested packet should be scheduled + // : if zero, this request should be cancelled + time_point getNextSendTime(const time_point& previous, const time_point& latest); + + bool defineSchedTimes(int32_t lo, int32_t hi, time_point& w_start, duration& w_step); + void scheduleRexmitRange(int32_t lo, int32_t hi); #if USE_RECEIVER_UNIT_POOL SRT_TSA_NEEDS_NONLOCKED(m_RcvTsbPdStartupLock, m_StatsLock, m_RecvLock, m_RcvLossLock, m_RcvBufferLock) diff --git a/srtcore/filelist.maf b/srtcore/filelist.maf index c0a9365c1..3fb67b6b4 100644 --- a/srtcore/filelist.maf +++ b/srtcore/filelist.maf @@ -22,6 +22,7 @@ packet.cpp packetfilter.cpp queue.cpp congctl.cpp +schedule_snd.cpp socketconfig.cpp srt_c_api.cpp strerror_defs.cpp @@ -76,6 +77,7 @@ packet.h sync.h queue.h congctl.h +schedule_snd.h socketconfig.h ../logging/hvu_compat.h stats.h diff --git a/srtcore/queue.cpp b/srtcore/queue.cpp index 9bcc5f5bc..1abd3de3d 100644 --- a/srtcore/queue.cpp +++ b/srtcore/queue.cpp @@ -605,6 +605,7 @@ void CSendOrderList::signalInterrupt() // CSndQueue::CSndQueue(CMultiplexer* parent): m_parent(parent), + m_pWorkerFunction(NULL), m_SendOrderList(parent->m_SocketsLock), m_pChannel(NULL), m_bClosing(false) @@ -621,8 +622,14 @@ void CSndQueue::stop() { // We use the decent way, so we say to the thread "please exit". m_bClosing = true; - - m_SendOrderList.signalInterrupt(); + if (m_pWorkerFunction == &workerSendOrder_fwd) + { + m_SendOrderList.signalInterrupt(); + } + else if (m_pWorkerFunction == &workerPacketScheduler_fwd) + { + m_Scheduler.interrupt(); + } // Sanity check of the function's affinity. if (sync::this_thread_is(m_WorkerThread)) @@ -657,12 +664,22 @@ void CSndQueue::init(CChannel* c) #else const char* thname = "SRT:SndQ"; #endif - if (!StartThread((m_WorkerThread), CSndQueue::worker_fwd, this, thname)) + m_pWorkerFunction = SelectWorkerFunction(); + + if (!StartThread((m_WorkerThread), *m_pWorkerFunction, this, thname)) { throw CUDTException(MJ_SYSTEMRES, MN_THREAD); } } +CSndQueue::worker_fn* CSndQueue::SelectWorkerFunction() +{ + if (m_parent->cfg().uSenderMode == 0) + return workerSendOrder_fwd; + + return workerPacketScheduler_fwd; +} + #if defined(SRT_DEBUG_SNDQ_HIGHRATE) static void CSndQueueDebugHighratePrint(const CSndQueue* self, const steady_clock::time_point currtime) @@ -815,6 +832,118 @@ void CSndQueue::workerSendOrder() THREAD_EXIT(); } +void CMultiplexer::scheduleSend(CUDTSocket* src, int32_t seqno, sched::Type type, + const sync::steady_clock::time_point& latest_send, + const sync::steady_clock::time_point& latest_delivery) +{ + if (type == sched::TP_REXMIT) + m_SndQueue.m_Scheduler.prescheduleLoss(src, seqno); + else + m_SndQueue.m_Scheduler.prescheduleRegular(SendTaskProto(SchedPacket(src, seqno, type), latest_send, latest_delivery)); +} + +void CMultiplexer::notifyScheduledSendReady() +{ + m_SndQueue.m_Scheduler.notifyPendingTasks(); +} + +void CSndQueue::workerPacketScheduler() +{ + std::string thname; + ThreadName::get(thname); + THREAD_STATE_INIT(thname.c_str()); + + int interrupt_credit = 5; + for (;;) + { + if (m_bClosing) + { + HLOGC(qslog.Debug, log << "SndQ: closed, exiting"); + break; + } + + HLOGC(qslog.Debug, log << "SndQ: waiting to get next send candidate..."); + THREAD_PAUSED(); + // + // API Scheduling adds directly to the queue only if it is free. + // Otherwise it's added to the "forequeue", with latest sending time. + // If the m_Scheduler contains no packets in the past, instead of + // waiting YET, you pick the earliest (sorted by expected send time) + // packet and enqueue it in the m_Scheduler. At this moment you select + // the sending time basing on the currently shaped sending interval. + + // SENDMODE: PLAIN (0): use default order scheduler. + // SENDMODE: EAGER (1): schedule packets ASAP, just preserve the timestamp + // SENDMODE: PLANNED (2): schedule packets at their declared send time + SendTaskProto task; + SchedPacket pspec = m_Scheduler.wait_pop(&task); + THREAD_RESUMED(); + INCREMENT_THREAD_ITERATIONS(); + + if (pspec.empty()) + { + --interrupt_credit; + if (!m_bClosing) + { + LOGC(qslog.Error, log << "SndQ: IPE: scheduler interrupted while queue is running (tolerate next " + << interrupt_credit << ")!"); + if (!interrupt_credit) + { + m_bClosing = true; + LOGC(qslog.Fatal, log << "SndQ: IPE: closing sender queue to prevent spamming"); + break; + } + } + continue; // If m_bClosing, the loop will exit at the next iteration + } + + CSndPacket sndpkt; + CNetworkInterface source_addr; + CUDTSocket* s = pspec.m_Socket.socket; + const CUDT::PackResult result = s->core().packData(pspec, (sndpkt), (source_addr)); + if (result == CUDT::PACK_UNAVAILABLE) + continue; + if (result != CUDT::PACK_OK) + { + LOGC(qslog.Error, log << "SndQ: IPE: @" << s->id() + << " couldn't pack packet for scheduled specification (result=" << result << ")"); + --interrupt_credit; + if (!interrupt_credit) + { + // XXX Signal broken socket by IPE + m_bClosing = true; + } + continue; + } + const sockaddr_any target_addr = s->core().m_PeerAddr; + m_pChannel->sendto(target_addr, sndpkt.pkt, source_addr); + + steady_clock::time_point last_send_time = steady_clock::now(); + s->core().m_LastSend.update(last_send_time, sndpkt.pkt.getLength()); + + // Restore to maximum after a successful extraction. + interrupt_credit = 5; + + /* XXX There's no chained scheduling anymore + Any next sending will be based on the direct submission. + + steady_clock::time_point when = s->core().calculateRegularSchedTime(); + + // Schedule the next packet, if possible. + // If not possible, the next packet will be chained directly + // by the API function. + CLastSched& sc = s->core().m_LastSched; + if (sc.chainSchedule(sndpkt.pkt.getSeqNo(), when)) + { + SendTask task (SchedPacket(s, sc.lastSchedSeq() , sched::TP_REGULAR), when); + m_Scheduler.enqueue_task(s->id(), task); + } + */ + } + THREAD_EXIT(); +} + + void CMultiplexer::removeRID(const SRTSOCKET& id) { ScopedLock lkv(m_SocketsLock); diff --git a/srtcore/queue.h b/srtcore/queue.h index 8083c9ed5..1c537fe28 100644 --- a/srtcore/queue.h +++ b/srtcore/queue.h @@ -60,6 +60,7 @@ modified by #include "common.h" #include "packet.h" #include "socketconfig.h" +#include "schedule_snd.h" #include "netinet_any.h" #include "utilities.h" @@ -664,6 +665,7 @@ struct CMultiplexer; class CSndQueue { + friend class TestMockCUDT; friend class CUDT; friend class CUDTUnited; friend struct CMultiplexer; @@ -691,15 +693,20 @@ class CSndQueue void stop(); private: - static void* worker_fwd(void* param) - { - CSndQueue* self = (CSndQueue*)param; - self->workerSendOrder(); + typedef void* worker_fn(void*); +#define DEFINE_FWD(classname, name) \ + static void* name##_fwd(void* param) { classname* self = (classname*)param; self->name(); return NULL; } - return NULL; - } void workerSendOrder(); + DEFINE_FWD(CSndQueue, workerSendOrder); + void workerPacketScheduler(); + DEFINE_FWD(CSndQueue, workerPacketScheduler); + +#undef DEFINE_FWD + + worker_fn* m_pWorkerFunction; + sync::CThread m_WorkerThread; CSendOrderList m_SendOrderList; // List of socket instances for data sending @@ -707,6 +714,10 @@ class CSndQueue sync::atomic m_bClosing; // closing the worker + SendScheduler m_Scheduler; + + worker_fn* SelectWorkerFunction(); + public: bool stopped() const { return !m_WorkerThread.joinable(); } @@ -899,6 +910,8 @@ struct CMultiplexer // Dissolution might be a better idea, but this is better for separation. friend class CSndQueue; + friend class TestMockCUDT; + int m_iID; // multiplexer ID mutable sync::Mutex m_SocketsLock; @@ -1090,6 +1103,14 @@ struct CMultiplexer void updateSendFast(CUDTSocket* s); void removeSender(CUDT* u); + // SCHEDULER API + + void scheduleSend(CUDTSocket* src, int32_t seqno, sched::Type type, + const sync::steady_clock::time_point& when, + const sync::steady_clock::time_point& delivery); + // Recheck pending packets after a flight-window or key-readiness change. + void notifyScheduledSendReady(); + }; } // namespace srt diff --git a/srtcore/schedule_snd.cpp b/srtcore/schedule_snd.cpp new file mode 100644 index 000000000..9cecc0563 --- /dev/null +++ b/srtcore/schedule_snd.cpp @@ -0,0 +1,571 @@ + +#include +#include + +#include "schedule_snd.h" +#include "api.h" +#include "core.h" +#include "logging.h" +#include "hvu_compat.h" +#include "ofmt_iostream.h" + +using namespace std; +using namespace srt; +using namespace srt::sync; + +using srt::logging::qslog; +using namespace hvu; + +namespace srt +{ + +SchedPacketInfo::SchedPacketInfo(CUDTSocket* sock, int32_t seqno): + m_Socket(CUDT::uglobal()) +{ + if (sock) + m_Socket = CUDT::keep(sock); + + m_iSeqNo = seqno; +} + +void SchedPacketInfo::set_socket(CUDTSocket* sock) +{ + if (sock) + m_Socket = CUDT::keep(sock); +} + +static const char* const schedtype [] = {"regular", "rexmit", "pf-control"}; + +std::string SendTask::print(SendTask::taskiter_t v) +{ + using namespace hvu; + + ofmt_bufs out; + // Complicated pre-C++20 time formatting... + time_t tval = count_seconds(v->m_tsSendTime.time_since_epoch()); + struct tm tme = sys_localtime(tval); + + int64_t total_usec = count_microseconds(v->m_tsSendTime.time_since_epoch()); + int64_t usec = total_usec - (tval * 1000000); + out << "<" << fmt(tme, "%T") << "." << fmt(usec, fmtc().width(6).fillzero()) + << "> @" << v->m_Packet.m_Socket.id(); + int32_t seq = v->m_Packet.seqno(); + if (seq == SRT_SEQNO_NONE) + { + out << " [empty]"; + } + else + { + out << " [" << schedtype[v->m_Packet.type()] << "] %" << seq; + } + return out.str(); +} + +SendTask::taskrep_t SendScheduler::createTask(const SendTaskProto& sp) +{ + SRTSOCKET id = sp.m_Packet.m_Socket.socket->id(); + SendTask::tasklist_t& wlist = m_TaskMap[id]; + + wlist.push_back(SendTask(sp)); + SendTask::taskiter_t itask = --wlist.end(); + itask->m_pBaseList = &wlist; + + return SendTask::taskrep_t(wlist, itask); +} + +void SendScheduler::prescheduleLoss(CUDTSocket* provider, int32_t first_seqno) +{ + SRT_ASSERT(provider); + if (!provider) + { + LOGC(qslog.Error, log << "Schedule: PRE-ENQ: cannot schedule loss without a socket"); + return; + } + + if (m_bBroken) + { + HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: DENIED, schedule is broken"); + return; + } + + SRTSOCKET id = provider->id(); + sync::ScopedLock lk (m_Lock); + + SchedPacketInfo* psi; + bool isnew; + Tie(psi, isnew) = map_tryinsertp(m_PendingRexmit, id); + + if (!isnew) + { + HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: @" << id << " LOSS first=%" << first_seqno + << ": ALREADY SCHEDULED, skipping"); + return; + } + + IF_HEAVY_LOGGING(const char* notif = ""); + // Insert a new item + psi->set_socket(provider); + SRT_ASSERT(psi->m_Socket.socket); + if (!psi->m_Socket.socket) + { + LOGC(qslog.Error, log << "Schedule: PRE-ENQ: failed to keep socket @" << id << " for loss scheduling"); + m_PendingRexmit.erase(id); + return; + } + psi->m_iSeqNo = first_seqno; + + if (!have_task_ready()) + { + m_TaskReadyCond.notify_all(); + IF_HEAVY_LOGGING(notif = " [NOTIFIED]"); + } + + HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: @" << id << " LOSS first=%" << first_seqno + << ": newly scheduled " << notif); +} + +void SendScheduler::prescheduleRegular(const SendTaskProto& sp) +{ + SRT_ASSERT(sp.m_Packet.m_Socket.socket); + if (!sp.m_Packet.m_Socket.socket) + { + LOGC(qslog.Error, log << "Schedule: PRE-ENQ: cannot schedule regular packet without a socket"); + return; + } + + if (m_bBroken) + { + HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: DENIED, schedule is broken"); + return; + } + + + sync::ScopedLock lk (m_Lock); + SendTask::taskrep_t rtask = createTask(sp); + + m_PendingRegularQueue.push_back(rtask); + + IF_HEAVY_LOGGING(sched::Type type = sp.m_Packet.m_Type); + IF_HEAVY_LOGGING(SRTSOCKET id = sp.m_Packet.m_Socket.socket->id()); + IF_HEAVY_LOGGING(const char* notif = ""); + IF_HEAVY_LOGGING(static const char* const type_name[3] = {"REGULAR", "REXMIT", "CONTROL"}); + if (!have_task_ready()) + { + m_TaskReadyCond.notify_all(); + IF_HEAVY_LOGGING(notif = " [NOTIFIED]"); + } + + HLOGC(qslog.Debug, log << "Schedule: PRE-ENQ: @" << id << " %" << sp.m_Packet.m_iSeqNo << " type=" << type_name[type] + << "LATEST: SEND=" << FormatTime(sp.m_tsSendTime) << " DELIVERY=" << FormatTime(sp.m_tsLatestDeliveryTime) << notif); +} + +// NOTE: This is a function that enqueues a task in the queue directly; +// that should be rather not used because you need to know the exact execution time. + /* +SendTask::taskiter_t SendScheduler::enqueue_task(socket_t id, const SchedPacket& proto, const clock_time& when, const clock_time& delivery) +{ + if (m_bBroken) + { + HLOGC(qslog.Debug, log << "Schedule: ENQ: DENIED, schedule is broken"); + return SendTask::none(); + } + + sync::ScopedLock lk (m_Lock); + SendTask::taskiter_t itask = create_task(proto, when, delivery); + + bool was_ready = have_task_ready(); + + size_t pos = m_TaskQueue.insert(itask); + + IF_HEAVY_LOGGING(bool was_first = false); + IF_HEAVY_LOGGING(bool now_ready = false); + if (pos == 0) // earliest task + { + m_tsAboutTime = m_TaskQueue.top()->m_tsSendTime; // INSERTED: will not be empty + IF_HEAVY_LOGGING(was_first = true); + } + + // XXX Shouldn't it update always if m_tsAboutTime was updated? + if (!was_ready && have_task_ready()) + { + m_TaskReadyCond.notify_all(); + IF_HEAVY_LOGGING(now_ready = true); + } + + HLOGC(qslog.Debug, log << "Schedule: ENQ: new" + << fmt_if(now_ready, " READY") + << " task at T=" << FormatTime(itask->m_tsSendTime) + << fmt_if(was_first, " (NEW TOP)") + << fmt_if(was_ready, " (NOW READY)", " (ONLY ADDED)") + << fmt_if(now_ready, " - NOTIFY")); + + return itask; +} +// */ + +bool SendScheduler::updatePreschedule(size_t max_sched) +{ + // The next call to wait() will return an already ready task, + // so do not reschedule new ones until they are all dispatched. + if (have_task_ready()) + { + HLOGC(qslog.Debug, log << "updatePreschedule: POSTPONED, have ready tasks"); + return true; + } + + if (m_PendingRegularQueue.empty() && m_PendingRexmit.empty()) + { + HLOGC(qslog.Debug, log << "updatePreschedule: NO PENDING tasks"); + return false; + } + + steady_clock::time_point now = steady_clock::now(); + + // Prioritization: + // 1. Check if there is any regular packet already overdue. If so, + // it takes precedence before any retransmission packet. + size_t nsched = 0; + std::map blocked; + for (pending_t::iterator it = m_PendingRegularQueue.begin(); it != m_PendingRegularQueue.end();) + { + SendTask::taskrep_t pt = *it; + if (pt->m_tsSendTime > now) + break; // stop at first being in the future + + const socket_t id = pt->m_Packet.id(); + if (blocked.find(id) != blocked.end()) + { + ++it; + continue; + } + CUDT& core = pt->m_Packet.m_Socket.socket->core(); + if (!core.isSendReady() || !core.hasSendCapacity()) + { + blocked[id] = true; + ++it; + continue; + } + + it = m_PendingRegularQueue.erase(it); + + SchedPacket& proto = pt->m_Packet; + proto.m_Socket.socket->core().planSendingTime(proto.m_Type, pt->m_tsLatestDeliveryTime, (pt->m_tsSendTime)); + size_t pos = m_TaskQueue.insert(pt); + + if (pos == 0) // earliest task + { + // INSERTED: will not be empty + m_tsAboutTime = m_TaskQueue.top_raw()->m_tsSendTime; + // NOTE: theoretically you should notify() here, but it's not necessary + // because this function has the same affinity as the wait() caller. + } + + ++nsched; + if (nsched >= max_sched) + { + HLOGC(qslog.Debug, log << "updatePreschedule: enqueued " << max_sched << " REGULAR tasks, " + << fmt_if(pos == 0, " NEW HEAD,") << " still pending: " + << m_PendingRegularQueue.size() << " REG + " << m_PendingRexmit.size() << " REXMIT"); + return have_task_ready(); + } + } + + // 2. Review the retransmission packets; insert those that still have + // remaining delivery time. Drop the others. + + IF_HEAVY_LOGGING(int regsched = nsched); + IF_HEAVY_LOGGING(bool new_head = false); + std::vector to_delete; + + // Ok, the rexmit queue is different. + // We have normally information about ONE loss, which at the moment of + // pickup need not hold. We then run the loop by index, with notifying up + // to which element we have passed the task to the task queue, and those + // will be then removed. + + // Walk through all entries. Later delete those that don't report any more lost packets + for (map::const_iterator i = m_PendingRexmit.begin(); i != m_PendingRexmit.end(); ++i) + { + SendTaskProto proto; + + for (;;) + { + // This should check what losses are expected to be sent, which losses + // are too old and should be deleted, and whether anything is still about to be sent. + int loss_chain = i->second.m_Socket.socket->core().extractPlannedLoss((proto)); + // RETURNS: + // 0: No loss scheduled at the moment for that iteration (note: should not happen IN ITERATION) + // 1: One loss was detected, but then there is none. + // 2: The loss was extracted and there's a new one waiting. + if (loss_chain) + { + if (!proto.m_Packet.empty()) + { + SRT_ASSERT(proto.m_Packet.m_Socket.socket); + if (!proto.m_Packet.m_Socket.socket) + { + LOGC(qslog.Error, log << "Schedule: PRE-ENQ: loss extraction returned a packet without a kept socket"); + to_delete.push_back(i->first); + break; + } + + ++nsched; + // We have something - schedule it. + SendTask::taskrep_t itask = createTask(proto); + + size_t pos = m_TaskQueue.insert(itask); + if (pos == 0) // earliest task + { + m_tsAboutTime = m_TaskQueue.top_raw()->m_tsSendTime; + IF_HEAVY_LOGGING(new_head = true); + } + } + } + + if (loss_chain != 2) + { + to_delete.push_back(i->first); + break; // forget about this one; check the next socket + } + + if (nsched >= max_sched) + { + HLOGC(qslog.Debug, log << "updatePreschedule: enqueued " << max_sched << " = " << regsched + << " REG + " << (nsched - regsched) << " REXMIT tasks," + << fmt_if(new_head, " NEW HEAD,") << " still pending: " + << m_PendingRegularQueue.size() << " REG + " << m_PendingRexmit.size() << " sockets in REXMIT"); + return have_task_ready(); + } + } + + if (nsched >= max_sched) + { + return have_task_ready(); + } + } + + // Delete entries for sockets that do not report any rexmit + for (size_t i = 0; i < to_delete.size(); ++i) + m_PendingRexmit.erase(to_delete[i]); + + IF_HEAVY_LOGGING(int rexsched = nsched); + // 3. Try to schedule regular packets, if there are still free slots + for (pending_t::iterator it = m_PendingRegularQueue.begin(); it != m_PendingRegularQueue.end();) + { + SendTask::taskrep_t pt = *it; + const socket_t id = pt->m_Packet.id(); + if (blocked.find(id) != blocked.end()) + { + ++it; + continue; + } + CUDT& core = pt->m_Packet.m_Socket.socket->core(); + if (!core.isSendReady() || !core.hasSendCapacity()) + { + blocked[id] = true; + ++it; + continue; + } + + it = m_PendingRegularQueue.erase(it); + SchedPacket& proto = pt->m_Packet; + proto.m_Socket.socket->core().planSendingTime(proto.m_Type, pt->m_tsLatestDeliveryTime, (pt->m_tsSendTime)); + size_t pos = m_TaskQueue.insert(pt); + + if (pos == 0) // earliest task + { + // INSERTED: will not be empty + m_tsAboutTime = m_TaskQueue.top_raw()->m_tsSendTime; + // NOTE: theoretically you should notify() here, but it's not necessary + // because this function has the same affinity as the wait() caller. + IF_HEAVY_LOGGING(new_head = true); + } + + ++nsched; + if (nsched >= max_sched) + { + break; + } + } + + HLOGC(qslog.Debug, log << "updatePreschedule: enqueued " << nsched << " = " + << regsched << "YD + " << (rexsched - regsched) << "RX + " + << (nsched - rexsched) << "RG tasks," + << fmt_if(new_head, " NEW HEAD,") << " still pending: " + << m_PendingRegularQueue.size() << " REG + " << m_PendingRexmit.size() << " sockets in REXMIT"); + return have_task_ready(); +} + +bool SendScheduler::have_task_ready() +{ + // XXX Make sure it works. The m_tsAboutTime is being updated + // with every update in m_Scheduler, so it should keep the top + // element's time, or be 0 if there's no top element. + + return !sync::is_zero(m_tsAboutTime) && m_tsAboutTime <= clock_type::now(); +#if 0 + + if (!m_TaskQueue.empty()) + { + SendTask::taskiter_t earliest = m_TaskQueue.top(); + if (earliest->is_ready(clock_type::now())) + { + return true; + } + } + return false; +#endif +} + +void SendScheduler::withdraw(socket_t id) +{ + sync::ScopedLock lk (m_Lock); + // Delete all tasks for the given socket id. + // We have them collected in the list: m_TaskMap + + SendTask::tasklist_t& id_list = m_TaskMap[id]; + + // As we know that all these items were added to m_TaskQueue, + // we need to withdraw them all from m_TaskQueue. + + IF_HEAVY_LOGGING(int nerased = 0); + for (SendTask::taskiter_t idt = id_list.begin(); idt != id_list.end(); ++idt) + { + if (m_TaskQueue.erase(SendTask::taskrep_t(id_list, idt))) + { + IF_HEAVY_LOGGING(++nerased); + } + } + // The list should be empty, so delete the entry itself. + int iderased SRT_ATR_UNUSED = m_TaskMap.erase(id); + // We don't know if the earliest in the queue was deleted, + // so just rewrite it anyway. + pop_update_time(); + +#if HVU_ENABLE_HEAVY_LOGGING + hvu::ofmt_bufs nextone; + if (m_TaskQueue.empty()) + nextone << "NO NEXT TASK"; + else + nextone << "next in " << FormatDurationAuto(m_tsAboutTime - steady_clock::now()) + << " from @" << m_TaskQueue.top()->m_Packet.id(); +#endif + HLOGC(qslog.Debug, log << "Schedule: withdrawn @" << int(id) + << (iderased ? "" : " (NOT FOUND!)") << " - erased " << nerased << " tasks -" << nextone); +} + +void SendScheduler::pop_update_time() +{ + if (!m_TaskQueue.empty()) + m_tsAboutTime = m_TaskQueue.top()->m_tsSendTime; // checked that ! empty + else + m_tsAboutTime = clock_time(); +} + +void SendScheduler::cancel(SendTask::taskrep_t itask) +{ + sync::ScopedLock lk (m_Lock); + cancel_nolock(itask); +} + +void SendScheduler::interrupt() +{ + m_bBroken = true; + HLOGC(qslog.Debug, log << "Schedule: INTERRUPT: locking..."); + sync::ScopedLock hold (m_Lock); + HLOGC(qslog.Debug, log << "Schedule: INTERRUPT: notifying waiters"); + + m_TaskReadyCond.notify_all(); // Force waiting functions to exit +} + +void SendScheduler::notifyPendingTasks() +{ + sync::ScopedLock hold (m_Lock); + m_TaskReadyCond.notify_all(); +} + + +void SendScheduler::cancel_nolock(SendTask::taskrep_t itask) +{ + HLOGC(qslog.Debug, log << "Schedule: CANCEL: @" << itask->m_Packet.id() << " T=" << FormatTime(itask->m_tsSendTime)); + m_TaskQueue.erase(itask); + itask->m_pBaseList->erase(itask.get()); + pop_update_time(); +} + +SchedPacket SendScheduler::wait_pop(SendTaskProto* task) +{ + SchedPacket packet; + sync::UniqueLock lk (m_Lock); + // Wait until the time has come to execute + // the next task. Extract the task structure + // and remove the task from the list. + + for (;;) + { + if (m_bBroken) + { + HLOGC(qslog.Debug, log << "Schedule: wait_pop: broken"); + return SchedPacket(); + } + + // This will move pending tasks if there's no ready task yet. + // Returns true if there's at least one task ready for pickup + if (updatePreschedule(4 /*XXX USE CONSTANT*/)) + { + HLOGC(qslog.Debug, log << "Schedule: WAIT: task ready since " << FormatDurationAuto(steady_clock::now() - m_tsAboutTime)); + break; + } + + if (m_TaskQueue.empty()) // This means that also m_ForeQueue was empty + { + HLOGC(qslog.Debug, log << "Schedule: WAIT: task NOT ready, NO NEW TASKS, WAIT FOR SIGNAL)"); + // This will be signaled if anything is added to m_ForeQueue or m_TaskQueue. + m_TaskReadyCond.wait(lk); + } + else + { + clock_time now = steady_clock::now(); + HLOGC(qslog.Debug, log << "Schedule: WAIT: task NOT ready, next in " << FormatDurationAuto(m_tsAboutTime - now)); + + duration remain = m_tsAboutTime - now; + if (remain > duration()) // m_tsAboutTime is in the future + { + // Limit long waits to 3/4 of the remaining time so pending work + // and capacity notifications can wake the scheduler promptly. + if (remain > sync::milliseconds_from(10)) + m_TaskReadyCond.wait_for(lk, remain * 3 / 4); + else + m_TaskReadyCond.wait_for(lk, milliseconds_from(1)); + } + } + // CONTINUE. + } + // Here we are sure that the top() task is ready to execute + SendTask::taskiter_t itask = m_TaskQueue.pop(); + pop_update_time(); + + if (itask == SendTask::none()) + { + HLOGC(qslog.Debug, log << "Schedule: wait_pop: IPE: THE QUEUE IS EMPTY"); + return SchedPacket(); + } + // The node is already removed from the heapset. + + // Extract the required data + packet = itask->m_Packet; + if (task) + *task = SendTaskProto(itask->m_Packet, itask->m_tsSendTime, itask->m_tsLatestDeliveryTime); + + // Now remove it from the corresponding list. + itask->m_pBaseList->erase(itask); + + IF_HEAVY_LOGGING(static string typenames[3] = {"REGULAR", "REXMIT", "CONTROL"}); + HLOGC(qslog.Debug, log << "Schedule: wait_pop: PICKUP from @" << packet.id() + << " %" << packet.seqno() + << " type=" << typenames[packet.type()]); + + return packet; +} + +} diff --git a/srtcore/schedule_snd.h b/srtcore/schedule_snd.h new file mode 100644 index 000000000..a7dc1cc09 --- /dev/null +++ b/srtcore/schedule_snd.h @@ -0,0 +1,279 @@ + +#ifndef INC_SRT_SCHEDULE_SND_H +#define INC_SRT_SCHEDULE_SND_H + +#include +#include +#include "sync.h" +#include "atomic.h" +#include "common.h" // SocketKeeper +#include "utilities.h" +#include "buffer_snd.h" + +namespace srt +{ + +namespace sched +{ + enum Type + { + TP_REGULAR = 0, + TP_REXMIT = 1, + TP_CONTROL = 2 + }; +} + +// This structure contains the information about the +// socket, packet contents and sequence number of +// the packet that is about to be sent. + +// Note: scheduling should happen at the exact place +// where the scheduling event should appear: +// - When calling srt_send: schedule regular packet +// - - If packetfilter control packet is ready AFTER that, schedule that, too. +// - When dispatching LOSSREPORT: schedule rexmit packet +// - When NAKREPORT timer expired: schedule rexmit packet + +struct SchedPacketInfo +{ + SocketKeeper m_Socket; + int32_t m_iSeqNo; + + void set_socket(CUDTSocket* sock); + + SRTSOCKET id() const { return m_Socket.id(); } + + bool empty() const { return m_iSeqNo == SRT_SEQNO_NONE; } + + int32_t seqno() const { return m_iSeqNo; } + + SchedPacketInfo(CUDTSocket* sock = NULL, int32_t seqno = SRT_SEQNO_NONE); +}; + +struct SchedPacket: SchedPacketInfo +{ + sched::Type m_Type; + + // NOTE: Both constructor and set_socket() call will need to + // perform the official acquisition of the socket, which requires + // locking CUDTUnited::m_GlobControlLock. Further copying of the + // SocketKeeper object doesn't require any locking. + SchedPacket(CUDTSocket* sock = NULL, int32_t seqno = SRT_SEQNO_NONE, sched::Type t = sched::TP_REGULAR): + SchedPacketInfo(sock, seqno), + m_Type(t) + { + } + sched::Type type() const { return m_Type; } +}; + +struct SendTaskProto +{ + typedef sync::steady_clock::time_point ClockTime; + // DOUBLE INTERPRETATION: + // - for pending queue, it's the latest scheduling time = mctrl.srctime + // - for schedule queue, it's the time to pickup + ClockTime m_tsSendTime; + ClockTime m_tsLatestDeliveryTime; + SchedPacket m_Packet; + + SendTaskProto() + : m_tsSendTime(), m_Packet() {} + + SendTaskProto(const SchedPacket& sp, sync::steady_clock::time_point when, sync::steady_clock::time_point delivery) + : m_tsSendTime(when), m_tsLatestDeliveryTime(delivery), m_Packet(sp) {} +}; + +struct SendTask: SendTaskProto +{ + // NODE FIELDS for HeapSet + typedef std::list tasklist_t; + typedef typename tasklist_t::iterator taskiter_t; + typedef MaybeIterator taskrep_t; + typedef sync::steady_clock::time_point key_type; + + static taskrep_t rep(tasklist_t& ls, taskiter_t i) { return taskrep_t(ls, i); } + key_type key() const { return m_tsSendTime; } + sync::atomic m_zHeapPos; // Required by HeapSet + + // "Payload" fields. + + // = mctrl.srctime + optimisticRTT() + latency + // This is the latest time when the packet is expected to be + // given up to the application - at latest, if in EAGER mode. + // REXMIT packets that are past this time already should be dropped. + key_type m_tsLatestDeliveryTime; + SchedPacket m_Packet; + // XXX This pointer is duplicated in taskrep_t + // find a way to unify them + std::list* m_pBaseList; + + // Same definition as by HeapSet; here a shortcut. + // Can't use the definition from HeapSet because it's + // a template that has requirements for the type parameter. + static const size_t npos = std::string::npos; + + SendTask() + : SendTaskProto(), m_zHeapPos(npos), m_pBaseList(0) {} + + SendTask(const SendTaskProto& proto) + : SendTaskProto(proto), m_zHeapPos(npos), m_tsLatestDeliveryTime(proto.m_tsLatestDeliveryTime), + m_Packet(proto.m_Packet), m_pBaseList(0) {} + + // Note: Copying a task is only allowed because of the need + // to move from one container to another. A single task that is + // pinned to a sender buffer may however exist only in one instance. + SendTask(const SendTask& src): + SendTaskProto(src.m_Packet, src.m_tsSendTime, src.m_tsLatestDeliveryTime), + // m_zHeapPos(src.m_zHeapPos.load()), + m_zHeapPos(npos), // Copied element is NEVER in ths container! + m_tsLatestDeliveryTime(src.m_tsLatestDeliveryTime), + m_Packet(src.m_Packet), + m_pBaseList(src.m_pBaseList) + {} + + // convenience + void set_time(sync::steady_clock::time_point tp) { m_tsSendTime = tp; } + + bool is_ready(key_type basetime) const + { + return m_tsSendTime < basetime; + } + + SendTask& operator=(const SendTask& src) + { + m_tsSendTime = src.m_tsSendTime; + m_tsLatestDeliveryTime = src.m_tsLatestDeliveryTime; + m_Packet = src.m_Packet; + m_zHeapPos = src.m_zHeapPos.load(); + return *this; + } + + static sync::atomic& position(taskrep_t v) { return v->m_zHeapPos; } + static key_type& key(taskrep_t v) { return v->m_tsSendTime; } + static bool order(const key_type& left, const key_type& right) + { + return left < right; + } + + static taskrep_t none() { return taskrep_t(); } + + static std::string print(taskiter_t v); +}; + +struct SendScheduler +{ + typedef SRTSOCKET socket_t; + typedef sync::steady_clock clock_type; + typedef clock_type::time_point clock_time; + typedef clock_type::duration duration; + +protected: + std::map m_TaskMap; + HeapSet m_TaskQueue; + typedef std::deque pending_t; + pending_t m_PendingRegularQueue; + + // We use map in order to keep always only one entry per socket. + std::map m_PendingRexmit; + clock_time m_tsAboutTime; + + sync::Mutex m_Lock; + sync::Condition m_TaskReadyCond; + sync::atomic m_bBroken; + + SRT_TSA_NEEDS_LOCKED(m_Lock) + void pop_update_time(); + + // Helper functions to operate with the pending queues + // You better check if q.empty() before the call! + SendTask::taskrep_t pullTask(pending_t& q) + { + SendTask::taskrep_t i = *q.begin(); + q.pop_front(); + return i; + } + + void deletePending(pending_t& q) + { + for (pending_t::iterator i = q.begin(); i != q.end(); ++i) + { + SendTask::taskiter_t pt = *i; + pt->m_pBaseList->erase(pt); + // NOTE: All elements become dangling iterators here! + } + q.clear(); + } + +public: + const HeapSet& queue() { return m_TaskQueue; } + + SendScheduler(): m_bBroken(false) + { + sync::setupCond(m_TaskReadyCond, "SendScheduler"); + } + + void interrupt(); + void notifyPendingTasks(); + + bool running() + { + return !m_bBroken; + } + + // SendTask::taskiter_t enqueue_task(socket_t id, const SendTask& proto); + void prescheduleRegular(const SendTaskProto& sp); + void prescheduleLoss(CUDTSocket* provider, int32_t first_seqno); + + SendTask::taskrep_t createTask(const SendTaskProto& sp); + void updateTask(SendTask::taskiter_t ti); + +protected: + // This is NOLOCK; derived classes please use lock. + SRT_TSA_NEEDS_LOCKED(m_Lock) + bool have_task_ready(); + + SRT_TSA_NEEDS_LOCKED(m_Lock) + bool updatePreschedule(size_t max_sched); + +public: + + // Wait until the time has come + bool wait_extlock(srt::sync::UniqueLock&); + + void withdraw(socket_t id); + + template + void withdraw_if(socket_t id, Predicate match) + { + sync::ScopedLock lk (m_Lock); + // Delete all tasks for the given socket id. + // We have them collected in the list: m_TaskMap + + SendTask::tasklist_t& id_list = m_TaskMap[id]; + + // As we know that all these items were added to m_TaskQueue, + // we need to withdraw them all from m_TaskQueue. + SendTask::taskiter_t idt_next = id_list.begin(); + for (SendTask::taskiter_t idt = idt_next; idt != id_list.end(); idt = idt_next) + { + ++idt_next; + if (match(idt)) + { + cancel_nolock(SendTask::rep(id_list, idt)); + } + } + } + +protected: + void cancel_nolock(SendTask::taskrep_t itask); + +public: + void cancel(SendTask::taskrep_t itask); + + SchedPacket wait_pop(SendTaskProto* task = NULL); + +}; + +} + +#endif diff --git a/srtcore/socketconfig.cpp b/srtcore/socketconfig.cpp index e25908eef..48ef13f07 100644 --- a/srtcore/socketconfig.cpp +++ b/srtcore/socketconfig.cpp @@ -1009,6 +1009,23 @@ struct CSrtConfigSetter }; #endif +template<> +struct CSrtConfigSetter +{ + static void set(CSrtConfig& co, const void* optval, int optlen) + { + const int val = cast_optval(optval, optlen); + + if (val < 0 || val > 2) + { + LOGC(aclog.Error, log << "OPTION: sendmode: allowed 0=live 1=stow 2=planned"); + throw CUDTException(MJ_NOTSUP, MN_INVAL, 0); + } + + co.uSenderMode = val; + } +}; + int dispatchSet(SRT_SOCKOPT optName, CSrtConfig& co, const void* optval, int optlen) { switch (optName) @@ -1069,6 +1086,7 @@ int dispatchSet(SRT_SOCKOPT optName, CSrtConfig& co, const void* optval, int opt #ifdef SRT_ENABLE_MAXREXMITBW DISPATCH(SRTO_MAXREXMITBW); #endif + DISPATCH(SRTO_SENDMODE); #undef DISPATCH default: diff --git a/srtcore/socketconfig.h b/srtcore/socketconfig.h index 06e7cf10d..239a2c412 100644 --- a/srtcore/socketconfig.h +++ b/srtcore/socketconfig.h @@ -87,6 +87,7 @@ struct CSrtMuxerConfig int iIpToS; int iIpV6Only; // IPV6_V6ONLY option (-1 if not set) bool bReuseAddr; // reuse an exiting port or not, for UDP multiplexer + uint32_t uSenderMode; #ifdef SRT_ENABLE_BINDTODEVICE std::string sBindToDevice; @@ -102,6 +103,7 @@ struct CSrtMuxerConfig return CEQUAL(iIpTTL) && CEQUAL(iIpToS) && CEQUAL(bReuseAddr) + && CEQUAL(uSenderMode) #ifdef SRT_ENABLE_BINDTODEVICE && CEQUAL(sBindToDevice) #endif @@ -122,6 +124,7 @@ struct CSrtMuxerConfig , iIpToS(-1) /* IPv4 Type of Service or IPv6 Traffic Class [0x00..0xff] (-1:undefined) */ , iIpV6Only(-1) , bReuseAddr(true) // This is default in SRT + , uSenderMode(0) , iUDPSndBufSize(DEF_UDP_BUFFER_SIZE) , iUDPRcvBufSize(DEF_UDP_BUFFER_SIZE) { diff --git a/srtcore/srt.h b/srtcore/srt.h index 6cd347f17..0598d9946 100644 --- a/srtcore/srt.h +++ b/srtcore/srt.h @@ -262,6 +262,7 @@ typedef enum SRT_SOCKOPT { SRTO_RETRANSMITALGO = 61, // An option to select packet retransmission algorithm SRTO_CRYPTOMODE = 62, // Encryption cipher mode (AES-CTR, AES-GCM, ...). SRTO_MAXREXMITBW = 63, // Maximum bandwidth limit for retransmision (Bytes/s) + SRTO_SENDMODE = 64, // Use of the sending mode SRTO_E_SIZE // Always last element, not a valid option. } SRT_SOCKOPT; @@ -308,6 +309,13 @@ typedef enum SRT_TRANSTYPE SRTT_INVALID } SRT_TRANSTYPE; +typedef enum SRT_SENDMODETYPE +{ + SRTM_LIVE = 0, // default + SRTM_EAGER = 1, + SRTM_PLANNED = 2 +} SRT_SENDMODETYPE; + // These sizes should be used for Live mode. In Live mode you should not // exceed the size that fits in a single MTU. diff --git a/test/test_buffer_snd.cpp b/test/test_buffer_snd.cpp index 26bb9f1da..67321a036 100644 --- a/test/test_buffer_snd.cpp +++ b/test/test_buffer_snd.cpp @@ -1,11 +1,16 @@ #include #include +#include +#include #include "gtest/gtest.h" #include "test_env.h" #include "sync.h" #include "ofmt.h" #include "buffer_snd.h" +#include "schedule_snd.h" +#include "api.h" +#include "queue.h" #include "crypto.h" #include "ofmt_iostream.h" @@ -69,7 +74,8 @@ class TestSndBuffer: public srt::Test void scheduleRexmit(int32_t seqlo, int32_t seqhi, steady_clock::duration uptime = steady_clock::duration()) { //time_point sched_at = steady_clock::now() + uptime; - m_buffer->insertLoss(seqlo, seqhi, steady_clock::now() + uptime); + steady_clock::time_point first; + m_buffer->insertLoss(seqlo, seqhi, steady_clock::now() + uptime, (first)); } void cancelRexmit(int32_t seq) @@ -322,6 +328,304 @@ TEST_F(TestSndBuffer, Basic) ofcoutl(m_buffer->show()); } +TEST_F(TestSndBuffer, ScheduledTaskPreservesMetadata) +{ + TestInit srtinit; + CUDTSocket* socket = NULL; + const SRTSOCKET socket_id = CUDT::uglobal().newSocket(&socket); + ASSERT_NE(socket_id, SRT_INVALID_SOCK); + + const steady_clock::time_point scheduled = steady_clock::now(); + const steady_clock::time_point deadline = scheduled + milliseconds_from(100); + { + const SendTaskProto proto(SchedPacket(socket, 4321, sched::TP_REGULAR), scheduled, deadline); + + SendTask task(proto); + EXPECT_EQ(task.m_Packet.seqno(), 4321); + EXPECT_EQ(task.m_Packet.type(), sched::TP_REGULAR); + EXPECT_EQ(task.m_tsSendTime, scheduled); + EXPECT_EQ(task.m_tsLatestDeliveryTime, deadline); + + SendTask copy(task); + EXPECT_EQ(copy.m_Packet.seqno(), 4321); + EXPECT_EQ(copy.m_tsSendTime, scheduled); + EXPECT_EQ(copy.m_tsLatestDeliveryTime, deadline); + + SendTask assigned; + assigned = task; + EXPECT_EQ(assigned.m_Packet.seqno(), 4321); + EXPECT_EQ(assigned.m_tsLatestDeliveryTime, deadline); + + SendTask empty; + assigned = empty; + EXPECT_EQ(assigned.m_Packet.seqno(), SRT_SEQNO_NONE); + } + + srt_close(socket_id); +} + +TEST(SendScheduler, SkipsBlockedSocketAndReschedulesItAfterWindowOpens) +{ + TestInit srtinit; + CUDTSocket* blocked_socket = NULL; + CUDTSocket* ready_socket = NULL; + const SRTSOCKET blocked_id = CUDT::uglobal().newSocket(&blocked_socket); + const SRTSOCKET ready_id = CUDT::uglobal().newSocket(&ready_socket); + ASSERT_NE(blocked_id, SRT_INVALID_SOCK); + ASSERT_NE(ready_id, SRT_INVALID_SOCK); + + TestMockCUDT blocked_mock; + blocked_mock.core = &blocked_socket->core(); + blocked_mock.setSendWindows(0, 10); + TestMockCUDT ready_mock; + ready_mock.core = &ready_socket->core(); + ready_mock.setSendWindows(10, 10); + + SendScheduler scheduler; + { + const steady_clock::time_point now = steady_clock::now(); + scheduler.prescheduleRegular(SendTaskProto( + SchedPacket(blocked_socket, 4321, sched::TP_REGULAR), + now - milliseconds_from(20), now + seconds_from(1))); + scheduler.prescheduleRegular(SendTaskProto( + SchedPacket(ready_socket, 8765, sched::TP_REGULAR), + now - milliseconds_from(10), now + seconds_from(1))); + scheduler.prescheduleRegular(SendTaskProto( + SchedPacket(blocked_socket, 4322, sched::TP_REGULAR), + now - milliseconds_from(5), now + seconds_from(1))); + + SendTaskProto selected; + const SchedPacket first = scheduler.wait_pop(&selected); + EXPECT_EQ(first.id(), ready_id); + EXPECT_EQ(first.seqno(), 8765); + + std::future waiting = std::async(std::launch::async, [&scheduler, &selected]() + { + return scheduler.wait_pop(&selected); + }); + const std::future_status early_status = waiting.wait_for(std::chrono::milliseconds(20)); + + blocked_mock.setSendWindows(10, 10); + scheduler.notifyPendingTasks(); + const std::future_status status = waiting.wait_for(std::chrono::seconds(2)); + if (status != std::future_status::ready) + scheduler.interrupt(); + const SchedPacket second = waiting.get(); + EXPECT_EQ(early_status, std::future_status::timeout); + EXPECT_EQ(status, std::future_status::ready); + EXPECT_EQ(second.id(), blocked_id); + EXPECT_TRUE(second.seqno() == 4321 || second.seqno() == 4322); + ASSERT_EQ(scheduler.queue().size(), size_t(1)); + } + + srt_close(blocked_id); + srt_close(ready_id); +} + +#ifdef SRT_ENABLE_ENCRYPTION +TEST(SendScheduler, KeepsKeyBlockedPacketsPendingUntilNotified) +{ + TestInit srtinit; + CUDTSocket* blocked_socket = NULL; + CUDTSocket* ready_socket = NULL; + const SRTSOCKET blocked_id = CUDT::uglobal().newSocket(&blocked_socket); + const SRTSOCKET ready_id = CUDT::uglobal().newSocket(&ready_socket); + ASSERT_NE(blocked_id, SRT_INVALID_SOCK); + ASSERT_NE(ready_id, SRT_INVALID_SOCK); + + TestMockCUDT blocked_mock; + blocked_mock.core = &blocked_socket->core(); + blocked_mock.setSendWindows(10, 10); + const char passphrase[] = "scheduler-test-key"; + ASSERT_EQ(srt_setsockflag(blocked_id, SRTO_PASSPHRASE, passphrase, sizeof(passphrase) - 1), SRT_STATUS_OK); + CMultiplexer mux; + CPacket response; + CPacket previous_response; + ASSERT_TRUE(blocked_mock.prepareInitialKeyRefresh(mux, previous_response)); + TestMockCUDT ready_mock; + ready_mock.core = &ready_socket->core(); + ready_mock.setSendWindows(10, 10); + + SendScheduler scheduler; + { + const steady_clock::time_point now = steady_clock::now(); + const steady_clock::time_point deadline = now + seconds_from(1); + scheduler.prescheduleRegular(SendTaskProto( + SchedPacket(blocked_socket, 4321, sched::TP_REGULAR), now - milliseconds_from(20), deadline)); + scheduler.prescheduleRegular(SendTaskProto( + SchedPacket(ready_socket, 8765, sched::TP_REGULAR), now - milliseconds_from(10), deadline)); + scheduler.prescheduleRegular(SendTaskProto( + SchedPacket(blocked_socket, 4322, sched::TP_REGULAR), now + seconds_from(1), deadline)); + + const SchedPacket first = scheduler.wait_pop(); + EXPECT_EQ(first.id(), ready_id); + EXPECT_EQ(first.seqno(), 8765); + EXPECT_TRUE(scheduler.queue().empty()); + + SendTaskProto selected; + std::future waiting = std::async(std::launch::async, [&scheduler, &selected]() + { + return scheduler.wait_pop(&selected); + }); + EXPECT_EQ(waiting.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout); + + EXPECT_TRUE(blocked_mock.startInitialKeyRefresh(response)); + EXPECT_TRUE(blocked_mock.processSrtMsg(&response)); + scheduler.notifyPendingTasks(); + const std::future_status status = waiting.wait_for(std::chrono::seconds(2)); + if (status != std::future_status::ready) + scheduler.interrupt(); + const SchedPacket second = waiting.get(); + EXPECT_EQ(status, std::future_status::ready); + EXPECT_EQ(second.id(), blocked_id); + EXPECT_EQ(second.seqno(), 4321); + EXPECT_EQ(selected.m_tsLatestDeliveryTime, deadline); + EXPECT_EQ(scheduler.queue().size(), size_t(1)); + } + + blocked_mock.resetScheduledCrypto(); + srt_close(blocked_id); + srt_close(ready_id); +} + +class ScheduledKeyRefresh: public srt::Test +{ +protected: + CMultiplexer mux; + CUDTSocket* socket = NULL; + SRTSOCKET socket_id = SRT_INVALID_SOCK; + TestMockCUDT mock; + CPacket response; + CPacket previous_response; + + void setup() override + { + socket_id = CUDT::uglobal().newSocket(&socket); + ASSERT_NE(socket_id, SRT_INVALID_SOCK); + const char passphrase[] = "scheduler-test-key"; + ASSERT_EQ(srt_setsockflag(socket_id, SRTO_PASSPHRASE, passphrase, sizeof(passphrase) - 1), SRT_STATUS_OK); + mock.core = &socket->core(); + mock.setSendWindows(10, 10); + ASSERT_TRUE(mock.prepareInitialKeyRefresh(mux, previous_response)); + } + + void teardown() override + { + SendScheduler& scheduler = TestMockCUDT::sendScheduler(mux); + scheduler.interrupt(); + scheduler.withdraw(socket_id); + if (mock.core) + mock.resetScheduledCrypto(); + if (socket_id != SRT_INVALID_SOCK) + srt_close(socket_id); + } + + void verifyResponseWakeup(bool response_before_wait, bool window_blocked = false) + { + SendScheduler& scheduler = TestMockCUDT::sendScheduler(mux); + const steady_clock::time_point now = steady_clock::now(); + const char payload[] = "buffered while the independent key is pending"; + SRT_MSGCTRL control = srt_msgctrl_default; + control.srctime = count_microseconds((now - milliseconds_from(20)).time_since_epoch()); + EXPECT_EQ(mock.sendScheduledData(payload, sizeof(payload), control), int(sizeof(payload))); + EXPECT_EQ(control.pktseq, 4321); + EXPECT_TRUE(mock.initialKeyRefreshPending()); + EXPECT_TRUE(scheduler.queue().empty()); + + SendTaskProto selected; + std::future waiting; + if (!response_before_wait) + { + waiting = std::async(std::launch::async, [&scheduler, &selected]() + { + return scheduler.wait_pop(&selected); + }); + EXPECT_EQ(waiting.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout); + + EXPECT_TRUE(mock.processSrtMsg(&previous_response)); + EXPECT_TRUE(mock.initialKeyRefreshPending()); + EXPECT_EQ(waiting.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout); + } + + // checkSndTimers, not prescheduling or packet extraction, starts the + // refresh. Echo its actual refreshed KM payload as the KMRSP. + EXPECT_TRUE(mock.startInitialKeyRefresh(response)); + EXPECT_TRUE(mock.initialKeyRefreshPending()); + EXPECT_TRUE(scheduler.queue().empty()); + if (!response_before_wait) + { + CPacket rejected; + rejected.allocate(response.getLength()); + memcpy(rejected.m_pcData, response.m_pcData, response.getLength()); + rejected.setLength(response.getLength()); + rejected.setExtendedType(SRT_CMD_KMRSP); + rejected.m_pcData[rejected.getLength() - 1] ^= 1; + EXPECT_TRUE(mock.processSrtMsg(&rejected)); + EXPECT_TRUE(mock.initialKeyRefreshPending()); + EXPECT_EQ(waiting.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout); + + EXPECT_TRUE(mock.processSrtMsg(&previous_response)); + EXPECT_TRUE(mock.initialKeyRefreshPending()); + EXPECT_EQ(waiting.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout); + } + + if (window_blocked) + mock.setSendWindows(0, 0); + EXPECT_TRUE(mock.processSrtMsg(&response)); + EXPECT_FALSE(mock.initialKeyRefreshPending()); + if (window_blocked) + { + EXPECT_EQ(waiting.wait_for(std::chrono::milliseconds(20)), std::future_status::timeout); + EXPECT_TRUE(scheduler.queue().empty()); + mock.setSendWindows(10, 10); + mux.notifyScheduledSendReady(); + } + if (response_before_wait) + { + waiting = std::async(std::launch::async, [&scheduler, &selected]() + { + return scheduler.wait_pop(&selected); + }); + } + const std::future_status status = waiting.wait_for(std::chrono::seconds(2)); + if (status != std::future_status::ready) + scheduler.interrupt(); + const SchedPacket packet = waiting.get(); + EXPECT_EQ(status, std::future_status::ready); + EXPECT_EQ(packet.id(), socket_id); + EXPECT_EQ(packet.seqno(), 4321); + EXPECT_GE(selected.m_tsLatestDeliveryTime, selected.m_tsSendTime); + EXPECT_TRUE(scheduler.queue().empty()); + + // Capacity was checked before timing the task. A later window shrink + // must not discard the already committed regular packet. + mock.setSendWindows(0, 0); + CSndPacket packed; + EXPECT_TRUE(mock.packScheduledData(packet.seqno(), packed)); + EXPECT_EQ(packed.pkt.getLength(), sizeof(payload)); + + // A duplicate response must not block the next readiness check. + EXPECT_TRUE(mock.processSrtMsg(&response)); + EXPECT_TRUE(mock.core->isSendReady()); + } +}; + +TEST_F(ScheduledKeyRefresh, MatchingResponseWakesPendingSender) +{ + verifyResponseWakeup(false); +} + +TEST_F(ScheduledKeyRefresh, MatchingResponseBeforeWaitIsNotLost) +{ + verifyResponseWakeup(true); +} + +TEST_F(ScheduledKeyRefresh, MatchingResponseStillWaitsForFlightCapacity) +{ + verifyResponseWakeup(false, true); +} +#endif + static size_t generateRandomPayload(char* pw_out, size_t minsize, size_t maxsize) { // Generate the size @@ -1211,8 +1515,8 @@ TEST_F(TestSndLoss, Jumpover_removal_and_selective_clearing) EXPECT_EQ(packets.loss_length(), 6); // Now insert 8, but with 1h ahead - time_point out; - packets.insert_loss(8, 8, steady_clock::now() + seconds_from(3600), (out)); + steady_clock::time_point firstout; + packets.insert_loss(8, 8, steady_clock::now() + seconds_from(3600), (firstout)); EXPECT_EQ(packets.loss_length(), 7); @@ -1256,3 +1560,80 @@ TEST_F(TestSndLoss, Jumpover_removal_and_selective_clearing) EXPECT_EQ(packets.loss_length(), 0); } + +TEST_F(TestSndLoss, Remove_from_single_range_middle_split) +{ + // Initial: Nodes = {5: len=3, next=0}; first=5, last=5. + packets.insert_loss(5, 7); + + // Operation: Remove single first. + EXPECT_TRUE(packets.extractLoss(6)); + + // Expected: Nodes = {6: len=2, next=0}; first=6, last=6. (Implicit len=0 at old 5 position.) + EXPECT_EQ(packets.loss_length(), 2); + EXPECT_EQ(packets.first_loss(), 5); + EXPECT_EQ(packets.last_loss(), 7); + + std::string validmsg; + EXPECT_TRUE(packets.validateLossIntegrity((validmsg))) << ">>> " << validmsg; +} + +TEST_F(TestSndLoss, Remove_explicit_from_first_range_len_1_multiple_ranges_AKA_update_first) +{ + // Initial: Nodes = {1: len=1, next=4}, {5: len=3, next=0}; first=1, last=5. + packets.insert_loss(5, 7); + packets.insert_loss(1, 1); + + // Operation: Remove single first. + EXPECT_TRUE(packets.extractLoss(1)); + + // EXPECT: removed nodes + EXPECT_EQ(packets[1].m_iLossLength, 0); + + // Expected: Nodes = {5: len=3, next=0}; first=5, last=5. + EXPECT_EQ(packets[5].m_iLossLength, 3); + + EXPECT_EQ(packets.loss_length(), 3); + EXPECT_EQ(packets.first_loss(), 5); + EXPECT_EQ(packets.last_loss(), 5); + + std::string validmsg; + EXPECT_TRUE(packets.validateLossIntegrity((validmsg))) << ">>> " << validmsg; +} + +TEST_F(TestSndLoss, Remove_from_first_range_explicit_multiple_ranges_AKA_shrink_last_unchanged) +{ + // Initial: Nodes = {1: len=3, next=5}, {8: len=2, next=0}; first=1, last=8, len=5 + packets.insert_loss(1, 4); + packets.insert_loss(8, 9); + EXPECT_EQ(packets.loss_length(), 6); + EXPECT_EQ(packets[1].m_iLossLength, 4); + + EXPECT_FALSE(packets.extractLoss(5)); + EXPECT_EQ(packets.loss_length(), 6); + EXPECT_EQ(packets[1].m_iLossLength, 4); // Extraction failed, length unchanged + + EXPECT_TRUE(packets.extractLoss(4)); + EXPECT_EQ(packets[1].m_iLossLength, 3); // One extracted + + // Operation: Remove single first. + EXPECT_TRUE(packets.extractLoss(1)); + + // EXPECT: removed one node + EXPECT_EQ(packets[1].m_iLossLength, 0); + + // Expected: Nodes = {2: len=2, next=6}, {8: len=2, next=0}; first=2, last=8. (Next updated: 2 to 8 offset=6 > 2.) + EXPECT_EQ(packets[2].m_iLossLength, 2); + EXPECT_EQ(packets[2].m_iNextLossRecOff, 8-2); + + EXPECT_EQ(packets[8].m_iLossLength, 2); + EXPECT_EQ(packets[8].m_iNextLossRecOff, 0); + + EXPECT_EQ(packets.first_loss(), 2); + EXPECT_EQ(packets.last_loss(), 8); + + EXPECT_EQ(packets.loss_length(), 4); + + std::string validmsg; + EXPECT_TRUE(packets.validateLossIntegrity((validmsg))) << ">>> " << validmsg; +} diff --git a/test/test_env.h b/test/test_env.h index 9d10f6882..b0e18761f 100644 --- a/test/test_env.h +++ b/test/test_env.h @@ -151,8 +151,11 @@ class Test: public testing::Test class CUDT; class CPacket; +struct CSndPacket; struct CUnit; class CUDTSocket; +struct CMultiplexer; +struct SendScheduler; class TestMockCUDT { @@ -173,7 +176,18 @@ class TestMockCUDT void processCtrlAck(const CPacket& pkt, const sync::steady_clock::time_point& t); int flowWindowSize() const; + int64_t lastSendSrctime() const; void setFlowWindowSize(int v); + void setSendWindows(int flow_window, int congestion_window); + bool initialKeyRefreshPending() const; +#ifdef SRT_ENABLE_ENCRYPTION + bool prepareInitialKeyRefresh(CMultiplexer& mux, CPacket& previous_response); + bool startInitialKeyRefresh(CPacket& response); + int sendScheduledData(const char* data, int length, SRT_MSGCTRL& control); + bool packScheduledData(int32_t seqno, CSndPacket& packet); + void resetScheduledCrypto(); + static SendScheduler& sendScheduler(CMultiplexer& mux); +#endif }; struct sockaddr_any CreateAddr(const std::string& name, unsigned short port, int pref_family); diff --git a/test/test_main.cpp b/test/test_main.cpp index 19030a6df..d177bee3c 100644 --- a/test/test_main.cpp +++ b/test/test_main.cpp @@ -15,6 +15,9 @@ #include "netinet_any.h" #include "core.h" #include "api.h" +#include "queue.h" +#include "channel.h" +#include "buffer_snd.h" #include "logging_api.h" #include "logger_fas.h" @@ -363,6 +366,95 @@ bool TestMockCUDT::setSocket(int32_t sock) void TestMockCUDT::processCtrlAck(const CPacket& pkt, const sync::steady_clock::time_point& t) { core->processCtrlAck(pkt, t); } int TestMockCUDT::flowWindowSize() const { return core->m_iFlowWindowSize; } +int64_t TestMockCUDT::lastSendSrctime() const { return core->m_iLastSndSrctime; } void TestMockCUDT::setFlowWindowSize(int v) { core->m_iFlowWindowSize = v; } +void TestMockCUDT::setSendWindows(int flow_window, int congestion_window) +{ + core->m_iFlowWindowSize = flow_window; + core->m_iCongestionWindow = congestion_window; +} + +bool TestMockCUDT::initialKeyRefreshPending() const { return !core->isSendReady(); } +#ifdef SRT_ENABLE_ENCRYPTION +bool TestMockCUDT::prepareInitialKeyRefresh(CMultiplexer& mux, CPacket& previous_response) +{ + sync::ScopedLock lock(core->m_ConnectionLock); + core->m_config.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; + core->m_CryptoControl.setCryptoSecret(core->m_config.CryptoSecret); + if (!core->m_CryptoControl.initialized() + && !core->m_CryptoControl.init(core->m_SocketID, HSD_INITIATOR, core->m_config, false)) + return false; + + const CCryptoControl::KmMessage* initial = core->m_CryptoControl.getKmMsg(0); + uint32_t data[SRTDATA_MAXSIZE]; + NtoHLA(data, reinterpret_cast(initial->bytedata()), initial->bytesize() / sizeof(uint32_t)); + previous_response.allocate(initial->bytesize()); + memcpy(previous_response.m_pcData, data, initial->bytesize()); + previous_response.setLength(initial->bytesize()); + previous_response.setExtendedType(SRT_CMD_KMRSP); + if (core->m_CryptoControl.processSrtMsg_KMRSP( + data, initial->bytesize(), SrtVersion(1, 6, 0), true) != 1) + return false; + + mux.m_pChannel = new CChannel; + mux.m_pChannel->setConfig(CSrtMuxerConfig()); + mux.m_pChannel->open(CreateAddr("127.0.0.1", 0, AF_INET)); + core->m_pMuxer = &mux; + core->m_PeerAddr = mux.m_pChannel->getSockAddr(); + core->m_uPeerSrtVersion = SrtVersion(1, 6, 0); + core->m_config.uSenderMode = 1; + core->m_TransferIPVersion = AF_INET; + core->m_iMaxDataPayloadSize = 1456; + core->m_pSndBuffer = new CSndBuffer(32 * 1024, 32, 1500, + CPacket::HDR_SIZE + CPacket::udpHeaderSize(AF_INET), 0, 10); + core->m_iISN = core->m_iSndNextSeqNo = core->m_iSndLastAck = 4321; + core->m_iSndCurrSeqNo = 4320; + core->m_stats.tsStartTime = sync::steady_clock::now() - sync::seconds_from(1); + if (core->setupCC() != SRT_REJ_UNKNOWN) + return false; + setSendWindows(10, 10); + core->m_bOpened = true; + core->m_State = CUDT::SSS_CONNECTED; + return initialKeyRefreshPending(); +} + +bool TestMockCUDT::startInitialKeyRefresh(CPacket& response) +{ + core->checkSndTimers(); + const CCryptoControl::KmMessage* refreshed = core->m_CryptoControl.getKmMsg(1); + if (refreshed->bytesize() == 0) + return false; + response.allocate(refreshed->bytesize()); + NtoHLA(reinterpret_cast(response.m_pcData), + reinterpret_cast(refreshed->bytedata()), refreshed->bytesize() / sizeof(uint32_t)); + response.setLength(refreshed->bytesize()); + response.setExtendedType(SRT_CMD_KMRSP); + // Make the KM retransmit due without sleeping for the default RTT. + const int retries = refreshed->iPeerRetry; + core->m_iSRTT = 0; + core->checkSndTimers(); + return initialKeyRefreshPending() && retries > 0 && refreshed->iPeerRetry == retries - 1; +} + +int TestMockCUDT::sendScheduledData(const char* data, int length, SRT_MSGCTRL& control) +{ + return core->sendmsg2(data, length, control); +} + +bool TestMockCUDT::packScheduledData(int32_t seqno, CSndPacket& packet) +{ + CNetworkInterface source; + return core->packData(SchedPacket(core->m_parent, seqno, sched::TP_REGULAR), packet, source) == CUDT::PACK_OK; +} + +void TestMockCUDT::resetScheduledCrypto() +{ + core->m_State = CUDT::SSS_INIT; + core->m_bOpened = false; + core->m_pMuxer = NULL; +} + +SendScheduler& TestMockCUDT::sendScheduler(CMultiplexer& mux) { return mux.m_SndQueue.m_Scheduler; } +#endif } diff --git a/test/test_socketdata.cpp b/test/test_socketdata.cpp index 0b2a2bf51..8f55d9103 100644 --- a/test/test_socketdata.cpp +++ b/test/test_socketdata.cpp @@ -72,6 +72,48 @@ TEST(SocketData, PeerName) srt_close(lsock); } +TEST(SocketData, RejectsOutOfOrderSourceTime) +{ + srt::TestInit srtinit; + + srt::UniqueSocket listener; + srt::UniqueSocket caller; + srt::UniqueSocket accepted; + listener.ref() = srt_create_socket(); + caller.ref() = srt_create_socket(); + ASSERT_NE(listener.get(), SRT_INVALID_SOCK); + ASSERT_NE(caller.get(), SRT_INVALID_SOCK); + + const bool sender = true; + ASSERT_EQ(srt_setsockflag(caller.get(), SRTO_SENDER, &sender, sizeof(sender)), SRT_STATUS_OK); + + sockaddr_any addr = srt::CreateAddr("127.0.0.1", 0, AF_INET); + ASSERT_EQ(srt_bind(listener.get(), addr.get(), addr.size()), SRT_STATUS_OK); + ASSERT_EQ(srt_getsockname(listener.get(), addr.get(), &addr.len), SRT_STATUS_OK); + ASSERT_EQ(srt_listen(listener.get(), 1), SRT_STATUS_OK); + ASSERT_EQ(srt_connect(caller.get(), addr.get(), addr.size()), SRT_STATUS_OK); + + sockaddr_any peer; + accepted.ref() = srt_accept(listener.get(), peer.get(), &peer.len); + ASSERT_NE(accepted.get(), SRT_INVALID_SOCK); + + const int64_t now = sync::count_microseconds(sync::steady_clock::now().time_since_epoch()); + char data = 'x'; + SRT_MSGCTRL first = srt_msgctrl_default; + first.srctime = now + 5000000; + EXPECT_EQ(srt_sendmsg2(caller.get(), &data, 1, &first), 1); + TestMockCUDT caller_mock; + CUDTSocket* caller_socket = caller_mock.locateSocket(caller.get()); + ASSERT_NE(caller_socket, nullptr); + caller_mock.core = &caller_socket->core(); + EXPECT_EQ(caller_mock.lastSendSrctime(), first.srctime); + + SRT_MSGCTRL earlier = srt_msgctrl_default; + earlier.srctime = now + 4000000; + EXPECT_EQ(srt_sendmsg2(caller.get(), &data, 1, &earlier), SRT_ERROR); + EXPECT_STREQ(srt_getlasterror_str(), "Operation not supported: Bad parameters"); +} + // Wait until `sock` reaches the expected state, but no longer than `cap`. // Polling instead of sleeping blindly keeps the assertion just as strong // while returning as soon as the condition is met. @@ -245,4 +287,3 @@ TEST(SocketData, CheckDragCaller) EXPECT_EQ(core.getSockets().size(), size_t()); EXPECT_EQ(core.getClosedSockets().size(), size_t()); } -