diff --git a/src/lib/dnssd/minimal_mdns/ActiveResolveAttempts.cpp b/src/lib/dnssd/ActiveResolveAttempts.cpp similarity index 74% rename from src/lib/dnssd/minimal_mdns/ActiveResolveAttempts.cpp rename to src/lib/dnssd/ActiveResolveAttempts.cpp index f61799db6170c0..9137b1bdb169b4 100644 --- a/src/lib/dnssd/minimal_mdns/ActiveResolveAttempts.cpp +++ b/src/lib/dnssd/ActiveResolveAttempts.cpp @@ -33,7 +33,7 @@ void ActiveResolveAttempts::Reset() { for (auto & item : mRetryQueue) { - item.peerId.SetNodeId(kUndefinedNodeId); + item.attempt.Clear(); } } @@ -41,9 +41,9 @@ void ActiveResolveAttempts::Complete(const PeerId & peerId) { for (auto & item : mRetryQueue) { - if (item.peerId == peerId) + if (item.attempt.Matches(peerId)) { - item.peerId.SetNodeId(kUndefinedNodeId); + item.attempt.Clear(); return; } } @@ -53,7 +53,31 @@ void ActiveResolveAttempts::Complete(const PeerId & peerId) ChipLogProgress(Discovery, "Discovered node without a pending query"); } -void ActiveResolveAttempts::MarkPending(const PeerId & peerId) +void ActiveResolveAttempts::Complete(const chip::Dnssd::DiscoveredNodeData & data) +{ + for (auto & item : mRetryQueue) + { + if (item.attempt.Matches(data)) + { + item.attempt.Clear(); + return; + } + } +} + +void ActiveResolveAttempts::MarkPending(const chip::PeerId & peerId) +{ + ScheduledAttempt attempt(peerId, /* firstSend */ true); + MarkPending(attempt); +} + +void ActiveResolveAttempts::MarkPending(const chip::Dnssd::DiscoveryFilter & filter, const chip::Dnssd::DiscoveryType type) +{ + ScheduledAttempt attempt(filter, type, /* firstSend */ true); + MarkPending(attempt); +} + +void ActiveResolveAttempts::MarkPending(const ScheduledAttempt & attempt) { // Strategy when picking the peer id to use: // 1 if a matching peer id is already found, use that one @@ -66,27 +90,27 @@ void ActiveResolveAttempts::MarkPending(const PeerId & peerId) for (size_t i = 1; i < kRetryQueueSize; i++) { - if (entryToUse->peerId == peerId) + if (entryToUse->attempt.Matches(attempt)) { break; // best match possible } RetryEntry * entry = mRetryQueue + i; - // Rule 1: peer id match always matches - if (entry->peerId == peerId) + // Rule 1: attempt match always matches + if (entry->attempt.Matches(attempt)) { entryToUse = entry; continue; } // Rule 2: select unused entries - if ((entryToUse->peerId.GetNodeId() != kUndefinedNodeId) && (entry->peerId.GetNodeId() == kUndefinedNodeId)) + if (!entryToUse->attempt.IsEmpty() && entry->attempt.IsEmpty()) { entryToUse = entry; continue; } - else if (entryToUse->peerId.GetNodeId() == kUndefinedNodeId) + else if (entryToUse->attempt.IsEmpty()) { continue; } @@ -106,7 +130,7 @@ void ActiveResolveAttempts::MarkPending(const PeerId & peerId) } } - if ((entryToUse->peerId.GetNodeId() != kUndefinedNodeId) && (entryToUse->peerId != peerId)) + if ((!entryToUse->attempt.IsEmpty()) && (!entryToUse->attempt.Matches(attempt))) { // TODO: node was evicted here, if/when resolution failures are // supported this could be a place for error callbacks @@ -118,8 +142,7 @@ void ActiveResolveAttempts::MarkPending(const PeerId & peerId) ChipLogError(Discovery, "Re-using pending resolve entry before reply was received."); } - entryToUse->peerId = peerId; - entryToUse->firstSend = true; + entryToUse->attempt = attempt; entryToUse->queryDueTime = mClock->GetMonotonicTimestamp(); entryToUse->nextRetryDelay = System::Clock::Seconds16(1); } @@ -132,7 +155,7 @@ Optional ActiveResolveAttempts::GetTimeUntilNextExpected for (auto & entry : mRetryQueue) { - if (entry.peerId.GetNodeId() == kUndefinedNodeId) + if (entry.attempt.IsEmpty()) { continue; } @@ -153,13 +176,13 @@ Optional ActiveResolveAttempts::GetTimeUntilNextExpected return minDelay; } -Optional ActiveResolveAttempts::NextScheduledPeer() +Optional ActiveResolveAttempts::NextScheduled() { chip::System::Clock::Timestamp now = mClock->GetMonotonicTimestamp(); for (auto & entry : mRetryQueue) { - if (entry.peerId.GetNodeId() == kUndefinedNodeId) + if (entry.attempt.IsEmpty()) { continue; // not a pending item } @@ -172,20 +195,20 @@ Optional ActiveResolveAttempts::NextSch if (entry.nextRetryDelay > kMaxRetryDelay) { ChipLogError(Discovery, "Timeout waiting for mDNS resolution."); - entry.peerId.SetNodeId(kUndefinedNodeId); + entry.attempt.Clear(); continue; } entry.queryDueTime = now + entry.nextRetryDelay; entry.nextRetryDelay *= 2; - ScheduledResolve result(entry); - entry.firstSend = false; + Optional attempt = MakeOptional(entry.attempt); + entry.attempt.firstSend = false; - return Optional::Value(result); + return attempt; } - return Optional::Missing(); + return Optional::Missing(); } } // namespace Minimal diff --git a/src/lib/dnssd/ActiveResolveAttempts.h b/src/lib/dnssd/ActiveResolveAttempts.h new file mode 100644 index 00000000000000..27a32a3e5f909d --- /dev/null +++ b/src/lib/dnssd/ActiveResolveAttempts.h @@ -0,0 +1,195 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace mdns { +namespace Minimal { + +/// Keeps track of active resolve attempts +/// +/// Maintains a list of 'pending mdns resolve queries' and provides operations +/// for: +/// - add/remove to the list +/// - figuring out a 'next query time' for items in the list +/// - iterating through the 'schedule now' items of the list +/// +class ActiveResolveAttempts +{ +public: + static constexpr size_t kRetryQueueSize = 4; + static constexpr chip::System::Clock::Timeout kMaxRetryDelay = chip::System::Clock::Seconds16(16); + + struct ScheduledAttempt + { + enum AttemptType + { + kInvalid, + kResolve, + kBrowse, + }; + + ScheduledAttempt() : attemptType(kInvalid) {} + ScheduledAttempt(const chip::PeerId & peer, bool first) : attemptType(kResolve), peerId(peer), firstSend(first) {} + ScheduledAttempt(const chip::Dnssd::DiscoveryFilter discoveryFilter, const chip::Dnssd::DiscoveryType type, bool first) : + attemptType(kBrowse), browse(discoveryFilter, type), firstSend(first) + {} + bool operator==(const ScheduledAttempt & other) const { return Matches(other) && other.firstSend == firstSend; } + bool Matches(const ScheduledAttempt & other) const + { + if (other.attemptType != attemptType) + { + return false; + } + switch (attemptType) + { + case kInvalid: + return true; + case kBrowse: + return (other.browse.filter == browse.filter && other.browse.type == browse.type); + case kResolve: + return other.peerId == peerId; + default: + return false; + } + } + bool Matches(const chip::PeerId & peer) const { return (attemptType == kResolve) && (peerId == peer); } + bool Matches(const chip::Dnssd::DiscoveredNodeData & data) const + { + if (attemptType != kBrowse) + { + return false; + } + // TODO: we should mark returned node data based on the query + if (browse.type != chip::Dnssd::DiscoveryType::kCommissionableNode) + { + // We don't currently have markers in the returned DiscoveredNodeData to differentiate these, so assume all returned + // packets match + return true; + } + switch (browse.filter.type) + { + case chip::Dnssd::DiscoveryFilterType::kNone: + return true; + case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator: + return browse.filter.code == static_cast((data.longDiscriminator >> 8) & 0x0F); + case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator: + return browse.filter.code == data.longDiscriminator; + case chip::Dnssd::DiscoveryFilterType::kVendorId: + return browse.filter.code == data.vendorId; + case chip::Dnssd::DiscoveryFilterType::kDeviceType: + return browse.filter.code == data.deviceType; + case chip::Dnssd::DiscoveryFilterType::kCommissioningMode: + return browse.filter.code == data.commissioningMode; + case chip::Dnssd::DiscoveryFilterType::kInstanceName: + return strncmp(browse.filter.instanceName, data.instanceName, + chip::Dnssd::Commission::kInstanceNameMaxLength + 1) == 0; + case chip::Dnssd::DiscoveryFilterType::kCommissioner: + case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId: + default: + // These are for other discovery types. + return false; + } + } + bool IsEmpty() const { return attemptType == kInvalid; } + bool IsResolve() const { return attemptType == kResolve; } + bool IsBrowse() const { return attemptType == kBrowse; } + void Clear() { attemptType = kInvalid; } + + // Not using Variant because it assumes a heap impl + AttemptType attemptType; + struct Browse + { + Browse(const chip::Dnssd::DiscoveryFilter discoveryFilter, const chip::Dnssd::DiscoveryType discoveryType) : + filter(discoveryFilter), type(discoveryType) + {} + chip::Dnssd::DiscoveryFilter filter; + chip::Dnssd::DiscoveryType type; + }; + union + { + chip::PeerId peerId; // Peer id for resolve attempts + Browse browse; + }; + // First packet send is marked separately: minMDNS logic can choose + // to first send a unicast query followed by a multicast one. + bool firstSend = false; + }; + + ActiveResolveAttempts(chip::System::Clock::ClockBase * clock) : mClock(clock) { Reset(); } + + /// Clear out the internal queue + void Reset(); + + /// Mark a resolution as a success, removing it from the internal list + void Complete(const chip::PeerId & peerId); + void Complete(const chip::Dnssd::DiscoveredNodeData & data); + + /// Mark that a resolution is pending, adding it to the internal list + /// + /// Once this complete, this peer id will be returned immediately + /// by NextScheduled (potentially with others as well) + void MarkPending(const chip::PeerId & peerId); + void MarkPending(const chip::Dnssd::DiscoveryFilter & filter, const chip::Dnssd::DiscoveryType type); + + // Get minimum time until the next pending reply is required. + // + // Returns missing if no actively tracked elements exist. + chip::Optional GetTimeUntilNextExpectedResponse() const; + + // Get the peer Id that needs scheduling for a query + // + // Assumes that the resolution is being sent and will apply internal + // query logic. This means: + // - internal tracking of 'next due time' will updated as 'request sent + // now' + // - there is NO sorting implied by this call. Returned value will be + // any peer that needs a new request sent + chip::Optional NextScheduled(); + +private: + struct RetryEntry + { + ScheduledAttempt attempt; + // When a reply is expected for this item + chip::System::Clock::Timestamp queryDueTime; + + // Next expected delay for sending if reply is not reached by + // 'queryDueTimeMs' + // + // Based on RFC 6762 expectations are: + // - the interval between the first two queries MUST be at least + // one second + // - the intervals between successive queries MUST increase by at + // least a factor of two + chip::System::Clock::Timeout nextRetryDelay = chip::System::Clock::Seconds16(1); + }; + void MarkPending(const ScheduledAttempt & attempt); + chip::System::Clock::ClockBase * mClock; + RetryEntry mRetryQueue[kRetryQueueSize]; +}; + +} // namespace Minimal +} // namespace mdns diff --git a/src/lib/dnssd/BUILD.gn b/src/lib/dnssd/BUILD.gn index 4ad5559c9d455d..d4ce965ba4cd77 100644 --- a/src/lib/dnssd/BUILD.gn +++ b/src/lib/dnssd/BUILD.gn @@ -46,6 +46,8 @@ static_library("dnssd") { ] } else if (chip_mdns == "minimal") { sources += [ + "ActiveResolveAttempts.cpp", + "ActiveResolveAttempts.h", "Advertiser_ImplMinimalMdns.cpp", "MinimalMdnsServer.cpp", "MinimalMdnsServer.h", diff --git a/src/lib/dnssd/Resolver.h b/src/lib/dnssd/Resolver.h index 1b5951f89908cb..84c1f0b5d1eec9 100644 --- a/src/lib/dnssd/Resolver.h +++ b/src/lib/dnssd/Resolver.h @@ -301,12 +301,28 @@ enum class DiscoveryFilterType : uint8_t struct DiscoveryFilter { DiscoveryFilterType type; - uint64_t code; - const char * instanceName; + uint64_t code = 0; + const char * instanceName = nullptr; DiscoveryFilter() : type(DiscoveryFilterType::kNone), code(0) {} - DiscoveryFilter(DiscoveryFilterType newType) : type(newType) {} - DiscoveryFilter(DiscoveryFilterType newType, uint64_t newCode) : type(newType), code(newCode) {} - DiscoveryFilter(DiscoveryFilterType newType, const char * newInstanceName) : type(newType), instanceName(newInstanceName) {} + DiscoveryFilter(const DiscoveryFilterType newType) : type(newType) {} + DiscoveryFilter(const DiscoveryFilterType newType, uint64_t newCode) : type(newType), code(newCode) {} + DiscoveryFilter(const DiscoveryFilterType newType, const char * newInstanceName) : type(newType), instanceName(newInstanceName) + {} + bool operator==(const DiscoveryFilter & other) const + { + if (type != other.type) + { + return false; + } + if (type == DiscoveryFilterType::kInstanceName) + { + return (instanceName != nullptr) && (other.instanceName != nullptr) && (strcmp(instanceName, other.instanceName) == 0); + } + else + { + return code == other.code; + } + } }; enum class DiscoveryType { diff --git a/src/lib/dnssd/Resolver_ImplMinimalMdns.cpp b/src/lib/dnssd/Resolver_ImplMinimalMdns.cpp index 077525a786558a..8336122f5be7a4 100644 --- a/src/lib/dnssd/Resolver_ImplMinimalMdns.cpp +++ b/src/lib/dnssd/Resolver_ImplMinimalMdns.cpp @@ -20,11 +20,11 @@ #include #include +#include #include #include #include #include -#include #include #include #include @@ -324,6 +324,7 @@ void PacketDataReporter::OnComplete(ActiveResolveAttempts & activeAttempts) if ((mDiscoveryType == DiscoveryType::kCommissionableNode || mDiscoveryType == DiscoveryType::kCommissionerNode) && mDiscoveredNodeData.IsValid()) { + activeAttempts.Complete(mDiscoveredNodeData); if (mCommissioningDelegate != nullptr) { mCommissioningDelegate->OnNodeDiscovered(mDiscoveredNodeData); @@ -385,11 +386,13 @@ class MinMdnsResolver : public Resolver, public MdnsPacketDelegate ActiveResolveAttempts mActiveResolves; CHIP_ERROR SendPendingResolveQueries(); - CHIP_ERROR ScheduleResolveRetries(); + CHIP_ERROR SendPendingBrowseQueries(); + CHIP_ERROR SendAllPendingQueries(); + CHIP_ERROR ScheduleRetries(); - static void ResolveRetryCallback(System::Layer *, void * self); + static void RetryCallback(System::Layer *, void * self); - CHIP_ERROR SendQuery(mdns::Minimal::FullQName qname, mdns::Minimal::QType type); + CHIP_ERROR SendQuery(mdns::Minimal::FullQName qname, mdns::Minimal::QType type, bool unicastResponse); CHIP_ERROR BrowseNodes(DiscoveryType type, DiscoveryFilter subtype); template mdns::Minimal::FullQName CheckAndAllocateQName(Args &&... parts) @@ -421,7 +424,7 @@ void MinMdnsResolver::OnMdnsPacketData(const BytesRange & data, const chip::Inet else { reporter.OnComplete(mActiveResolves); - ScheduleResolveRetries(); + ScheduleRetries(); } } @@ -444,7 +447,7 @@ void MinMdnsResolver::Shutdown() GlobalMinimalMdnsServer::Instance().ShutdownServer(); } -CHIP_ERROR MinMdnsResolver::SendQuery(mdns::Minimal::FullQName qname, mdns::Minimal::QType type) +CHIP_ERROR MinMdnsResolver::SendQuery(mdns::Minimal::FullQName qname, mdns::Minimal::QType type, bool unicastResponse) { System::PacketBufferHandle buffer = System::PacketBufferHandle::New(kMdnsMaxPacketSize); ReturnErrorCodeIf(buffer.IsNull(), CHIP_ERROR_NO_MEMORY); @@ -454,13 +457,28 @@ CHIP_ERROR MinMdnsResolver::SendQuery(mdns::Minimal::FullQName qname, mdns::Mini mdns::Minimal::Query query(qname); query.SetType(type).SetClass(mdns::Minimal::QClass::IN); - query.SetAnswerViaUnicast(true); + query.SetAnswerViaUnicast(unicastResponse); builder.AddQuery(query); ReturnErrorCodeIf(!builder.Ok(), CHIP_ERROR_INTERNAL); - return GlobalMinimalMdnsServer::Server().BroadcastUnicastQuery(builder.ReleasePacket(), kMdnsPort); + if (unicastResponse) + { + ReturnErrorOnFailure(GlobalMinimalMdnsServer::Server().BroadcastUnicastQuery(builder.ReleasePacket(), kMdnsPort)); + } + else + { + ReturnErrorOnFailure(GlobalMinimalMdnsServer::Server().BroadcastSend(builder.ReleasePacket(), kMdnsPort)); + } + return CHIP_NO_ERROR; +} + +CHIP_ERROR MinMdnsResolver::SendAllPendingQueries() +{ + CHIP_ERROR browseErr = SendPendingBrowseQueries(); + CHIP_ERROR resolveErr = SendPendingResolveQueries(); + return resolveErr == CHIP_NO_ERROR ? browseErr : resolveErr; } CHIP_ERROR MinMdnsResolver::FindCommissionableNodes(DiscoveryFilter filter) @@ -473,57 +491,87 @@ CHIP_ERROR MinMdnsResolver::FindCommissioners(DiscoveryFilter filter) return BrowseNodes(DiscoveryType::kCommissionerNode, filter); } -// TODO(cecille): Extend filter and use this for Resolve CHIP_ERROR MinMdnsResolver::BrowseNodes(DiscoveryType type, DiscoveryFilter filter) { mDiscoveryType = type; + mActiveResolves.MarkPending(filter, type); - mdns::Minimal::FullQName qname; + return SendPendingBrowseQueries(); +} - switch (type) +CHIP_ERROR MinMdnsResolver::SendPendingBrowseQueries() +{ + CHIP_ERROR returnErr = CHIP_NO_ERROR; + while (true) { - case DiscoveryType::kOperational: - qname = CheckAndAllocateQName(kOperationalServiceName, kOperationalProtocol, kLocalDomain); - break; - case DiscoveryType::kCommissionableNode: - if (filter.type == DiscoveryFilterType::kNone) + Optional attempt = mActiveResolves.NextScheduled(); + + if (!attempt.HasValue()) { - qname = CheckAndAllocateQName(kCommissionableServiceName, kCommissionProtocol, kLocalDomain); + break; } - else if (filter.type == DiscoveryFilterType::kInstanceName) + if (!attempt.Value().IsBrowse()) { - qname = CheckAndAllocateQName(filter.instanceName, kCommissionableServiceName, kCommissionProtocol, kLocalDomain); + continue; } - else + + mdns::Minimal::FullQName qname; + + switch (attempt.Value().browse.type) { - char subtypeStr[Common::kSubTypeMaxLength + 1]; - ReturnErrorOnFailure(MakeServiceSubtype(subtypeStr, sizeof(subtypeStr), filter)); - qname = CheckAndAllocateQName(subtypeStr, kSubtypeServiceNamePart, kCommissionableServiceName, kCommissionProtocol, - kLocalDomain); + case DiscoveryType::kOperational: + qname = CheckAndAllocateQName(kOperationalServiceName, kOperationalProtocol, kLocalDomain); + break; + case DiscoveryType::kCommissionableNode: + if (attempt.Value().browse.filter.type == DiscoveryFilterType::kNone) + { + qname = CheckAndAllocateQName(kCommissionableServiceName, kCommissionProtocol, kLocalDomain); + } + else if (attempt.Value().browse.filter.type == DiscoveryFilterType::kInstanceName) + { + qname = CheckAndAllocateQName(attempt.Value().browse.filter.instanceName, kCommissionableServiceName, + kCommissionProtocol, kLocalDomain); + } + else + { + char subtypeStr[Common::kSubTypeMaxLength + 1]; + ReturnErrorOnFailure(MakeServiceSubtype(subtypeStr, sizeof(subtypeStr), attempt.Value().browse.filter)); + qname = CheckAndAllocateQName(subtypeStr, kSubtypeServiceNamePart, kCommissionableServiceName, kCommissionProtocol, + kLocalDomain); + } + break; + case DiscoveryType::kCommissionerNode: + if (attempt.Value().browse.filter.type == DiscoveryFilterType::kNone) + { + qname = CheckAndAllocateQName(kCommissionerServiceName, kCommissionProtocol, kLocalDomain); + } + else + { + char subtypeStr[Common::kSubTypeMaxLength + 1]; + ReturnErrorOnFailure(MakeServiceSubtype(subtypeStr, sizeof(subtypeStr), attempt.Value().browse.filter)); + qname = CheckAndAllocateQName(subtypeStr, kSubtypeServiceNamePart, kCommissionerServiceName, kCommissionProtocol, + kLocalDomain); + } + break; + case DiscoveryType::kUnknown: + break; } - break; - case DiscoveryType::kCommissionerNode: - if (filter.type == DiscoveryFilterType::kNone) + if (!qname.nameCount) { - qname = CheckAndAllocateQName(kCommissionerServiceName, kCommissionProtocol, kLocalDomain); + return CHIP_ERROR_NO_MEMORY; } - else + + bool unicastResponse = attempt.Value().firstSend; + + CHIP_ERROR err = SendQuery(qname, mdns::Minimal::QType::ANY, unicastResponse); + if (err != CHIP_NO_ERROR) { - char subtypeStr[Common::kSubTypeMaxLength + 1]; - ReturnErrorOnFailure(MakeServiceSubtype(subtypeStr, sizeof(subtypeStr), filter)); - qname = CheckAndAllocateQName(subtypeStr, kSubtypeServiceNamePart, kCommissionerServiceName, kCommissionProtocol, - kLocalDomain); + // We want to continue sending, but we do want this error returned + returnErr = err; } - break; - case DiscoveryType::kUnknown: - break; } - if (!qname.nameCount) - { - return CHIP_ERROR_NO_MEMORY; - } - - return SendQuery(qname, mdns::Minimal::QType::ANY); + ReturnErrorOnFailure(ScheduleRetries()); + return returnErr; } CHIP_ERROR MinMdnsResolver::ResolveNodeId(const PeerId & peerId, Inet::IPAddressType type) @@ -540,10 +588,10 @@ bool MinMdnsResolver::ResolveNodeIdFromInternalCache(const PeerId & peerId, Inet return false; } -CHIP_ERROR MinMdnsResolver::ScheduleResolveRetries() +CHIP_ERROR MinMdnsResolver::ScheduleRetries() { ReturnErrorCodeIf(mSystemLayer == nullptr, CHIP_ERROR_INCORRECT_STATE); - mSystemLayer->CancelTimer(&ResolveRetryCallback, this); + mSystemLayer->CancelTimer(&RetryCallback, this); Optional delay = mActiveResolves.GetTimeUntilNextExpectedResponse(); @@ -552,24 +600,28 @@ CHIP_ERROR MinMdnsResolver::ScheduleResolveRetries() return CHIP_NO_ERROR; } - return mSystemLayer->StartTimer(delay.Value(), &ResolveRetryCallback, this); + return mSystemLayer->StartTimer(delay.Value(), &RetryCallback, this); } -void MinMdnsResolver::ResolveRetryCallback(System::Layer *, void * self) +void MinMdnsResolver::RetryCallback(System::Layer *, void * self) { - reinterpret_cast(self)->SendPendingResolveQueries(); + reinterpret_cast(self)->SendAllPendingQueries(); } CHIP_ERROR MinMdnsResolver::SendPendingResolveQueries() { while (true) { - Optional resolve = mActiveResolves.NextScheduledPeer(); + Optional resolve = mActiveResolves.NextScheduled(); if (!resolve.HasValue()) { break; } + if (!resolve.Value().IsResolve()) + { + continue; + } System::PacketBufferHandle buffer = System::PacketBufferHandle::New(kMdnsMaxPacketSize); ReturnErrorCodeIf(buffer.IsNull(), CHIP_ERROR_NO_MEMORY); @@ -620,7 +672,7 @@ CHIP_ERROR MinMdnsResolver::SendPendingResolveQueries() } } - return ScheduleResolveRetries(); + return ScheduleRetries(); } MinMdnsResolver gResolver; diff --git a/src/lib/dnssd/minimal_mdns/ActiveResolveAttempts.h b/src/lib/dnssd/minimal_mdns/ActiveResolveAttempts.h deleted file mode 100644 index 464fff21503af2..00000000000000 --- a/src/lib/dnssd/minimal_mdns/ActiveResolveAttempts.h +++ /dev/null @@ -1,117 +0,0 @@ -/* - * - * Copyright (c) 2021 Project CHIP Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#pragma once - -#include -#include - -#include -#include -#include - -namespace mdns { -namespace Minimal { - -/// Keeps track of active resolve attempts -/// -/// Maintains a list of 'pending mdns resolve queries' and provides operations -/// for: -/// - add/remove to the list -/// - figuring out a 'next query time' for items in the list -/// - iterating through the 'schedule now' items of the list -/// -class ActiveResolveAttempts -{ -private: - struct RetryEntry - { - // What peer id is pending resolution. - // - // Inactive entries are marked by having NodeId == kUndefinedNodeId - chip::PeerId peerId; - - // First packet send is marked separately: minMDNS logic can choose - // to first send a unicast query followed by a multicast one. - bool firstSend = false; - - // When a reply is expected for this item - chip::System::Clock::Timestamp queryDueTime; - - // Next expected delay for sending if reply is not reached by - // 'queryDueTimeMs' - // - // Based on RFC 6762 expectations are: - // - the interval between the first two queries MUST be at least - // one second - // - the intervals between successive queries MUST increase by at - // least a factor of two - chip::System::Clock::Timeout nextRetryDelay = chip::System::Clock::Seconds16(1); - }; - -public: - static constexpr size_t kRetryQueueSize = 4; - static constexpr chip::System::Clock::Timeout kMaxRetryDelay = chip::System::Clock::Seconds16(16); - - struct ScheduledResolve - { - const chip::PeerId peerId; - const bool firstSend = false; - - ScheduledResolve(const RetryEntry & entry) : peerId(entry.peerId), firstSend(entry.firstSend) {} - ScheduledResolve(const chip::PeerId & peer, bool first) : peerId(peer), firstSend(first) {} - ScheduledResolve(const ScheduledResolve &) = default; - - bool operator==(const ScheduledResolve & other) const { return (peerId == other.peerId) && (firstSend == other.firstSend); } - }; - - ActiveResolveAttempts(chip::System::Clock::ClockBase * clock) : mClock(clock) { Reset(); } - - /// Clear out the internal queue - void Reset(); - - /// Mark a resolution as a success, removing it from the internal list - void Complete(const chip::PeerId & peerId); - - /// Mark that a resolution is pending, adding it to the internal list - /// - /// Once this complete, this peer id will be returned immediately - /// by NextScheduledPeer (potentially with others as well) - void MarkPending(const chip::PeerId & peerId); - - // Get minimum time until the next pending reply is required. - // - // Returns missing if no actively tracked elements exist. - chip::Optional GetTimeUntilNextExpectedResponse() const; - - // Get the peer Id that needs scheduling for a query - // - // Assumes that the resolution is being sent and will apply internal - // query logic. This means: - // - internal tracking of 'next due time' will updated as 'request sent - // now' - // - there is NO sorting implied by this call. Returned value will be - // any peer that needs a new request sent - chip::Optional NextScheduledPeer(); - -private: - chip::System::Clock::ClockBase * mClock; - RetryEntry mRetryQueue[kRetryQueueSize]; -}; - -} // namespace Minimal -} // namespace mdns diff --git a/src/lib/dnssd/minimal_mdns/BUILD.gn b/src/lib/dnssd/minimal_mdns/BUILD.gn index a9df17520598f1..6e824c5be89ad8 100644 --- a/src/lib/dnssd/minimal_mdns/BUILD.gn +++ b/src/lib/dnssd/minimal_mdns/BUILD.gn @@ -41,8 +41,6 @@ config("config") { static_library("minimal_mdns") { sources = [ - "ActiveResolveAttempts.cpp", - "ActiveResolveAttempts.h", "Parser.cpp", "Parser.h", "Query.h", diff --git a/src/lib/dnssd/minimal_mdns/tests/BUILD.gn b/src/lib/dnssd/minimal_mdns/tests/BUILD.gn index 6612cfe250e7c4..d3ee7aade5179f 100644 --- a/src/lib/dnssd/minimal_mdns/tests/BUILD.gn +++ b/src/lib/dnssd/minimal_mdns/tests/BUILD.gn @@ -22,7 +22,6 @@ chip_test_suite("tests") { output_name = "libMinimalMdnstests" test_sources = [ - "TestActiveResolveAttempts.cpp", "TestMinimalMdnsAllocator.cpp", "TestQueryReplyFilter.cpp", "TestRecordData.cpp", diff --git a/src/lib/dnssd/minimal_mdns/tests/TestActiveResolveAttempts.cpp b/src/lib/dnssd/minimal_mdns/tests/TestActiveResolveAttempts.cpp deleted file mode 100644 index 701a892eff32f5..00000000000000 --- a/src/lib/dnssd/minimal_mdns/tests/TestActiveResolveAttempts.cpp +++ /dev/null @@ -1,264 +0,0 @@ -/* - * - * Copyright (c) 2021 Project CHIP Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include - -#include - -#include - -namespace { - -using namespace chip; -using namespace chip::System::Clock::Literals; -using chip::System::Clock::Timeout; -using mdns::Minimal::ActiveResolveAttempts; - -PeerId MakePeerId(NodeId nodeId) -{ - PeerId peerId; - return peerId.SetNodeId(nodeId).SetCompressedFabricId(123); -} - -Optional ScheduledPeer(NodeId id, bool first) -{ - return Optional::Value(ActiveResolveAttempts::ScheduledResolve(MakePeerId(id), first)); -} - -void TestSinglePeerAddRemove(nlTestSuite * inSuite, void * inContext) -{ - System::Clock::Internal::MockClock mockClock; - mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); - - mockClock.AdvanceMonotonic(1234_ms32); - - // Starting up, no scheduled peers are expected - NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // Adding a single peer should result in it being scheduled - - attempts.MarkPending(MakePeerId(1)); - - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(1, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // one Next schedule is called, expect to have a delay of 1000 ms - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); - mockClock.AdvanceMonotonic(500_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(500_ms32)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // past due date: timeout should be 0 - mockClock.AdvanceMonotonic(800_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(1, false)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // one Next schedule is called, expect to have a delay of 2000 ms - // sincve the timeout doubles every time - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(2000_ms32)); - mockClock.AdvanceMonotonic(100_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1900_ms32)); - - // once complete, nothing to schedule - attempts.Complete(MakePeerId(1)); - NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); -} - -void TestRescheduleSamePeerId(nlTestSuite * inSuite, void * inContext) -{ - System::Clock::Internal::MockClock mockClock; - mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); - - mockClock.AdvanceMonotonic(112233_ms32); - - attempts.MarkPending(MakePeerId(1)); - - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(1, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // one Next schedule is called, expect to have a delay of 1000 ms - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); - - // 2nd try goes to 2 seconds (once at least 1 second passes) - mockClock.AdvanceMonotonic(1234_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(1, false)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(2000_ms32)); - - // reschedule starts fresh - attempts.MarkPending(MakePeerId(1)); - - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(1, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); -} - -void TestLRU(nlTestSuite * inSuite, void * inContext) -{ - // validates that the LRU logic is working - System::Clock::Internal::MockClock mockClock; - mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); - - mockClock.AdvanceMonotonic(334455_ms32); - - // add a single very old peer - attempts.MarkPending(MakePeerId(9999)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(9999, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - mockClock.AdvanceMonotonic(1000_ms32); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(9999, false)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - mockClock.AdvanceMonotonic(2000_ms32); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(9999, false)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // at this point, peer 9999 has a delay of 4 seconds. Fill up the rest of the table - - for (uint32_t i = 1; i < mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize; i++) - { - attempts.MarkPending(MakePeerId(i)); - mockClock.AdvanceMonotonic(1_ms32); - - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(i, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - } - - // +2 because: 1 element skipped, one element is the "current" that has a delay of 1000ms - NL_TEST_ASSERT(inSuite, - attempts.GetTimeUntilNextExpectedResponse() == - Optional::Value( - System::Clock::Milliseconds32(1000 - mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize + 2))); - - // add another element - this should overwrite peer 9999 - attempts.MarkPending(MakePeerId(mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize)); - mockClock.AdvanceMonotonic(32_s16); - - for (Optional s = attempts.NextScheduledPeer(); s.HasValue(); - s = attempts.NextScheduledPeer()) - { - NL_TEST_ASSERT(inSuite, s.Value().peerId.GetNodeId() != 9999); - } - - // Still have active pending items (queue is full) - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse().HasValue()); - - // expire all of them. Since we double timeout every expiry, we expect a - // few iteratios to be able to expire the entire queue - constexpr int kMaxIterations = 10; - - int i = 0; - for (; i < kMaxIterations; i++) - { - Optional ms = attempts.GetTimeUntilNextExpectedResponse(); - if (!ms.HasValue()) - { - break; - } - - mockClock.AdvanceMonotonic(ms.Value()); - - Optional s = attempts.NextScheduledPeer(); - while (s.HasValue()) - { - NL_TEST_ASSERT(inSuite, s.Value().peerId.GetNodeId() != 9999); - s = attempts.NextScheduledPeer(); - } - } - NL_TEST_ASSERT(inSuite, i < kMaxIterations); -} - -void TestNextPeerOrdering(nlTestSuite * inSuite, void * inContext) -{ - System::Clock::Internal::MockClock mockClock; - mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); - - mockClock.AdvanceMonotonic(123321_ms32); - - // add a single peer that will be resolved quickly - attempts.MarkPending(MakePeerId(1)); - - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(1, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); - mockClock.AdvanceMonotonic(20_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(980_ms32)); - - // expect peerid to be resolve within 1 second from now - attempts.MarkPending(MakePeerId(2)); - - // mock that we are querying 2 as well - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(2, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - mockClock.AdvanceMonotonic(80_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(900_ms32)); - - // Peer 1 is done, now peer2 should be pending (in 980ms) - attempts.Complete(MakePeerId(1)); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(920_ms32)); - mockClock.AdvanceMonotonic(20_ms32); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(900_ms32)); - - // Once peer 3 is added, queue should be - // - 900 ms until peer id 2 is pending - // - 1000 ms until peer id 3 is pending - attempts.MarkPending(MakePeerId(3)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(3, true)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(900_ms32)); - - // After the clock advance - // - 400 ms until peer id 2 is pending - // - 500 ms until peer id 3 is pending - mockClock.AdvanceMonotonic(500_ms32); - - NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(400_ms32)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); - - // advancing the clock 'too long' will return both other entries, in reverse order due to how - // the internal cache is built - mockClock.AdvanceMonotonic(500_ms32); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(3, false)); - NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == ScheduledPeer(2, false)); - NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); -} - -const nlTest sTests[] = { - NL_TEST_DEF("TestSinglePeerAddRemove", TestSinglePeerAddRemove), // - NL_TEST_DEF("TestRescheduleSamePeerId", TestRescheduleSamePeerId), // - NL_TEST_DEF("TestLRU", TestLRU), // - NL_TEST_DEF("TestNextPeerOrdering", TestNextPeerOrdering), // - NL_TEST_SENTINEL() // -}; - -} // namespace - -int TestActiveResolveAttempts(void) -{ - nlTestSuite theSuite = { "ActiveResolveAttempts", sTests, nullptr, nullptr }; - nlTestRunner(&theSuite, nullptr); - return nlTestRunnerStats(&theSuite); -} - -CHIP_REGISTER_TEST_SUITE(TestActiveResolveAttempts) diff --git a/src/lib/dnssd/tests/BUILD.gn b/src/lib/dnssd/tests/BUILD.gn index 355bea3f144ecf..57910816825001 100644 --- a/src/lib/dnssd/tests/BUILD.gn +++ b/src/lib/dnssd/tests/BUILD.gn @@ -29,6 +29,9 @@ chip_test_suite("tests") { if (chip_device_platform != "efr32") { test_sources += [ "TestDnssdCache.cpp" ] } + if (chip_mdns == "minimal") { + test_sources += [ "TestActiveResolveAttempts.cpp" ] + } cflags = [ "-Wconversion" ] diff --git a/src/lib/dnssd/tests/TestActiveResolveAttempts.cpp b/src/lib/dnssd/tests/TestActiveResolveAttempts.cpp new file mode 100644 index 00000000000000..8c819b27b8d719 --- /dev/null +++ b/src/lib/dnssd/tests/TestActiveResolveAttempts.cpp @@ -0,0 +1,405 @@ +/* + * + * Copyright (c) 2021 Project CHIP Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include + +#include + +namespace { + +using namespace chip; +using namespace chip::System::Clock::Literals; +using chip::System::Clock::Timeout; +using mdns::Minimal::ActiveResolveAttempts; + +PeerId MakePeerId(NodeId nodeId) +{ + PeerId peerId; + return peerId.SetNodeId(nodeId).SetCompressedFabricId(123); +} + +Optional ScheduledPeer(NodeId id, bool first) +{ + return Optional::Value(ActiveResolveAttempts::ScheduledAttempt(MakePeerId(id), first)); +} +Optional ScheduledBrowse(const Dnssd::DiscoveryFilter & filter, + const Dnssd::DiscoveryType type, bool first) +{ + return Optional::Value(ActiveResolveAttempts::ScheduledAttempt(filter, type, first)); +} + +void TestSinglePeerAddRemove(nlTestSuite * inSuite, void * inContext) +{ + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMonotonic(1234_ms32); + + // Starting up, no scheduled peers are expected + NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // Adding a single peer should result in it being scheduled + + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // one Next schedule is called, expect to have a delay of 1000 ms + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); + mockClock.AdvanceMonotonic(500_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(500_ms32)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // past due date: timeout should be 0 + mockClock.AdvanceMonotonic(800_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // one Next schedule is called, expect to have a delay of 2000 ms + // sincve the timeout doubles every time + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(2000_ms32)); + mockClock.AdvanceMonotonic(100_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1900_ms32)); + + // once complete, nothing to schedule + attempts.Complete(MakePeerId(1)); + NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); +} + +void TestSingleBrowseAddRemove(nlTestSuite * inSuite, void * inContext) +{ + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + Dnssd::DiscoveryFilter filter(Dnssd::DiscoveryFilterType::kLongDiscriminator, 1234); + Dnssd::DiscoveryType type = Dnssd::DiscoveryType::kCommissionableNode; + + mockClock.AdvanceMonotonic(1234_ms32); + + // Starting up, no scheduled peers are expected + NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // Adding a single attempt should result in it being scheduled + attempts.MarkPending(filter, type); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // one Next schedule is called, expect to have a delay of 1000 ms + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); + mockClock.AdvanceMonotonic(500_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(500_ms32)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // past due date: timeout should be 0 + mockClock.AdvanceMonotonic(800_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // one Next schedule is called, expect to have a delay of 2000 ms + // sincve the timeout doubles every time + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(2000_ms32)); + mockClock.AdvanceMonotonic(100_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1900_ms32)); + + // once complete, nothing to schedule + Dnssd::DiscoveredNodeData data; + data.longDiscriminator = 1234; + attempts.Complete(data); + NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); +} + +void TestRescheduleSamePeerId(nlTestSuite * inSuite, void * inContext) +{ + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMonotonic(112233_ms32); + + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // one Next schedule is called, expect to have a delay of 1000 ms + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); + + // 2nd try goes to 2 seconds (once at least 1 second passes) + mockClock.AdvanceMonotonic(1234_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(2000_ms32)); + + // reschedule starts fresh + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); +} + +void TestRescheduleSameFilter(nlTestSuite * inSuite, void * inContext) +{ + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + Dnssd::DiscoveryFilter filter(Dnssd::DiscoveryFilterType::kLongDiscriminator, 1234); + Dnssd::DiscoveryType type = Dnssd::DiscoveryType::kCommissionableNode; + + mockClock.AdvanceMonotonic(112233_ms32); + + attempts.MarkPending(filter, type); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // one Next schedule is called, expect to have a delay of 1000 ms + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); + + // 2nd try goes to 2 seconds (once at least 1 second passes) + mockClock.AdvanceMonotonic(1234_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(2000_ms32)); + + // reschedule starts fresh + attempts.MarkPending(filter, type); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); +} + +void TestLRU(nlTestSuite * inSuite, void * inContext) +{ + // validates that the LRU logic is working + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMonotonic(334455_ms32); + + // add a single very old peer + attempts.MarkPending(MakePeerId(9999)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(9999, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + mockClock.AdvanceMonotonic(1000_ms32); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(9999, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + mockClock.AdvanceMonotonic(2000_ms32); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(9999, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // at this point, peer 9999 has a delay of 4 seconds. Fill up the rest of the table + + for (uint32_t i = 1; i < mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize; i++) + { + attempts.MarkPending(MakePeerId(i)); + mockClock.AdvanceMonotonic(1_ms32); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(i, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + } + + // +2 because: 1 element skipped, one element is the "current" that has a delay of 1000ms + NL_TEST_ASSERT(inSuite, + attempts.GetTimeUntilNextExpectedResponse() == + Optional::Value( + System::Clock::Milliseconds32(1000 - mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize + 2))); + + // add another element - this should overwrite peer 9999 + attempts.MarkPending(MakePeerId(mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize)); + mockClock.AdvanceMonotonic(32_s16); + + for (Optional s = attempts.NextScheduled(); s.HasValue(); s = attempts.NextScheduled()) + { + NL_TEST_ASSERT(inSuite, s.Value().peerId.GetNodeId() != 9999); + } + + // Still have active pending items (queue is full) + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse().HasValue()); + + // expire all of them. Since we double timeout every expiry, we expect a + // few iteratios to be able to expire the entire queue + constexpr int kMaxIterations = 10; + + int i = 0; + for (; i < kMaxIterations; i++) + { + Optional ms = attempts.GetTimeUntilNextExpectedResponse(); + if (!ms.HasValue()) + { + break; + } + + mockClock.AdvanceMonotonic(ms.Value()); + + Optional s = attempts.NextScheduled(); + while (s.HasValue()) + { + NL_TEST_ASSERT(inSuite, s.Value().peerId.GetNodeId() != 9999); + s = attempts.NextScheduled(); + } + } + NL_TEST_ASSERT(inSuite, i < kMaxIterations); +} + +void TestNextPeerOrdering(nlTestSuite * inSuite, void * inContext) +{ + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMonotonic(123321_ms32); + + // add a single peer that will be resolved quickly + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); + mockClock.AdvanceMonotonic(20_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(980_ms32)); + + // expect peerid to be resolve within 1 second from now + attempts.MarkPending(MakePeerId(2)); + + // mock that we are querying 2 as well + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(2, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + mockClock.AdvanceMonotonic(80_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(900_ms32)); + + // Peer 1 is done, now peer2 should be pending (in 980ms) + attempts.Complete(MakePeerId(1)); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(920_ms32)); + mockClock.AdvanceMonotonic(20_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(900_ms32)); + + // Once peer 3 is added, queue should be + // - 900 ms until peer id 2 is pending + // - 1000 ms until peer id 3 is pending + attempts.MarkPending(MakePeerId(3)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(3, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(900_ms32)); + + // After the clock advance + // - 400 ms until peer id 2 is pending + // - 500 ms until peer id 3 is pending + mockClock.AdvanceMonotonic(500_ms32); + + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(400_ms32)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // advancing the clock 'too long' will return both other entries, in reverse order due to how + // the internal cache is built + mockClock.AdvanceMonotonic(500_ms32); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(3, false)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(2, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); +} + +void TestCombination(nlTestSuite * inSuite, void * inContext) +{ + System::Clock::Internal::MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + Dnssd::DiscoveryFilter filter(Dnssd::DiscoveryFilterType::kLongDiscriminator, 1234); + Dnssd::DiscoveryType type = Dnssd::DiscoveryType::kCommissionableNode; + + // Schedule a Resolve + attempts.MarkPending(MakePeerId(1)); + // 20ms later, schedule a browse + mockClock.AdvanceMonotonic(20_ms32); + attempts.MarkPending(filter, type); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, true)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // At this point, both should reset, so we're back to 1000ms + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(1000_ms32)); + + // We used 20 ms, so the next time for the peer and resolve should be 980 ms + mockClock.AdvanceMonotonic(20_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(980_ms32)); + + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // Add a second Peer + mockClock.AdvanceMonotonic(20_ms32); + attempts.MarkPending(MakePeerId(2)); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(2, true)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // Advance to the retry time of peer 1 and the resolve + mockClock.AdvanceMonotonic(960_ms32); + NL_TEST_ASSERT(inSuite, attempts.GetTimeUntilNextExpectedResponse() == Optional(0_ms32)); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledPeer(1, false)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduled() == ScheduledBrowse(filter, type, false)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); + + // Complete all, we should see no more scheduled. + attempts.Complete(MakePeerId(2)); + attempts.Complete(MakePeerId(1)); + Dnssd::DiscoveredNodeData data; + data.longDiscriminator = 1234; + attempts.Complete(data); + + NL_TEST_ASSERT(inSuite, !attempts.GetTimeUntilNextExpectedResponse().HasValue()); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduled().HasValue()); +} + +const nlTest sTests[] = { + NL_TEST_DEF("TestSinglePeerAddRemove", TestSinglePeerAddRemove), // + NL_TEST_DEF("TestSingleBrowseAddRemove", TestSingleBrowseAddRemove), // + NL_TEST_DEF("TestRescheduleSamePeerId", TestRescheduleSamePeerId), // + NL_TEST_DEF("TestRescheduleSameFilter", TestRescheduleSameFilter), // + NL_TEST_DEF("TestLRU", TestLRU), // + NL_TEST_DEF("TestNextPeerOrdering", TestNextPeerOrdering), // + NL_TEST_DEF("TestCombination", TestCombination), // + NL_TEST_SENTINEL() // +}; + +} // namespace + +int TestActiveResolveAttempts(void) +{ + nlTestSuite theSuite = { "ActiveResolveAttempts", sTests, nullptr, nullptr }; + nlTestRunner(&theSuite, nullptr); + return nlTestRunnerStats(&theSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestActiveResolveAttempts)