From edbdf8a56e23f9e6ffc4bfcd8b5d91d8b02cd54c Mon Sep 17 00:00:00 2001 From: lpbeliveau-silabs <112982107+lpbeliveau-silabs@users.noreply.github.com> Date: Thu, 13 Jul 2023 12:20:44 -0400 Subject: [PATCH] [ReadHandler] Report Scheduler class (#27553) * Added a new class that will handle the scheduling of reports. * Restyled by clang-format * Removed un-necessary define in TestReportScheduler and applied refactor of SetReportingIntervals to SetMaxReportingIntervals to platform code * Added TimerDelegate and wrapper functions around calls to Timer. Remove unnecessary checks for nullptr * Added VerifyOrReturn after NL_TEST_ASSERTS for nullptr * Completed TimerDelegate class and modified ReadHandlerNodes so they carry their own callback * Modified TimerDelegate to allow to pass different objects as context * ifdefing out ScheduleRun() to debug failing CI * Added issue # to TODOs, refactored Min/Max Intervals to Min/Max Timestamp * Clarified some comments regarding timing * Restyled by whitespace * Restyled by clang-format * Added interface to GetMonotonicTimestamp in the timer delegate * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Completed renaming to eliminate compiling error, moved TestReporScehduler in reporting namespace, addressed some low hanging fruits * Removed useless objects from tests as well as useless typecasting, and unnecessary check * Fixed comment about private methods used in ReportScheduler as a friend class * Changed to SetMinReportInterval to SetMinReportingIntervalForTests, removed the IsChunkedReport from comment about friend class, added a mock timestamp and timer to test to better control time in simulation for specific timing test cases * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Restyled by clang-format * Removed all calls to ReadHandler States to prevent Engine calls from the Test as it seems to impact the CI --------- Co-authored-by: Restyled.io Co-authored-by: Boris Zbarsky --- examples/platform/nrfconnect/util/ICDUtil.cpp | 2 +- .../silabs/ICDSubscriptionCallback.cpp | 2 +- src/app/BUILD.gn | 3 + src/app/ReadHandler.cpp | 77 +++- src/app/ReadHandler.h | 87 +++- src/app/reporting/Engine.cpp | 1 + src/app/reporting/ReportScheduler.h | 150 +++++++ src/app/reporting/ReportSchedulerImpl.cpp | 188 ++++++++ src/app/reporting/ReportSchedulerImpl.h | 63 +++ src/app/tests/BUILD.gn | 1 + src/app/tests/TestReportScheduler.cpp | 423 ++++++++++++++++++ src/controller/tests/data_model/TestRead.cpp | 2 +- src/platform/telink/ICDUtil.cpp | 2 +- 13 files changed, 985 insertions(+), 16 deletions(-) create mode 100644 src/app/reporting/ReportScheduler.h create mode 100644 src/app/reporting/ReportSchedulerImpl.cpp create mode 100644 src/app/reporting/ReportSchedulerImpl.h create mode 100644 src/app/tests/TestReportScheduler.cpp diff --git a/examples/platform/nrfconnect/util/ICDUtil.cpp b/examples/platform/nrfconnect/util/ICDUtil.cpp index fd2130c6c22dcb..b3dc9c80bbc9dd 100644 --- a/examples/platform/nrfconnect/util/ICDUtil.cpp +++ b/examples/platform/nrfconnect/util/ICDUtil.cpp @@ -36,5 +36,5 @@ CHIP_ERROR ICDUtil::OnSubscriptionRequested(chip::app::ReadHandler & aReadHandle agreedMaxInterval = kSubscriptionMaxIntervalPublisherLimit; } - return aReadHandler.SetReportingIntervals(agreedMaxInterval); + return aReadHandler.SetMaxReportingInterval(agreedMaxInterval); } diff --git a/examples/platform/silabs/ICDSubscriptionCallback.cpp b/examples/platform/silabs/ICDSubscriptionCallback.cpp index 735246f042af87..eba28968085d60 100644 --- a/examples/platform/silabs/ICDSubscriptionCallback.cpp +++ b/examples/platform/silabs/ICDSubscriptionCallback.cpp @@ -61,5 +61,5 @@ CHIP_ERROR ICDSubscriptionCallback::OnSubscriptionRequested(chip::app::ReadHandl decidedMaxInterval = maximumMaxInterval; } - return aReadHandler.SetReportingIntervals(decidedMaxInterval); + return aReadHandler.SetMaxReportingInterval(decidedMaxInterval); } diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn index 8211d22bf707b7..7f2c4d82280392 100644 --- a/src/app/BUILD.gn +++ b/src/app/BUILD.gn @@ -193,6 +193,9 @@ static_library("app") { "WriteHandler.cpp", "reporting/Engine.cpp", "reporting/Engine.h", + "reporting/ReportScheduler.h", + "reporting/ReportSchedulerImpl.cpp", + "reporting/ReportSchedulerImpl.h", "reporting/reporting.h", ] diff --git a/src/app/ReadHandler.cpp b/src/app/ReadHandler.cpp index 4a92fcfac91f9c..a5209ae45d588e 100644 --- a/src/app/ReadHandler.cpp +++ b/src/app/ReadHandler.cpp @@ -39,7 +39,7 @@ namespace app { using Status = Protocols::InteractionModel::Status; ReadHandler::ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, - InteractionType aInteractionType) : + InteractionType aInteractionType, Observer * observer) : mExchangeCtx(*this), mManagementCallback(apCallback) #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS @@ -63,15 +63,37 @@ ReadHandler::ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeCon SetStateFlag(ReadHandlerFlags::PrimingReports); mSessionHandle.Grab(mExchangeCtx->GetSessionHandle()); + +// TODO (#27672): Uncomment when the ReportScheduler is implemented +#if 0 + if (nullptr != observer) + { + if (CHIP_NO_ERROR == SetObserver(observer)) + { + mObserver->OnReadHandlerCreated(this); + } + } +#endif } #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS -ReadHandler::ReadHandler(ManagementCallback & apCallback) : +ReadHandler::ReadHandler(ManagementCallback & apCallback, Observer * observer) : mExchangeCtx(*this), mManagementCallback(apCallback), mOnConnectedCallback(HandleDeviceConnected, this), mOnConnectionFailureCallback(HandleDeviceConnectionFailure, this) { mInteractionType = InteractionType::Subscribe; mFlags.ClearAll(); + +// TODO (#27672): Uncomment when the ReportScheduler is implemented +#if 0 + if (nullptr != observer) + { + if (CHIP_NO_ERROR == SetObserver(observer)) + { + mObserver->OnReadHandlerCreated(this); + } + } +#endif } void ReadHandler::ResumeSubscription(CASESessionManager & caseSessionManager, @@ -115,6 +137,13 @@ void ReadHandler::ResumeSubscription(CASESessionManager & caseSessionManager, ReadHandler::~ReadHandler() { + // TODO (#27672): Enable when the ReportScheduler is implemented and move in Close() after testing +#if 0 + if (nullptr != mObserver) + { + mObserver->OnReadHandlerDestroyed(this); + } +#endif auto * appCallback = mManagementCallback.GetAppCallback(); if (mFlags.Has(ReadHandlerFlags::ActiveSubscription) && appCallback) { @@ -319,6 +348,15 @@ CHIP_ERROR ReadHandler::SendReportData(System::PacketBufferHandle && aPayload, b if (IsType(InteractionType::Subscribe) && !IsPriming()) { +// TODO (#27672): Enable when the ReportScheduler is implemented and remove call to UpdateReportTimer, will be handled by +// the report Scheduler +#if 0 + if (nullptr != mObserver) + { + mObserver->OnSubscriptionAction(this); + } +#endif + // Ignore the error from UpdateReportTimer. If we've // successfully sent the message, we need to return success from // this method. @@ -593,6 +631,13 @@ void ReadHandler::MoveToState(const HandlerState aTargetState) // if (aTargetState == HandlerState::GeneratingReports && IsReportableNow()) { +// TODO (#27672): Enable when the ReportScheduler is implemented and remove the call to ScheduleRun() +#if 0 + if(nullptr != mObserver) + { + mObserver->OnBecameReportable(this); + } +#endif InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun(); } } @@ -634,6 +679,14 @@ CHIP_ERROR ReadHandler::SendSubscribeResponse() ReturnErrorOnFailure(writer.Finalize(&packet)); VerifyOrReturnLogError(mExchangeCtx, CHIP_ERROR_INCORRECT_STATE); + // TODO (#27672): Uncomment when the ReportScheduler is implemented and remove call to UpdateReportTimer, handled by + // the report Scheduler +#if 0 + if (nullptr != mObserver) + { + mObserver->OnSubscriptionAction(this); + } +#endif ReturnErrorOnFailure(UpdateReportTimer()); ClearStateFlag(ReadHandlerFlags::PrimingReports); @@ -753,6 +806,7 @@ void ReadHandler::PersistSubscription() } } +// TODO (#27672): Remove when ReportScheduler is enabled as timing will now be handled by the ReportScheduler void ReadHandler::MinIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState) { VerifyOrReturn(apAppState != nullptr); @@ -764,6 +818,7 @@ void ReadHandler::MinIntervalExpiredCallback(System::Layer * apSystemLayer, void readHandler); } +// TODO (#27672): Remove when ReportScheduler is enabled as timing will now be handled by the ReportScheduler void ReadHandler::MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState) { VerifyOrReturn(apAppState != nullptr); @@ -773,6 +828,7 @@ void ReadHandler::MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void readHandler->mMaxInterval - readHandler->mMinIntervalFloorSeconds); } +// TODO (#27672): Remove when ReportScheduler is enabled as timing will now be handled by the ReportScheduler CHIP_ERROR ReadHandler::UpdateReportTimer() { InteractionModelEngine::GetInstance()->GetExchangeManager()->GetSessionManager()->SystemLayer()->CancelTimer( @@ -812,7 +868,7 @@ void ReadHandler::AttributePathIsDirty(const AttributePathParams & aAttributeCha // Here we just reset the iterator to the beginning of the current cluster, if the dirty path affects it. // This will ensure the reports are consistent within a single cluster generated from a single path in the request. - // TODO (#16699): Currently we can only gurentee the reports generated from a single path in the request are consistent. The + // TODO (#16699): Currently we can only guarantee the reports generated from a single path in the request are consistent. The // data might be inconsistent if the user send a request with two paths from the same cluster. We need to clearify the behavior // or make it consistent. if (mAttributePathExpandIterator.Get(path) && @@ -831,6 +887,13 @@ void ReadHandler::AttributePathIsDirty(const AttributePathParams & aAttributeCha if (IsReportableNow()) { + // TODO (#27672): Enable when the ReportScheduler is implemented and remove the call to ScheduleRun() +#if 0 + if(nullptr != mObserver) + { + mObserver->OnBecameReportable(this); + } +#endif InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun(); } } @@ -853,9 +916,17 @@ void ReadHandler::SetStateFlag(ReadHandlerFlags aFlag, bool aValue) { bool oldReportable = IsReportableNow(); mFlags.Set(aFlag, aValue); + // If we became reportable, schedule a reporting run. if (!oldReportable && IsReportableNow()) { +// TODO (#27672): Enable when the ReportScheduler is implemented and remove the call to ScheduleRun() +#if 0 + if(nullptr != mObserver) + { + mObserver->OnBecameReportable(this); + } +#endif InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun(); } } diff --git a/src/app/ReadHandler.h b/src/app/ReadHandler.h index f460188935223e..77c88c12cd1ff9 100644 --- a/src/app/ReadHandler.h +++ b/src/app/ReadHandler.h @@ -64,6 +64,8 @@ namespace app { namespace reporting { class Engine; class TestReportingEngine; +class ReportScheduler; +class TestReportScheduler; } // namespace reporting class InteractionModelEngine; @@ -152,6 +154,38 @@ class ReadHandler : public Messaging::ExchangeDelegate virtual ApplicationCallback * GetAppCallback() = 0; }; + // TODO (#27675) : Merge existing callback and observer into one class and have an observer pool in the Readhandler to notify + // every + /* + * Observer class for ReadHandler, meant to allow multiple objects to observe the ReadHandler. Currently only one observer is + * supported but all above callbacks should be merged into observer type and an observer pool should be added to allow multiple + * objects to observe ReadHandler + */ + class Observer + { + public: + virtual ~Observer() = default; + + /// @brief Callback invoked to notify a ReadHandler was created and can be registered + /// @param[in] apReadHandler ReadHandler getting added + virtual void OnReadHandlerCreated(ReadHandler * apReadHandler) = 0; + + /// @brief Callback invoked when a ReadHandler went from a non reportable state to a reportable state so a report can be + /// sent immediately if the minimal interval allows it. Otherwise the report should be rescheduled to the earliest time + /// allowed. + /// @param[in] apReadHandler ReadHandler that became dirty + virtual void OnBecameReportable(ReadHandler * apReadHandler) = 0; + + /// @brief Callback invoked when the read handler needs to make sure to send a message to the subscriber within the next + /// maxInterval time period. + /// @param[in] apReadHandler ReadHandler that has generated a report + virtual void OnSubscriptionAction(ReadHandler * apReadHandler) = 0; + + /// @brief Callback invoked when a ReadHandler is getting removed so it can be unregistered + /// @param[in] apReadHandler ReadHandler getting destroyed + virtual void OnReadHandlerDestroyed(ReadHandler * apReadHandler) = 0; + }; + /* * Destructor - as part of destruction, it will abort the exchange context * if a valid one still exists. @@ -167,7 +201,8 @@ class ReadHandler : public Messaging::ExchangeDelegate * The callback passed in has to outlive this handler object. * */ - ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, InteractionType aInteractionType); + ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, InteractionType aInteractionType, + Observer * observer = nullptr); #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS /** @@ -177,7 +212,7 @@ class ReadHandler : public Messaging::ExchangeDelegate * The callback passed in has to outlive this handler object. * */ - ReadHandler(ManagementCallback & apCallback); + ReadHandler(ManagementCallback & apCallback, Observer * observer = nullptr); #endif const ObjectList * GetAttributePathList() const { return mpAttributePathList; } @@ -190,13 +225,22 @@ class ReadHandler : public Messaging::ExchangeDelegate aMaxInterval = mMaxInterval; } + CHIP_ERROR SetMinReportingIntervalForTests(uint16_t aMinInterval) + { + VerifyOrReturnError(IsIdle(), CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(aMinInterval <= mMaxInterval, CHIP_ERROR_INVALID_ARGUMENT); + // Ensures the new min interval is higher than the subscriber established one. + mMinIntervalFloorSeconds = std::max(mMinIntervalFloorSeconds, aMinInterval); + return CHIP_NO_ERROR; + } + /* - * Set the reporting intervals for the subscription. This SHALL only be called + * Set the maximum reporting interval for the subscription. This SHALL only be called * from the OnSubscriptionRequested callback above. The restriction is as below * MinIntervalFloor ≤ MaxInterval ≤ MAX(SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT, MaxIntervalCeiling) * Where SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT is set to 60m in the spec. */ - CHIP_ERROR SetReportingIntervals(uint16_t aMaxInterval) + CHIP_ERROR SetMaxReportingInterval(uint16_t aMaxInterval) { VerifyOrReturnError(IsIdle(), CHIP_ERROR_INCORRECT_STATE); VerifyOrReturnError(mMinIntervalFloorSeconds <= aMaxInterval, CHIP_ERROR_INVALID_ARGUMENT); @@ -206,6 +250,18 @@ class ReadHandler : public Messaging::ExchangeDelegate return CHIP_NO_ERROR; } + /// @brief Add an observer to the read handler, currently only one observer is supported but all other callbacks should be + /// merged with a general observer type to allow multiple object to observe readhandlers + /// @param aObserver observer to be added + /// @return CHIP_ERROR_INVALID_ARGUMENT if passing in nullptr + CHIP_ERROR SetObserver(Observer * aObserver) + { + VerifyOrReturnError(nullptr != aObserver, CHIP_ERROR_INVALID_ARGUMENT); + // TODO (#27675) : After merging the callbacks and observer, change so the method adds a new observer to an observer pool + mObserver = aObserver; + return CHIP_NO_ERROR; + } + private: PriorityLevel GetCurrentPriority() const { return mCurrentPriority; } EventNumber & GetEventMin() { return mEventMin; } @@ -214,13 +270,13 @@ class ReadHandler : public Messaging::ExchangeDelegate { // WaitingUntilMinInterval is used to prevent subscription data delivery while we are // waiting for the min reporting interval to elapse. - WaitingUntilMinInterval = (1 << 0), + WaitingUntilMinInterval = (1 << 0), // TODO (#27672): Remove once ReportScheduler is implemented or change to test flag // WaitingUntilMaxInterval is used to prevent subscription empty report delivery while we // are waiting for the max reporting interval to elaps. When WaitingUntilMaxInterval // becomes false, we are allowed to send an empty report to keep the // subscription alive on the client. - WaitingUntilMaxInterval = (1 << 1), + WaitingUntilMaxInterval = (1 << 1), // TODO (#27672): Remove once ReportScheduler is implemented // The flag indicating we are in the middle of a series of chunked report messages, this flag will be cleared during // sending last chunked message. @@ -291,6 +347,8 @@ class ReadHandler : public Messaging::ExchangeDelegate bool IsIdle() const { return mState == HandlerState::Idle; } + // TODO (#27672): Change back to IsReportable once ReportScheduler is implemented so this can assess reportability without + // considering timing. The ReporScheduler will handle timing. /// @brief Returns whether the ReadHandler is in a state where it can immediately send a report. This function /// is used to determine whether a report generation should be scheduled for the handler. bool IsReportableNow() const @@ -370,6 +428,7 @@ class ReadHandler : public Messaging::ExchangeDelegate friend class TestReadInteraction; friend class chip::app::reporting::TestReportingEngine; + friend class chip::app::reporting::TestReportScheduler; // // The engine needs to be able to Abort/Close a ReadHandler instance upon completion of work for a given read/subscribe @@ -379,6 +438,10 @@ class ReadHandler : public Messaging::ExchangeDelegate friend class chip::app::reporting::Engine; friend class chip::app::InteractionModelEngine; + // The report scheduler needs to be able to access StateFlag private functions IsGeneratingReports() and IsDirty() to + // know when to schedule a run so it is declared as a friend class. + friend class chip::app::reporting::ReportScheduler; + enum class HandlerState : uint8_t { Idle, ///< The handler has been initialized and is ready @@ -404,10 +467,13 @@ class ReadHandler : public Messaging::ExchangeDelegate /// @brief This function is called when the min interval timer has expired, it restarts the timer on a timeout equal to the /// difference between the max interval and the min interval. - static void MinIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState); - static void MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState); + static void MinIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState); // TODO (#27672): Remove once + // ReportScheduler is implemented. + static void MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState); // TODO (#27672): Remove once + // ReportScheduler is implemented. /// @brief This function is called when a report is sent and it restarts the min interval timer. - CHIP_ERROR UpdateReportTimer(); + CHIP_ERROR UpdateReportTimer(); // TODO (#27672) : Remove once ReportScheduler is implemented. + CHIP_ERROR SendSubscribeResponse(); CHIP_ERROR ProcessSubscribeRequest(System::PacketBufferHandle && aPayload); CHIP_ERROR ProcessReadRequest(System::PacketBufferHandle && aPayload); @@ -520,6 +586,9 @@ class ReadHandler : public Messaging::ExchangeDelegate BitFlags mFlags; InteractionType mInteractionType = InteractionType::Read; + // TODO (#27675): Merge all observers into one and that one will dispatch the callbacks to the right place. + Observer * mObserver = nullptr; + #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS // Callbacks to handle server-initiated session success/failure chip::Callback::Callback mOnConnectedCallback; diff --git a/src/app/reporting/Engine.cpp b/src/app/reporting/Engine.cpp index a9853215332fd5..f0fea66cc0b16c 100644 --- a/src/app/reporting/Engine.cpp +++ b/src/app/reporting/Engine.cpp @@ -636,6 +636,7 @@ void Engine::Run() ReadHandler * readHandler = imEngine->ActiveHandlerAt(mCurReadHandlerIdx % (uint32_t) imEngine->mReadHandlers.Allocated()); VerifyOrDie(readHandler != nullptr); + // TODO (#27672): Replace with check with Report Scheduler if the read handler is reportable if (readHandler->IsReportableNow()) { mRunningReadHandler = readHandler; diff --git a/src/app/reporting/ReportScheduler.h b/src/app/reporting/ReportScheduler.h new file mode 100644 index 00000000000000..80d391c171c7e5 --- /dev/null +++ b/src/app/reporting/ReportScheduler.h @@ -0,0 +1,150 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * 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 + +namespace chip { +namespace app { +namespace reporting { + +// Forward declaration of TestReportScheduler to allow it to be friend with ReportScheduler +class TestReportScheduler; + +using Timestamp = System::Clock::Timestamp; + +class ReportScheduler : public ReadHandler::Observer +{ +public: + /// @brief This class acts as an interface between the report scheduler and the system timer to reduce dependencies on the + /// system layer. + class TimerDelegate + { + public: + virtual ~TimerDelegate() {} + /// @brief Start a timer for a given context. The report scheduler must always cancel an existing timer for a context (using + /// CancelTimer) before starting a new one for that context. + /// @param context context to pass to the timer callback. + /// @param aTimeout time in miliseconds before the timer expires + virtual CHIP_ERROR StartTimer(void * context, System::Clock::Timeout aTimeout) = 0; + /// @brief Cancel a timer for a given context + /// @param context used to identify the timer to cancel + virtual void CancelTimer(void * context) = 0; + virtual bool IsTimerActive(void * context) = 0; + virtual Timestamp GetCurrentMonotonicTimestamp() = 0; + }; + + class ReadHandlerNode : public IntrusiveListNodeBase<> + { + public: + using TimerCompleteCallback = void (*)(); + + ReadHandlerNode(ReadHandler * aReadHandler, TimerDelegate * aTimerDelegate, TimerCompleteCallback aCallback) : + mTimerDelegate(aTimerDelegate), mCallback(aCallback) + { + VerifyOrDie(aReadHandler != nullptr); + VerifyOrDie(aTimerDelegate != nullptr); + VerifyOrDie(aCallback != nullptr); + + mReadHandler = aReadHandler; + SetIntervalTimeStamps(aReadHandler); + } + ReadHandler * GetReadHandler() const { return mReadHandler; } + /// @brief Check if the Node is reportable now, meaning its readhandler was made reportable by attribute dirtying and + /// handler state, and minimal time interval since last report has elapsed, or the maximal time interval since last + /// report has elapsed + bool IsReportableNow() const + { + // TODO: Add flags to allow for test to simulate waiting for the min interval or max intrval to elapse when integrating + // the scheduler in the ReadHandler + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + return (mReadHandler->IsGeneratingReports() && + ((now >= mMinTimestamp && mReadHandler->IsDirty()) || now >= mMaxTimestamp)); + } + + void SetIntervalTimeStamps(ReadHandler * aReadHandler) + { + uint16_t minInterval, maxInterval; + aReadHandler->GetReportingIntervals(minInterval, maxInterval); + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + mMinTimestamp = now + System::Clock::Seconds16(minInterval); + mMaxTimestamp = now + System::Clock::Seconds16(maxInterval); + } + + void RunCallback() { mCallback(); } + + Timestamp GetMinTimestamp() const { return mMinTimestamp; } + Timestamp GetMaxTimestamp() const { return mMaxTimestamp; } + + private: + TimerDelegate * mTimerDelegate; + TimerCompleteCallback mCallback; + ReadHandler * mReadHandler; + Timestamp mMinTimestamp; + Timestamp mMaxTimestamp; + }; + + ReportScheduler(TimerDelegate * aTimerDelegate) : mTimerDelegate(aTimerDelegate) {} + /** + * Interface to act on changes in the ReadHandler reportability + */ + virtual ~ReportScheduler() = default; + + /// @brief Check if a ReadHandler is scheduled for reporting + virtual bool IsReportScheduled(ReadHandler * aReadHandler) = 0; + /// @brief Check whether a ReadHandler is reportable right now, taking into account its minimum and maximum intervals. + /// @param aReadHandler read handler to check + bool IsReportableNow(ReadHandler * aReadHandler) { return FindReadHandlerNode(aReadHandler)->IsReportableNow(); }; + /// @brief Check if a ReadHandler is reportable without considering the timing + bool IsReadHandlerReportable(ReadHandler * aReadHandler) const + { + return aReadHandler->IsGeneratingReports() && aReadHandler->IsDirty(); + } + + /// @brief Get the number of ReadHandlers registered in the scheduler's node pool + size_t GetNumReadHandlers() const { return mNodesPool.Allocated(); } + +protected: + friend class chip::app::reporting::TestReportScheduler; + + /// @brief Find the ReadHandlerNode for a given ReadHandler pointer + /// @param [in] aReadHandler ReadHandler pointer to look for in the ReadHandler nodes list + /// @return Node Address if node was found, nullptr otherwise + ReadHandlerNode * FindReadHandlerNode(const ReadHandler * aReadHandler) + { + for (auto & iter : mReadHandlerList) + { + if (iter.GetReadHandler() == aReadHandler) + { + return &iter; + } + } + return nullptr; + } + + IntrusiveList mReadHandlerList; + ObjectPool mNodesPool; + TimerDelegate * mTimerDelegate; +}; +}; // namespace reporting +}; // namespace app +}; // namespace chip diff --git a/src/app/reporting/ReportSchedulerImpl.cpp b/src/app/reporting/ReportSchedulerImpl.cpp new file mode 100644 index 00000000000000..4b45ab9d1b6deb --- /dev/null +++ b/src/app/reporting/ReportSchedulerImpl.cpp @@ -0,0 +1,188 @@ +/* + * + * Copyright (c) 2023 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 + +namespace chip { +namespace app { +namespace reporting { + +using Seconds16 = System::Clock::Seconds16; +using Milliseconds32 = System::Clock::Milliseconds32; +using Timeout = System::Clock::Timeout; +using Timestamp = System::Clock::Timestamp; +using ReadHandlerNode = ReportScheduler::ReadHandlerNode; + +/// @brief Callback called when the report timer expires to schedule an engine run regardless of the state of the ReadHandlers, as +/// the engine already verifies that read handlers are reportable before sending a report +static void ReportTimerCallback() +{ + InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun(); +} + +ReportSchedulerImpl::ReportSchedulerImpl(TimerDelegate * aTimerDelegate) : ReportScheduler(aTimerDelegate) +{ + VerifyOrDie(nullptr != mTimerDelegate); +} + +/// @brief When a ReadHandler is added, register it, which will schedule an engine run +void ReportSchedulerImpl::OnReadHandlerCreated(ReadHandler * aReadHandler) +{ + RegisterReadHandler(aReadHandler); +} + +/// @brief When a ReadHandler becomes reportable, schedule, verifies if the min interval of a handleris elapsed. If not, +/// reschedule the report to happen when the min interval is elapsed. If it is, schedule an engine run. +void ReportSchedulerImpl::OnBecameReportable(ReadHandler * aReadHandler) +{ + ReadHandlerNode * node = FindReadHandlerNode(aReadHandler); + VerifyOrReturn(nullptr != node); + + Milliseconds32 newTimeout; + if (node->IsReportableNow()) + { + // If the handler is reportable now, just schedule a report immediately + newTimeout = Milliseconds32(0); + } + else + { + // If the handler is not reportable now, schedule a report for the min interval + newTimeout = node->GetMinTimestamp() - mTimerDelegate->GetCurrentMonotonicTimestamp(); + } + + ScheduleReport(newTimeout, node); +} + +void ReportSchedulerImpl::OnSubscriptionAction(ReadHandler * apReadHandler) +{ + ReadHandlerNode * node = FindReadHandlerNode(apReadHandler); + VerifyOrReturn(nullptr != node); + // Schedule callback for max interval by computing the difference between the max timestamp and the current timestamp + node->SetIntervalTimeStamps(apReadHandler); + Milliseconds32 newTimeout = node->GetMaxTimestamp() - mTimerDelegate->GetCurrentMonotonicTimestamp(); + ScheduleReport(newTimeout, node); +} + +/// @brief When a ReadHandler is removed, unregister it, which will cancel any scheduled report +void ReportSchedulerImpl::OnReadHandlerDestroyed(ReadHandler * aReadHandler) +{ + UnregisterReadHandler(aReadHandler); +} + +CHIP_ERROR ReportSchedulerImpl::RegisterReadHandler(ReadHandler * aReadHandler) +{ + ReadHandlerNode * newNode = FindReadHandlerNode(aReadHandler); + // Handler must not be registered yet; it's just being constructed. + VerifyOrDie(nullptr == newNode); + // The NodePool is the same size as the ReadHandler pool from the IM Engine, so we don't need a check for size here since if a + // ReadHandler was created, space should be available. + newNode = mNodesPool.CreateObject(aReadHandler, mTimerDelegate, ReportTimerCallback); + mReadHandlerList.PushBack(newNode); + + ChipLogProgress(DataManagement, + "Registered a ReadHandler that will schedule a report between system Timestamp: %" PRIu64 + " and system Timestamp %" PRIu64 ".", + newNode->GetMinTimestamp().count(), newNode->GetMaxTimestamp().count()); + + Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp(); + Milliseconds32 newTimeout; + // If the handler is reportable, schedule a report for the min interval, otherwise schedule a report for the max interval + if (newNode->IsReportableNow()) + { + // If the handler is reportable now, just schedule a report immediately + newTimeout = Milliseconds32(0); + } + else if (IsReadHandlerReportable(aReadHandler) && (newNode->GetMinTimestamp() > now)) + { + // If the handler is reportable now, but the min interval is not elapsed, schedule a report for the moment the min interval + // has elapsed + newTimeout = newNode->GetMinTimestamp() - now; + } + else + { + // If the handler is not reportable now, schedule a report for the max interval + newTimeout = newNode->GetMaxTimestamp() - now; + } + + ReturnErrorOnFailure(ScheduleReport(newTimeout, newNode)); + return CHIP_NO_ERROR; +} + +CHIP_ERROR ReportSchedulerImpl::ScheduleReport(Timeout timeout, ReadHandlerNode * node) +{ + // Cancel Report if it is currently scheduled + CancelSchedulerTimer(node); + StartSchedulerTimer(node, timeout); + + return CHIP_NO_ERROR; +} + +void ReportSchedulerImpl::CancelReport(ReadHandler * aReadHandler) +{ + ReadHandlerNode * node = FindReadHandlerNode(aReadHandler); + VerifyOrReturn(nullptr != node); + CancelSchedulerTimer(node); +} + +void ReportSchedulerImpl::UnregisterReadHandler(ReadHandler * aReadHandler) +{ + CancelReport(aReadHandler); + + ReadHandlerNode * removeNode = FindReadHandlerNode(aReadHandler); + // Nothing to remove if the handler is not found in the list + VerifyOrReturn(nullptr != removeNode); + + mReadHandlerList.Remove(removeNode); + mNodesPool.ReleaseObject(removeNode); +} + +void ReportSchedulerImpl::UnregisterAllHandlers() +{ + while (!mReadHandlerList.Empty()) + { + ReadHandler * firstReadHandler = mReadHandlerList.begin()->GetReadHandler(); + UnregisterReadHandler(firstReadHandler); + } +} + +bool ReportSchedulerImpl::IsReportScheduled(ReadHandler * aReadHandler) +{ + ReadHandlerNode * node = FindReadHandlerNode(aReadHandler); + VerifyOrReturnValue(nullptr != node, false); + return CheckSchedulerTimerActive(node); +} + +CHIP_ERROR ReportSchedulerImpl::StartSchedulerTimer(ReadHandlerNode * node, System::Clock::Timeout aTimeout) +{ + // Schedule Report + return mTimerDelegate->StartTimer(node, aTimeout); +} + +void ReportSchedulerImpl::CancelSchedulerTimer(ReadHandlerNode * node) +{ + mTimerDelegate->CancelTimer(node); +} + +bool ReportSchedulerImpl::CheckSchedulerTimerActive(ReadHandlerNode * node) +{ + return mTimerDelegate->IsTimerActive(node); +} + +} // namespace reporting +} // namespace app +} // namespace chip diff --git a/src/app/reporting/ReportSchedulerImpl.h b/src/app/reporting/ReportSchedulerImpl.h new file mode 100644 index 00000000000000..849f9b797b5f93 --- /dev/null +++ b/src/app/reporting/ReportSchedulerImpl.h @@ -0,0 +1,63 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * 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 + +namespace chip { +namespace app { +namespace reporting { + +class ReportSchedulerImpl : public ReportScheduler +{ +public: + ReportSchedulerImpl(TimerDelegate * aTimerDelegate); + ~ReportSchedulerImpl() override { UnregisterAllHandlers(); } + + // ReadHandlerObserver + void OnReadHandlerCreated(ReadHandler * aReadHandler) override; + void OnBecameReportable(ReadHandler * aReadHandler) override; + void OnSubscriptionAction(ReadHandler * aReadHandler) override; + void OnReadHandlerDestroyed(ReadHandler * aReadHandler) override; + +protected: + virtual CHIP_ERROR RegisterReadHandler(ReadHandler * aReadHandler); + virtual CHIP_ERROR ScheduleReport(System::Clock::Timeout timeout, ReadHandlerNode * node); + virtual void CancelReport(ReadHandler * aReadHandler); + virtual void UnregisterReadHandler(ReadHandler * aReadHandler); + virtual void UnregisterAllHandlers(); + +private: + friend class chip::app::reporting::TestReportScheduler; + + bool IsReportScheduled(ReadHandler * aReadHandler) override; + + /// @brief Start a timer for a given ReadHandlerNode, ensures that if a timer is already running for this node, it is cancelled + /// @param node Node of the ReadHandler list to start a timer for + /// @param aTimeout Delay before the timer expires + virtual CHIP_ERROR StartSchedulerTimer(ReadHandlerNode * node, System::Clock::Timeout aTimeout); + /// @brief Cancel the timer for a given ReadHandlerNode + virtual void CancelSchedulerTimer(ReadHandlerNode * node); + /// @brief Check if the timer for a given ReadHandlerNode is active + virtual bool CheckSchedulerTimerActive(ReadHandlerNode * node); +}; + +} // namespace reporting +} // namespace app +} // namespace chip diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn index c716f442852c53..365596da641d03 100644 --- a/src/app/tests/BUILD.gn +++ b/src/app/tests/BUILD.gn @@ -144,6 +144,7 @@ chip_test_suite("tests") { "TestOperationalStateDelegate.cpp", "TestPendingNotificationMap.cpp", "TestReadInteraction.cpp", + "TestReportScheduler.cpp", "TestReportingEngine.cpp", "TestSceneTable.cpp", "TestStatusIB.cpp", diff --git a/src/app/tests/TestReportScheduler.cpp b/src/app/tests/TestReportScheduler.cpp new file mode 100644 index 00000000000000..a24422010c47c9 --- /dev/null +++ b/src/app/tests/TestReportScheduler.cpp @@ -0,0 +1,423 @@ +/* + * + * Copyright (c) 2023 Project CHIP Authors + * All rights reserved. + * + * 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 +#include +#include +#include +#include + +namespace { + +class TestContext : public chip::Test::AppContext +{ +public: + static int Initialize(void * context) + { + if (AppContext::Initialize(context) != SUCCESS) + return FAILURE; + + auto * ctx = static_cast(context); + + if (ctx->mEventCounter.Init(0) != CHIP_NO_ERROR) + { + return FAILURE; + } + + return SUCCESS; + } + + static int Finalize(void * context) + { + chip::app::EventManagement::DestroyEventManagement(); + + if (AppContext::Finalize(context) != SUCCESS) + return FAILURE; + + return SUCCESS; + } + +private: + chip::MonotonicallyIncreasingCounter mEventCounter; +}; + +class NullReadHandlerCallback : public chip::app::ReadHandler::ManagementCallback +{ +public: + void OnDone(chip::app::ReadHandler & apReadHandlerObj) override {} + chip::app::ReadHandler::ApplicationCallback * GetAppCallback() override { return nullptr; } +}; + +} // namespace + +namespace chip { +namespace app { +namespace reporting { + +using InteractionModelEngine = InteractionModelEngine; +using ReportScheduler = reporting::ReportScheduler; +using ReportSchedulerImpl = reporting::ReportSchedulerImpl; +using ReadHandlerNode = reporting::ReportScheduler::ReadHandlerNode; +using Milliseconds64 = System::Clock::Milliseconds64; + +static const size_t kNumMaxReadHandlers = 16; + +class TestTimerDelegate : public ReportScheduler::TimerDelegate +{ +public: + struct NodeTimeoutPair + { + ReadHandlerNode * node; + System::Clock::Timeout timeout; + }; + + NodeTimeoutPair mPairArray[kNumMaxReadHandlers]; + size_t mPairArraySize = 0; + System::Clock::Timestamp mMockSystemTimestamp = System::Clock::Milliseconds64(0); + + NodeTimeoutPair * FindPair(ReadHandlerNode * node, size_t & position) + { + for (size_t i = 0; i < mPairArraySize; i++) + { + if (mPairArray[i].node == node) + { + position = i; + return &mPairArray[i]; + } + } + return nullptr; + } + + CHIP_ERROR insertPair(ReadHandlerNode * node, System::Clock::Timeout timeout) + { + VerifyOrReturnError(mPairArraySize < kNumMaxReadHandlers, CHIP_ERROR_NO_MEMORY); + mPairArray[mPairArraySize].node = node; + mPairArray[mPairArraySize].timeout = timeout; + mPairArraySize++; + + return CHIP_NO_ERROR; + } + + void removePair(ReadHandlerNode * node) + { + size_t position; + NodeTimeoutPair * pair = FindPair(node, position); + VerifyOrReturn(pair != nullptr); + + size_t nextPos = static_cast(position + 1); + size_t moveNum = static_cast(mPairArraySize - nextPos); + + // Compress array after removal, if the removed position is not the last + if (moveNum) + { + memmove(&mPairArray[position], &mPairArray[nextPos], sizeof(NodeTimeoutPair) * moveNum); + } + + mPairArraySize--; + } + + static void TimerCallbackInterface(System::Layer * aLayer, void * aAppState) + { + // Normaly we would call the callback here, thus scheduling an engine run, but we don't need it for this test as we simulate + // all the callbacks related to report emissions. The actual callback should look like this: + // + // ReadHandlerNode * node = static_cast(aAppState); + // node->RunCallback(); + ChipLogProgress(DataManagement, "Simluating engine run for Handler: %p", aAppState); + } + virtual CHIP_ERROR StartTimer(void * context, System::Clock::Timeout aTimeout) override + { + return insertPair(static_cast(context), aTimeout + mMockSystemTimestamp); + } + virtual void CancelTimer(void * context) override { removePair(static_cast(context)); } + virtual bool IsTimerActive(void * context) override + { + size_t position; + NodeTimeoutPair * pair = FindPair(static_cast(context), position); + VerifyOrReturnValue(pair != nullptr, false); + + return pair->timeout > mMockSystemTimestamp; + } + + virtual System::Clock::Timestamp GetCurrentMonotonicTimestamp() override { return mMockSystemTimestamp; } + + void SetMockSystemTimestamp(System::Clock::Timestamp aMockTimestamp) { mMockSystemTimestamp = aMockTimestamp; } + + // Increment the mock timestamp one milisecond at a time for a total of aTime miliseconds. Checks if + void IncrementMockTimestamp(System::Clock::Milliseconds64 aTime) + { + mMockSystemTimestamp = mMockSystemTimestamp + aTime; + for (size_t i = 0; i < mPairArraySize; i++) + { + if (mPairArray[i].timeout <= mMockSystemTimestamp) + { + TimerCallbackInterface(nullptr, mPairArray[i].node); + } + } + } +}; + +TestTimerDelegate sTestTimerDelegate; +ReportSchedulerImpl sScheduler(&sTestTimerDelegate); + +class TestReportScheduler +{ +public: + static void TestReadHandlerList(nlTestSuite * aSuite, void * aContext) + { + TestContext & ctx = *static_cast(aContext); + NullReadHandlerCallback nullCallback; + // exchange context + Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false); + + // Read handler pool + ObjectPool readHandlerPool; + + // Initialize mock timestamp + sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0)); + + for (size_t i = 0; i < kNumMaxReadHandlers; i++) + { + ReadHandler * readHandler = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, nullptr != readHandler); + VerifyOrReturn(nullptr != readHandler); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler)); + NL_TEST_ASSERT(aSuite, nullptr != sScheduler.FindReadHandlerNode(readHandler)); + } + + NL_TEST_ASSERT(aSuite, readHandlerPool.Allocated() == kNumMaxReadHandlers); + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers); + NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 1); + + // Test unregister first ReadHandler + ReadHandler * firstReadHandler = sScheduler.mReadHandlerList.begin()->GetReadHandler(); + sScheduler.UnregisterReadHandler(firstReadHandler); + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 1); + NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(firstReadHandler)); + + // Test unregister middle ReadHandler + auto iter = sScheduler.mReadHandlerList.begin(); + for (size_t i = 0; i < static_cast(kNumMaxReadHandlers / 2); i++) + { + iter++; + } + ReadHandler * middleReadHandler = iter->GetReadHandler(); + sScheduler.UnregisterReadHandler(middleReadHandler); + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 2); + NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(middleReadHandler)); + + // Test unregister last ReadHandler + iter = sScheduler.mReadHandlerList.end(); + iter--; + ReadHandler * lastReadHandler = iter->GetReadHandler(); + sScheduler.UnregisterReadHandler(lastReadHandler); + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 3); + NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(lastReadHandler)); + + sScheduler.UnregisterAllHandlers(); + // Confirm all ReadHandlers are unregistered from the scheduler + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0); + readHandlerPool.ForEachActiveObject([&](ReadHandler * handler) { + NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(handler)); + return Loop::Continue; + }); + + readHandlerPool.ReleaseAll(); + exchangeCtx->Close(); + NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); + } + + static void TestReportTiming(nlTestSuite * aSuite, void * aContext) + { + TestContext & ctx = *static_cast(aContext); + NullReadHandlerCallback nullCallback; + // exchange context + Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false); + + // Read handler pool + ObjectPool readHandlerPool; + + // Initialize mock timestamp + sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0)); + + // Dirty read handler, will be triggered at min interval + ReadHandler * readHandler1 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMaxReportingInterval(2)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMinReportingIntervalForTests(1)); + // Do those manually to avoid scheduling an engine run + readHandler1->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true); + readHandler1->mState = ReadHandler::HandlerState::GeneratingReports; + + // Clean read handler, will be triggered at max interval + ReadHandler * readHandler2 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMaxReportingInterval(3)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMinReportingIntervalForTests(0)); + // Do those manually to avoid scheduling an engine run + readHandler2->mState = ReadHandler::HandlerState::GeneratingReports; + + // Clean read handler, will be triggered at max interval, but will be cancelled before + ReadHandler * readHandler3 = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMaxReportingInterval(3)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMinReportingIntervalForTests(0)); + // Do those manually to avoid scheduling an engine run + readHandler3->mState = ReadHandler::HandlerState::GeneratingReports; + + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler1)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler2)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler3)); + + // Confirms that none of the ReadHandlers are currently reportable + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler3)); + + // Simulate system clock increment + sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100)); + + // Checks that the first ReadHandler is reportable after 1 second since it is dirty and min interval has expired + NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler2)); + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler3)); + + NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler3)); + sScheduler.CancelReport(readHandler3); + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler3)); + + // Simulate system clock increment + sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(2000)); + + // Checks that all ReadHandlers are reportable + NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler1)); + NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler2)); + // Even if its timer got cancelled, readHandler3 should still be considered reportable as the max interval has expired + // and it is in generating report state + NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler3)); + + sScheduler.UnregisterAllHandlers(); + readHandlerPool.ReleaseAll(); + exchangeCtx->Close(); + NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); + } + + static void TestObserverCallbacks(nlTestSuite * aSuite, void * aContext) + { + TestContext & ctx = *static_cast(aContext); + NullReadHandlerCallback nullCallback; + // exchange context + Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false); + + // Read handler pool + ObjectPool readHandlerPool; + + // Initialize mock timestamp + sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0)); + + ReadHandler * readHandler = + readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler->SetMaxReportingInterval(2)); + NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler->SetMinReportingIntervalForTests(1)); + // Do those manually to avoid scheduling an engine run + readHandler->mState = ReadHandler::HandlerState::GeneratingReports; + readHandler->SetObserver(&sScheduler); + + // Test OnReadHandlerCreated + readHandler->mObserver->OnReadHandlerCreated(readHandler); + // Should have registered the read handler in the scheduler and scheduled a report + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 1); + NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler)); + ReadHandlerNode * node = sScheduler.FindReadHandlerNode(readHandler); + NL_TEST_ASSERT(aSuite, nullptr != node); + VerifyOrReturn(nullptr != node); + NL_TEST_ASSERT(aSuite, node->GetReadHandler() == readHandler); + + // Test OnBecameReportable + readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true); + readHandler->mObserver->OnBecameReportable(readHandler); + // Should have changed the scheduled timeout to the handler's min interval, to check, we wait for the min interval to + // expire + // Simulate system clock increment + sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100)); + + // Check that no report is scheduled since the min interval has expired, the timer should now be stopped + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler)); + + // Test OnSubscriptionAction + readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, false); + readHandler->mObserver->OnSubscriptionAction(readHandler); + // Should have changed the scheduled timeout to the handlers max interval, to check, we wait for the min interval to + // confirm it is not expired yet so the report should still be scheduled + + NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler)); + // Simulate system clock increment + sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100)); + + // Check that the report is still scheduled as the max interval has not expired yet and the dirty flag was cleared + NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler)); + // Simulate system clock increment + sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(2100)); + + // Check that no report is scheduled since the max interval should have expired, the timer should now be stopped + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler)); + + // Test OnReadHandlerDestroyed + readHandler->mObserver->OnReadHandlerDestroyed(readHandler); + // Should have unregistered the read handler in the scheduler and cancelled the report + NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler)); + NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0); + NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(readHandler)); + + sScheduler.UnregisterReadHandler(readHandler); + readHandlerPool.ReleaseAll(); + exchangeCtx->Close(); + NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0); + } +}; + +} // namespace reporting +} // namespace app +} // namespace chip + +namespace { + +/** + * Test Suite. It lists all the test functions. + */ + +static nlTest sTests[] = { + NL_TEST_DEF("TestReadHandlerList", chip::app::reporting::TestReportScheduler::TestReadHandlerList), + NL_TEST_DEF("TestReportTiming", chip::app::reporting::TestReportScheduler::TestReportTiming), + NL_TEST_DEF("TestObserverCallbacks", chip::app::reporting::TestReportScheduler::TestObserverCallbacks), + NL_TEST_SENTINEL(), +}; + +nlTestSuite sSuite = { "TestReportScheduler", &sTests[0], TestContext::Initialize, TestContext::Finalize }; + +} // namespace + +int TestReportScheduler() +{ + return chip::ExecuteTestsWithContext(&sSuite); +} + +CHIP_REGISTER_TEST_SUITE(TestReportScheduler); diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp index 4bf94757e835e0..5c59c6c1501488 100644 --- a/src/controller/tests/data_model/TestRead.cpp +++ b/src/controller/tests/data_model/TestRead.cpp @@ -302,7 +302,7 @@ class TestReadInteraction : public app::ReadHandler::ApplicationCallback if (mAlterSubscriptionIntervals) { - ReturnErrorOnFailure(aReadHandler.SetReportingIntervals(mMaxInterval)); + ReturnErrorOnFailure(aReadHandler.SetMaxReportingInterval(mMaxInterval)); } return CHIP_NO_ERROR; } diff --git a/src/platform/telink/ICDUtil.cpp b/src/platform/telink/ICDUtil.cpp index fd2130c6c22dcb..b3dc9c80bbc9dd 100644 --- a/src/platform/telink/ICDUtil.cpp +++ b/src/platform/telink/ICDUtil.cpp @@ -36,5 +36,5 @@ CHIP_ERROR ICDUtil::OnSubscriptionRequested(chip::app::ReadHandler & aReadHandle agreedMaxInterval = kSubscriptionMaxIntervalPublisherLimit; } - return aReadHandler.SetReportingIntervals(agreedMaxInterval); + return aReadHandler.SetMaxReportingInterval(agreedMaxInterval); }