[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.
This commit is contained in:
Abtin Keshavarzian
2026-10-02 17:08:49 -07:00
committed by GitHub
parent 7ec78ee4a2
commit 0d40f2fe29
5 changed files with 226 additions and 9 deletions
+12
View File
@@ -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
*
+102 -9
View File
@@ -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<uint32_t>(1, kMaxDelayToAdd);
SelectUpdateTime(now, 1, kMaxDelayToAdd);
SetState(kAdding);
Get<Publisher>().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<uint32_t>(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<uint32_t>(aMinDelay, aMaxDelay);
#if OPENTHREAD_CONFIG_NETDATA_PUBLISHER_COALESCE_UPDATES_ENABLE
{
UpdateTimeCoalescer coalescer(aNow + aMinDelay, aNow + aMaxDelay, mUpdateTime);
Get<Publisher>().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
@@ -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<Publisher, &Publisher::HandleTimer>;
@@ -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
+84
View File
@@ -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<NetworkData::Leader>().GetVersion(NetworkData::kFullSet);
router.Get<NetworkData::Publisher>().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<NetworkData::Publisher>().PublishOnMeshPrefix(prefixConfig, NetworkData::Publisher::kFromUser));
}
for (uint32_t t = 0; t < 4000; t++)
{
nexus.AdvanceTime(1);
if (router.Get<NetworkData::Publisher>().IsDnsSrpServiceAdded())
{
break;
}
}
// All entries must be added at the exact same millisecond.
VerifyOrQuit(router.Get<NetworkData::Publisher>().IsDnsSrpServiceAdded());
for (const Ip6::Prefix &prefix : prefixes)
{
VerifyOrQuit(router.Get<NetworkData::Publisher>().IsPrefixAdded(prefix));
}
nexus.AdvanceTime(5000);
// Leader's Network Data version must have incremented by only 1.
VerifyOrQuit(leader.Get<NetworkData::Leader>().GetVersion(NetworkData::kFullSet) ==
static_cast<uint8_t>(initVersion + 1));
}
} // namespace Nexus
} // namespace ot
int main(void)
{
ot::Nexus::Test_NetDataPublisher();
ot::Nexus::Test_NetDataPublisherCoalesceUpdates();
printf("All tests passed\n");
return 0;
}