diff --git a/src/lib/mdns/Resolver_ImplMinimalMdns.cpp b/src/lib/mdns/Resolver_ImplMinimalMdns.cpp index 599cb9269fe4e3..9f4fa1632e4c9f 100644 --- a/src/lib/mdns/Resolver_ImplMinimalMdns.cpp +++ b/src/lib/mdns/Resolver_ImplMinimalMdns.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -82,9 +83,10 @@ class PacketDataReporter : public ParserDelegate void OnHeader(ConstHeaderRef & header) override; void OnQuery(const QueryData & data) override; void OnResource(ResourceType type, const ResourceData & data) override; + // Called after ParsePacket is complete to send final notifications to the delegate. // Used to ensure all the available IP addresses are attached before completion. - void OnComplete(); + void OnComplete(ActiveResolveAttempts & activeAttempts); private: ResolverDelegate * mDelegate = nullptr; @@ -311,7 +313,7 @@ void PacketDataReporter::OnResource(ResourceType type, const ResourceData & data } } -void PacketDataReporter::OnComplete() +void PacketDataReporter::OnComplete(ActiveResolveAttempts & activeAttempts) { if ((mDiscoveryType == DiscoveryType::kCommissionableNode || mDiscoveryType == DiscoveryType::kCommissionerNode) && mDiscoveredNodeData.IsValid()) @@ -320,6 +322,8 @@ void PacketDataReporter::OnComplete() } else if (mDiscoveryType == DiscoveryType::kOperational && mHasIP && mHasNodePort) { + activeAttempts.Complete(mNodeData.mPeerId); + mNodeData.LogNodeIdResolved(); mDelegate->OnNodeIdResolved(mNodeData); } @@ -328,7 +332,10 @@ void PacketDataReporter::OnComplete() class MinMdnsResolver : public Resolver, public MdnsPacketDelegate { public: - MinMdnsResolver() { GlobalMinimalMdnsServer::Instance().SetResponseDelegate(this); } + MinMdnsResolver() : mActiveResolves(chip::System::Internal::gClockBase) + { + GlobalMinimalMdnsServer::Instance().SetResponseDelegate(this); + } //// MdnsPacketDelegate implementation void OnMdnsPacketData(const BytesRange & data, const chip::Inet::IPPacketInfo * info) override; @@ -344,6 +351,13 @@ class MinMdnsResolver : public Resolver, public MdnsPacketDelegate private: ResolverDelegate * mDelegate = nullptr; DiscoveryType mDiscoveryType = DiscoveryType::kUnknown; + System::Layer * mSystemLayer = nullptr; + ActiveResolveAttempts mActiveResolves; + + CHIP_ERROR SendPendingResolveQueries(); + CHIP_ERROR ScheduleResolveRetries(); + + static void ResolveRetryCallback(System::Layer *, void * self); CHIP_ERROR SendQuery(mdns::Minimal::FullQName qname, mdns::Minimal::QType type); CHIP_ERROR BrowseNodes(DiscoveryType type, DiscoveryFilter subtype); @@ -379,7 +393,8 @@ void MinMdnsResolver::OnMdnsPacketData(const BytesRange & data, const chip::Inet } else { - reporter.OnComplete(); + reporter.OnComplete(mActiveResolves); + ScheduleResolveRetries(); } } @@ -392,6 +407,8 @@ CHIP_ERROR MinMdnsResolver::StartResolver(chip::Inet::InetLayer * inetLayer, uin return CHIP_NO_ERROR; } + mSystemLayer = inetLayer->SystemLayer(); + return GlobalMinimalMdnsServer::Instance().StartServer(inetLayer, port); } @@ -491,47 +508,86 @@ CHIP_ERROR MinMdnsResolver::BrowseNodes(DiscoveryType type, DiscoveryFilter filt CHIP_ERROR MinMdnsResolver::ResolveNodeId(const PeerId & peerId, Inet::IPAddressType type) { - mDiscoveryType = DiscoveryType::kOperational; - System::PacketBufferHandle buffer = System::PacketBufferHandle::New(kMdnsMaxPacketSize); - ReturnErrorCodeIf(buffer.IsNull(), CHIP_ERROR_NO_MEMORY); + mDiscoveryType = DiscoveryType::kOperational; + mActiveResolves.MarkPending(peerId); - QueryBuilder builder(std::move(buffer)); - builder.Header().SetMessageId(0); + return SendPendingResolveQueries(); +} + +CHIP_ERROR MinMdnsResolver::ScheduleResolveRetries() +{ + ReturnErrorCodeIf(mSystemLayer == nullptr, CHIP_ERROR_INCORRECT_STATE); + mSystemLayer->CancelTimer(&ResolveRetryCallback, this); + Optional delayMs = mActiveResolves.GetMsUntilNextExpectedResponse(); + + if (!delayMs.HasValue()) { - char nameBuffer[64] = ""; - - // Node and fabricid are encoded in server names. - ReturnErrorOnFailure(MakeInstanceName(nameBuffer, sizeof(nameBuffer), peerId)); - - const char * instanceQName[] = { nameBuffer, kOperationalServiceName, kOperationalProtocol, kLocalDomain }; - Query query(instanceQName); - - query - .SetClass(QClass::IN) // - .SetType(QType::ANY) // - .SetAnswerViaUnicast(false) // - ; - - // NOTE: type above is NOT A or AAAA because the name searched for is - // a SRV record. The layout is: - // SRV -> hostname - // Hostname -> A - // Hostname -> AAAA - // - // Query is sent for ANY and expectation is to receive A/AAAA records - // in the additional section of the reply. - // - // Sending a A/AAAA query will return no results - // Sending a SRV query will return the srv only and an additional query - // would be needed to resolve the host name to an IP address - - builder.AddQuery(query); + return CHIP_NO_ERROR; } - ReturnErrorCodeIf(!builder.Ok(), CHIP_ERROR_INTERNAL); + return mSystemLayer->StartTimer(delayMs.Value(), &ResolveRetryCallback, this); +} - return GlobalMinimalMdnsServer::Server().BroadcastSend(builder.ReleasePacket(), kMdnsPort); +void MinMdnsResolver::ResolveRetryCallback(System::Layer *, void * self) +{ + reinterpret_cast(self)->SendPendingResolveQueries(); +} + +CHIP_ERROR MinMdnsResolver::SendPendingResolveQueries() +{ + while (true) + { + Optional peerId = mActiveResolves.NextScheduledPeer(); + + if (!peerId.HasValue()) + { + break; + } + + System::PacketBufferHandle buffer = System::PacketBufferHandle::New(kMdnsMaxPacketSize); + ReturnErrorCodeIf(buffer.IsNull(), CHIP_ERROR_NO_MEMORY); + + QueryBuilder builder(std::move(buffer)); + builder.Header().SetMessageId(0); + + { + char nameBuffer[kMaxOperationalServiceNameSize] = ""; + + // Node and fabricid are encoded in server names. + ReturnErrorOnFailure(MakeInstanceName(nameBuffer, sizeof(nameBuffer), peerId.Value())); + + const char * instanceQName[] = { nameBuffer, kOperationalServiceName, kOperationalProtocol, kLocalDomain }; + Query query(instanceQName); + + query + .SetClass(QClass::IN) // + .SetType(QType::ANY) // + .SetAnswerViaUnicast(false) // + ; + + // NOTE: type above is NOT A or AAAA because the name searched for is + // a SRV record. The layout is: + // SRV -> hostname + // Hostname -> A + // Hostname -> AAAA + // + // Query is sent for ANY and expectation is to receive A/AAAA records + // in the additional section of the reply. + // + // Sending a A/AAAA query will return no results + // Sending a SRV query will return the srv only and an additional query + // would be needed to resolve the host name to an IP address + + builder.AddQuery(query); + } + + ReturnErrorCodeIf(!builder.Ok(), CHIP_ERROR_INTERNAL); + + ReturnErrorOnFailure(GlobalMinimalMdnsServer::Server().BroadcastSend(builder.ReleasePacket(), kMdnsPort)); + } + + return ScheduleResolveRetries(); } MinMdnsResolver gResolver; diff --git a/src/lib/mdns/minimal/ActiveResolveAttempts.cpp b/src/lib/mdns/minimal/ActiveResolveAttempts.cpp new file mode 100644 index 00000000000000..1cec24734f6fb5 --- /dev/null +++ b/src/lib/mdns/minimal/ActiveResolveAttempts.cpp @@ -0,0 +1,187 @@ +/* + * + * 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 "ActiveResolveAttempts.h" + +#include + +#include + +using namespace chip; + +namespace mdns { +namespace Minimal { + +void ActiveResolveAttempts::Reset() + +{ + for (auto & item : mRetryQueue) + { + item.peerId.SetNodeId(kUndefinedNodeId); + } +} + +void ActiveResolveAttempts::Complete(const PeerId & peerId) +{ + for (auto & item : mRetryQueue) + { + if (item.peerId == peerId) + { + item.peerId.SetNodeId(kUndefinedNodeId); + return; + } + } + + // This may happen during boot time adverisements: nodes come online + // and advertise their IP without any explicit queries for them + ChipLogProgress(Discovery, "Discovered node without a pending query"); +} + +void ActiveResolveAttempts::MarkPending(const PeerId & peerId) +{ + // Strategy when picking the peer id to use: + // 1 if a matching peer id is already found, use that one + // 2 if an 'unused' entry is found, use that + // 3 otherwise expire the one with the largest nextRetryDelaySec + // or if equal nextRetryDelaySec, pick the one with the oldest + // queryDueTimeMs + + RetryEntry * entryToUse = &mRetryQueue[0]; + + for (size_t i = 1; i < kRetryQueueSize; i++) + { + if (entryToUse->peerId == peerId) + { + break; // best match possible + } + + RetryEntry * entry = mRetryQueue + i; + + // Rule 1: peer id match always matches + if (entry->peerId == peerId) + { + entryToUse = entry; + continue; + } + + // Rule 2: select unused entries + if ((entryToUse->peerId.GetNodeId() != kUndefinedNodeId) && (entry->peerId.GetNodeId() == kUndefinedNodeId)) + { + entryToUse = entry; + continue; + } + else if (entryToUse->peerId.GetNodeId() == kUndefinedNodeId) + { + continue; + } + + // Rule 3: both choices are used (have a defined node id): + // - try to find the one with the largest next delay (oldest request) + // - on same delay, use queryDueTime to determine the oldest request + // (the one with the smallest due time was issued the longest time + // ago) + if (entry->nextRetryDelaySec > entryToUse->nextRetryDelaySec) + { + entryToUse = entry; + } + else if ((entry->nextRetryDelaySec == entryToUse->nextRetryDelaySec) && + (entry->queryDueTimeMs < entryToUse->queryDueTimeMs)) + { + entryToUse = entry; + } + } + + if ((entryToUse->peerId.GetNodeId() != kUndefinedNodeId) && (entryToUse->peerId != peerId)) + { + // TODO: node was evicted here, if/when resolution failures are + // supported this could be a place for error callbacks + // + // Note however that this is NOT an actual 'timeout' it is showing + // a burst of lookups for which we cannot maintain state. A reply may + // still be received for this peer id (query was already sent on the + // network) + ChipLogError(Discovery, "Re-using pending resolve entry before reply was received."); + } + + entryToUse->peerId = peerId; + entryToUse->queryDueTimeMs = mClock->GetMonotonicMilliseconds(); + entryToUse->nextRetryDelaySec = 1; +} + +Optional ActiveResolveAttempts::GetMsUntilNextExpectedResponse() const +{ + Optional minDelay = Optional::Missing(); + + chip::System::Clock::MonotonicMilliseconds nowMs = mClock->GetMonotonicMilliseconds(); + + for (auto & entry : mRetryQueue) + { + if (entry.peerId.GetNodeId() == kUndefinedNodeId) + { + continue; + } + + if (nowMs >= entry.queryDueTimeMs) + { + // found an entry that needs processing right now + return Optional::Value(0); + } + + uint32_t entryDelay = static_cast(entry.queryDueTimeMs - nowMs); + if (!minDelay.HasValue() || (minDelay.Value() > entryDelay)) + { + minDelay.SetValue(entryDelay); + } + } + + return minDelay; +} + +Optional ActiveResolveAttempts::NextScheduledPeer() +{ + chip::System::Clock::MonotonicMilliseconds nowMs = mClock->GetMonotonicMilliseconds(); + + for (auto & entry : mRetryQueue) + { + if (entry.peerId.GetNodeId() == kUndefinedNodeId) + { + continue; // not a pending item + } + + if (entry.queryDueTimeMs > nowMs) + { + continue; // not yet due + } + + if (entry.nextRetryDelaySec > kMaxRetryDelaySec) + { + ChipLogError(Discovery, "Timeout waiting for mDNS resolution."); + entry.peerId.SetNodeId(kUndefinedNodeId); + continue; + } + + entry.queryDueTimeMs = nowMs + entry.nextRetryDelaySec * 1000L; + entry.nextRetryDelaySec *= 2; + + return Optional::Value(entry.peerId); + } + + return Optional::Missing(); +} + +} // namespace Minimal +} // namespace mdns diff --git a/src/lib/mdns/minimal/ActiveResolveAttempts.h b/src/lib/mdns/minimal/ActiveResolveAttempts.h new file mode 100644 index 00000000000000..9f05c7b9dc8148 --- /dev/null +++ b/src/lib/mdns/minimal/ActiveResolveAttempts.h @@ -0,0 +1,100 @@ +/* + * + * 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 +{ +public: + static constexpr size_t kRetryQueueSize = 4; + static constexpr uint32_t kMaxRetryDelaySec = 16; + + ActiveResolveAttempts(chip::System::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 milliseconds until the next pending reply is required. + // + // Returns missing if no actively tracked elements exist. + chip::Optional GetMsUntilNextExpectedResponse() 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: + struct RetryEntry + { + // What peer id is pending resolution. + // + // Inactive entries are marked by having NodeId == kUndefinedNodeId + chip::PeerId peerId; + + // When a reply is expected for this item + chip::System::Clock::MonotonicMilliseconds queryDueTimeMs; + + // 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 + uint32_t nextRetryDelaySec = 1; + }; + + chip::System::ClockBase * mClock; + RetryEntry mRetryQueue[kRetryQueueSize]; +}; + +} // namespace Minimal +} // namespace mdns diff --git a/src/lib/mdns/minimal/BUILD.gn b/src/lib/mdns/minimal/BUILD.gn index b26102aacc5978..815e8c548eaf6d 100644 --- a/src/lib/mdns/minimal/BUILD.gn +++ b/src/lib/mdns/minimal/BUILD.gn @@ -16,6 +16,8 @@ import("//build_overrides/chip.gni") static_library("minimal") { sources = [ + "ActiveResolveAttempts.cpp", + "ActiveResolveAttempts.h", "Parser.cpp", "Parser.h", "Query.h", diff --git a/src/lib/mdns/minimal/tests/BUILD.gn b/src/lib/mdns/minimal/tests/BUILD.gn index 62d7562c9e6ff2..2d037e917646fd 100644 --- a/src/lib/mdns/minimal/tests/BUILD.gn +++ b/src/lib/mdns/minimal/tests/BUILD.gn @@ -22,6 +22,7 @@ chip_test_suite("tests") { output_name = "libMinimalMdnstests" test_sources = [ + "TestActiveResolveAttempts.cpp", "TestMinimalMdnsAllocator.cpp", "TestQueryReplyFilter.cpp", "TestRecordData.cpp", diff --git a/src/lib/mdns/minimal/tests/TestActiveResolveAttempts.cpp b/src/lib/mdns/minimal/tests/TestActiveResolveAttempts.cpp new file mode 100644 index 00000000000000..65f04e07cd0f7f --- /dev/null +++ b/src/lib/mdns/minimal/tests/TestActiveResolveAttempts.cpp @@ -0,0 +1,268 @@ +/* + * + * 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; + +class MockClock : public System::ClockBase +{ +public: + MonotonicMicroseconds GetMonotonicMicroseconds() override { return mUsec; } + MonotonicMilliseconds GetMonotonicMilliseconds() override { return mUsec / 1000; } + + void AdvanceMs(MonotonicMilliseconds ms) { mUsec += ms * 1000L; } + void AdvanceSec(uint32_t s) { AdvanceMs(s * 1000); } + +private: + MonotonicMicroseconds mUsec = 0; +}; + +PeerId MakePeerId(NodeId nodeId) +{ + PeerId peerId; + return peerId.SetNodeId(nodeId).SetCompressedFabricId(123); +} + +void TestSinglePeerAddRemove(nlTestSuite * inSuite, void * inContext) +{ + MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMs(1234); + + // Starting up, no scheduled peers are expected + NL_TEST_ASSERT(inSuite, !attempts.GetMsUntilNextExpectedResponse().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.GetMsUntilNextExpectedResponse() == Optional::Value(0u)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(1))); + 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.GetMsUntilNextExpectedResponse() == Optional::Value(1000u)); + mockClock.AdvanceMs(500); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(500u)); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + + // past due date: timeout should be 0 + mockClock.AdvanceMs(800); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(0u)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(1))); + 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.GetMsUntilNextExpectedResponse() == Optional::Value(2000u)); + mockClock.AdvanceMs(100); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(1900u)); + + // once complete, nothing to schedule + attempts.Complete(MakePeerId(1)); + NL_TEST_ASSERT(inSuite, !attempts.GetMsUntilNextExpectedResponse().HasValue()); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); +} + +void TestRescheduleSamePeerId(nlTestSuite * inSuite, void * inContext) +{ + MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMs(112233); + + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(0u)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(1))); + 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.GetMsUntilNextExpectedResponse() == Optional::Value(1000u)); + + // 2nd try goes to 2 seconds (once at least 1 second passes) + mockClock.AdvanceMs(1234); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(0u)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(1))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(2000u)); + + // reschedule starts fresh + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(0u)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(1))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(1000u)); +} + +void TestLRU(nlTestSuite * inSuite, void * inContext) +{ + // validates that the LRU logic is working + MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMs(334455); + + // add a single very old peer + attempts.MarkPending(MakePeerId(9999)); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(9999))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + + mockClock.AdvanceMs(1000); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(9999))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + + mockClock.AdvanceMs(2000); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(9999))); + 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.AdvanceMs(1); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(i))); + 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.GetMsUntilNextExpectedResponse() == + Optional::Value(static_cast(1000 - mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize + 2))); + + // add another element - this should overwrite peer 9999 + attempts.MarkPending(MakePeerId(mdns::Minimal::ActiveResolveAttempts::kRetryQueueSize)); + mockClock.AdvanceSec(32); + + for (Optional peerId = attempts.NextScheduledPeer(); peerId.HasValue(); peerId = attempts.NextScheduledPeer()) + { + NL_TEST_ASSERT(inSuite, peerId.Value().GetNodeId() != 9999); + } + + // Still have active pending items (queue is full) + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse().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.GetMsUntilNextExpectedResponse(); + if (!ms.HasValue()) + { + break; + } + + mockClock.AdvanceMs(ms.Value()); + + Optional peerId = attempts.NextScheduledPeer(); + while (peerId.HasValue()) + { + NL_TEST_ASSERT(inSuite, peerId.Value().GetNodeId() != 9999); + peerId = attempts.NextScheduledPeer(); + } + } + NL_TEST_ASSERT(inSuite, i < kMaxIterations); +} + +void TestNextPeerOrdering(nlTestSuite * inSuite, void * inContext) +{ + MockClock mockClock; + mdns::Minimal::ActiveResolveAttempts attempts(&mockClock); + + mockClock.AdvanceMs(123321); + + // add a single peer that will be resolved quickly + attempts.MarkPending(MakePeerId(1)); + + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(1))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(1000u)); + mockClock.AdvanceMs(20); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(980u)); + + // 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() == Optional::Value(MakePeerId(2))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + mockClock.AdvanceMs(80); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(900u)); + + // Peer 1 is done, now peer2 should be pending (in 980ms) + attempts.Complete(MakePeerId(1)); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(920u)); + mockClock.AdvanceMs(20); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(900u)); + + // 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() == Optional::Value(MakePeerId(3))); + NL_TEST_ASSERT(inSuite, !attempts.NextScheduledPeer().HasValue()); + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(900u)); + + // After the clock advance + // - 400 ms until peer id 2 is pending + // - 500 ms until peer id 3 is pending + mockClock.AdvanceMs(500); + + NL_TEST_ASSERT(inSuite, attempts.GetMsUntilNextExpectedResponse() == Optional::Value(400u)); + 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.AdvanceMs(500); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(3))); + NL_TEST_ASSERT(inSuite, attempts.NextScheduledPeer() == Optional::Value(MakePeerId(2))); + 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)