From 0d40f2fe290eea7d4c0bb4855d5cd4c761340ea0 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Fri, 2 Oct 2026 17:08:49 -0700 Subject: [PATCH] [netdata-publisher] coalesce scheduled entry update times (#13660) This commit updates `NetworkData::Publisher` to align the scheduled update times (`mUpdateTime`) of multiple pending entry additions or removals when they fall within the same valid delay window and are close in time (within `kMaxThresholdToCoalesce`). When an entry transitions to `kAdding` or `kRemoving`, it first selects a random update time within its allowed `[minDelay, maxDelay]` range and then uses `UpdateTimeCoalescer` to check if any other entry is already scheduled within that range and within the threshold. Aligning the update times allows multiple entry updates on the same device to be processed together and sent in a single `SVR_DATA.ntf` message to the Leader. This commit also adds the configuration option (enabled by default) `OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE` to control this feature. --- src/core/config/netdata_publisher.h | 12 +++ src/core/thread/network_data_publisher.cpp | 111 +++++++++++++++++++-- src/core/thread/network_data_publisher.hpp | 27 +++++ tests/nexus/openthread-core-nexus-config.h | 1 + tests/nexus/test_netdata_publisher.cpp | 84 ++++++++++++++++ 5 files changed, 226 insertions(+), 9 deletions(-) diff --git a/src/core/config/netdata_publisher.h b/src/core/config/netdata_publisher.h index c3a088e4b..e9719c7f9 100644 --- a/src/core/config/netdata_publisher.h +++ b/src/core/config/netdata_publisher.h @@ -61,6 +61,18 @@ (OPENTHREAD_CONFIG_SRP_SERVER_ENABLE || OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE) #endif +/** + * @def OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + * + * Define to 1 to enable coalescing scheduled entry updates in Network Data Publisher. + * + * When enabled, Publisher attempts to align the random delay times of multiple pending entry additions or removals + * so they can be performed together, reducing the number of Server Data Notification messages sent to the Leader. + */ +#ifndef OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE +#define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE 1 +#endif + /** * @def OPENTHREAD_CONFIG_NETDATA_PUBLISHER_MAX_DELAY_TO_ADD * diff --git a/src/core/thread/network_data_publisher.cpp b/src/core/thread/network_data_publisher.cpp index 35b755442..24b78c959 100644 --- a/src/core/thread/network_data_publisher.cpp +++ b/src/core/thread/network_data_publisher.cpp @@ -253,6 +253,26 @@ void Publisher::HandleTimer(void) #endif } +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + +void Publisher::CoalesceUpdateTime(UpdateTimeCoalescer &aCoalescer) const +{ +#if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE + aCoalescer.Evaluate(mDnsSrpServiceEntry); +#if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE + aCoalescer.Evaluate(mBorderAdmitterEntry); +#endif +#endif + +#if OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE + for (const PrefixEntry &entry : mPrefixEntries) + { + aCoalescer.Evaluate(entry); + } +#endif +} +#endif // OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + //--------------------------------------------------------------------------------------------------------------------- // Publisher::Entry @@ -305,7 +325,7 @@ void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEnt if (aNumEntries < aDesiredNumEntries) { - mUpdateTime = now + Random::NonCrypto::GenerateInClosedRange(1, kMaxDelayToAdd); + SelectUpdateTime(now, 1, kMaxDelayToAdd); SetState(kAdding); Get().GetTimer().FireAtIfEarlier(mUpdateTime); LogUpdateTime(now); @@ -341,16 +361,15 @@ void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEnt if (aNumEntries > aDesiredNumEntries) { - mUpdateTime = now; - - if (aDesiredNumEntries > 0) + if (aDesiredNumEntries == 0) { - mUpdateTime += Random::NonCrypto::GenerateInClosedRange(1, kMaxDelayToRemove); + mUpdateTime = now; + } + else + { + uint32_t extraDelay = (aNumPreferredEntries < aDesiredNumEntries) ? kExtraDelayToRemovePreferred : 0; - if (aNumPreferredEntries < aDesiredNumEntries) - { - mUpdateTime += kExtraDelayToRemovePreferred; - } + SelectUpdateTime(now, 1 + extraDelay, kMaxDelayToRemove + extraDelay); } SetState(kRemoving); @@ -380,6 +399,20 @@ void Publisher::Entry::UpdateState(uint8_t aNumEntries, uint8_t aNumPreferredEnt } } +void Publisher::Entry::SelectUpdateTime(TimeMilli aNow, uint32_t aMinDelay, uint32_t aMaxDelay) +{ + mUpdateTime = aNow + Random::NonCrypto::GenerateInClosedRange(aMinDelay, aMaxDelay); + +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + { + UpdateTimeCoalescer coalescer(aNow + aMinDelay, aNow + aMaxDelay, mUpdateTime); + + Get().CoalesceUpdateTime(coalescer); + mUpdateTime = coalescer.GetCoalescedTime(); + } +#endif +} + void Publisher::Entry::HandleTimer(void) { // Timer is used to delay adding/removing the entry. If we have @@ -1198,6 +1231,66 @@ exit: #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + +//--------------------------------------------------------------------------------------------------------------------- +// Publisher::UpdateTimeCoalescer + +Publisher::UpdateTimeCoalescer::UpdateTimeCoalescer(TimeMilli aMinTime, TimeMilli aMaxTime, TimeMilli aUpdateTime) + : mMinTime(aMinTime) + , mMaxTime(aMaxTime) + , mUpdateTime(aUpdateTime) + , mCoalescedTime(aUpdateTime) + , mMinDiff(kMaxThresholdToCoalesce) +{ +} + +void Publisher::UpdateTimeCoalescer::Evaluate(const Entry &aEntry) +{ + // Checks whether `aEntry` is already scheduled to be updated + // within `[mMinTime, mMaxTime]` and is within + // `kMaxThresholdToCoalesce` of the randomly selected `mUpdateTime`. + // If so, `mCoalescedTime` is updated to align with the closest + // such entry so that multiple updates can be sent together to the + // Leader. + // + // For `kAdding`, the `[mMinTime, mMaxTime]` range width is + // smaller than `kMaxThresholdToCoalesce` (`kMaxDelayToAdd`), so + // any scheduled entry within that range is always matched. For + // `kRemoving`, restricting to `[mMinTime, mMaxTime]` ensures that + // preferred and non-preferred removal time windows never cross + // into each other. + + uint32_t diff; + + VerifyOrExit((aEntry.GetState() == Entry::kAdding) || (aEntry.GetState() == Entry::kRemoving)); + + VerifyOrExit(IsValueInRange(aEntry.GetUpdateTime(), mMinTime, mMaxTime)); + + if (aEntry.GetUpdateTime() > mUpdateTime) + { + diff = aEntry.GetUpdateTime() - mUpdateTime; + VerifyOrExit(diff < mMinDiff); + } + else + { + // Use `<=` here (versus `<` above) so that if two entries are + // equidistant from `mUpdateTime`, we prefer the earlier update + // time over delaying further. + + diff = mUpdateTime - aEntry.GetUpdateTime(); + VerifyOrExit(diff <= mMinDiff); + } + + mMinDiff = diff; + mCoalescedTime = aEntry.GetUpdateTime(); + +exit: + return; +} + +#endif // OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + } // namespace NetworkData } // namespace ot diff --git a/src/core/thread/network_data_publisher.hpp b/src/core/thread/network_data_publisher.hpp index d6eed6c49..9f09601d2 100644 --- a/src/core/thread/network_data_publisher.hpp +++ b/src/core/thread/network_data_publisher.hpp @@ -337,8 +337,12 @@ public: #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE private: + class UpdateTimeCoalescer; + class Entry : public InstanceLocatorInit { + friend class UpdateTimeCoalescer; + protected: enum State : uint8_t { @@ -379,6 +383,7 @@ private: private: void Add(void); void Remove(State aNextState); + void SelectUpdateTime(TimeMilli aNow, uint32_t aMinDelay, uint32_t aMaxDelay); void LogUpdateTime(TimeMilli aNow) const; static const char *StateToString(State aState); @@ -541,6 +546,25 @@ private: }; #endif // OPENTHREAD_CONFIG_BORDER_ROUTER_ENABLE +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + class UpdateTimeCoalescer + { + public: + UpdateTimeCoalescer(TimeMilli aMinTime, TimeMilli aMaxTime, TimeMilli aUpdateTime); + void Evaluate(const Entry &aEntry); + TimeMilli GetCoalescedTime(void) const { return mCoalescedTime; } + + private: + static constexpr uint32_t kMaxThresholdToCoalesce = Entry::kMaxDelayToAdd; + + TimeMilli mMinTime; + TimeMilli mMaxTime; + TimeMilli mUpdateTime; + TimeMilli mCoalescedTime; + uint32_t mMinDiff; + }; +#endif + #if OPENTHREAD_CONFIG_TMF_NETDATA_SERVICE_ENABLE bool IsADnsSrpServiceEntry(const Entry &aEntry) const { return (&aEntry == &mDnsSrpServiceEntry); } #if OPENTHREAD_CONFIG_BORDER_AGENT_ENABLE && OPENTHREAD_CONFIG_BORDER_AGENT_ADMITTER_ENABLE @@ -559,6 +583,9 @@ private: TimerMilli &GetTimer(void) { return mTimer; } void HandleNotifierEvents(Events aEvents); void HandleTimer(void); +#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE + void CoalesceUpdateTime(UpdateTimeCoalescer &aCoalescer) const; +#endif using PublisherTimer = TimerMilliIn; diff --git a/tests/nexus/openthread-core-nexus-config.h b/tests/nexus/openthread-core-nexus-config.h index 0de1947e4..fc993add7 100644 --- a/tests/nexus/openthread-core-nexus-config.h +++ b/tests/nexus/openthread-core-nexus-config.h @@ -131,6 +131,7 @@ #define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_SW_VERSION "OT-simul-nexus" #define OPENTHREAD_CONFIG_NET_DIAG_VENDOR_OUI 0x020100 #define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_ENABLE 1 +#define OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE 1 #define OPENTHREAD_CONFIG_NUM_MESSAGE_BUFFERS 256 #define OPENTHREAD_CONFIG_PARENT_SEARCH_BACKOFF_INTERVAL (10 * 60) #define OPENTHREAD_CONFIG_PLATFORM_DNS_ENABLE 1 diff --git a/tests/nexus/test_netdata_publisher.cpp b/tests/nexus/test_netdata_publisher.cpp index 536dcc25e..a3dfd8659 100644 --- a/tests/nexus/test_netdata_publisher.cpp +++ b/tests/nexus/test_netdata_publisher.cpp @@ -915,12 +915,96 @@ void Test_NetDataPublisher(void) Log("All steps completed."); } +void Test_NetDataPublisherCoalesceUpdates(void) +{ + static constexpr uint8_t kNumPrefixes = 3; + + static const char *const kPrefixStrings[kNumPrefixes] = { + "fd00:1111::/64", + "fd00:2222::/64", + "fd00:3333::/64", + }; + + Core nexus; + Node &leader = nexus.CreateNode(); + Node &router = nexus.CreateNode(); + Ip6::Prefix prefixes[kNumPrefixes]; + NetworkData::OnMeshPrefixConfig prefixConfig; + uint8_t initVersion; + + Log("======================================================================================="); + Log("Test_NetDataPublisherCoalesceUpdates"); + + leader.SetName("LEADER"); + router.SetName("ROUTER1"); + + for (uint8_t i = 0; i < kNumPrefixes; i++) + { + SuccessOrQuit(prefixes[i].FromString(kPrefixStrings[i])); + } + + nexus.AdvanceTime(0); + + leader.Form(); + nexus.AdvanceTime(kFormNetworkTime); + + router.Join(leader); + + // Wait for all nodes and network data to stabilize. + nexus.AdvanceTime(kNetDataUpdateTime); + + Log("Simultaneous added entries on `router` coalesce into a single update"); + + initVersion = leader.Get().GetVersion(NetworkData::kFullSet); + + router.Get().PublishDnsSrpServiceUnicast(kDnsSrpPort, 0); + + prefixConfig.Clear(); + prefixConfig.mPreference = NetworkData::kRoutePreferenceMedium; + prefixConfig.mSlaac = true; + prefixConfig.mOnMesh = true; + prefixConfig.mStable = true; + prefixConfig.mPreferred = true; + + for (const Ip6::Prefix &prefix : prefixes) + { + prefixConfig.GetPrefix() = prefix; + SuccessOrQuit( + router.Get().PublishOnMeshPrefix(prefixConfig, NetworkData::Publisher::kFromUser)); + } + + for (uint32_t t = 0; t < 4000; t++) + { + nexus.AdvanceTime(1); + + if (router.Get().IsDnsSrpServiceAdded()) + { + break; + } + } + + // All entries must be added at the exact same millisecond. + VerifyOrQuit(router.Get().IsDnsSrpServiceAdded()); + + for (const Ip6::Prefix &prefix : prefixes) + { + VerifyOrQuit(router.Get().IsPrefixAdded(prefix)); + } + + nexus.AdvanceTime(5000); + + // Leader's Network Data version must have incremented by only 1. + VerifyOrQuit(leader.Get().GetVersion(NetworkData::kFullSet) == + static_cast(initVersion + 1)); +} + } // namespace Nexus } // namespace ot int main(void) { ot::Nexus::Test_NetDataPublisher(); + ot::Nexus::Test_NetDataPublisherCoalesceUpdates(); printf("All tests passed\n"); return 0; }