| #include "dbus_utility.hpp" |
| #include "managed_store.hpp" |
| #include "managed_store_types.hpp" |
| |
| #include <boost/asio/io_context.hpp> |
| |
| #include <memory> |
| #include <thread> |
| |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| namespace managedStore |
| { |
| namespace |
| { |
| |
| using namespace std::chrono_literals; |
| using testing::_; |
| using testing::DoAll; |
| using testing::InvokeArgument; |
| using testing::Return; |
| |
| constexpr std::chrono::duration kTestShortDuration = 20ms; |
| constexpr std::chrono::duration kTestMediumDuration = 50ms; |
| constexpr std::chrono::duration kTestLongDuration = 100ms; |
| |
| // Pending dbus response count used in tests. |
| constexpr size_t kPendingDbusResponseLow = 2; |
| constexpr size_t kPendingDbusResponseHigh = 10; |
| |
| class MockManagedObjectStore : public ManagedObjectStore |
| { |
| public: |
| MockManagedObjectStore(const ManagedObjectStoreConfig& cfg, |
| boost::asio::io_context& io) : |
| ManagedObjectStore(cfg, io) |
| {} |
| |
| const std::unordered_map<std::string, ValueType>& |
| getManagedObjectsFromStore() const |
| { |
| return this->getManagedObjects(); |
| } |
| |
| const ManagedStorePriorityQueue& getManagedStorePriorityQueue() const |
| { |
| return this->getPriorityQueue(); |
| } |
| |
| MOCK_METHOD(void, getManagedObjectsFromDbusService, |
| (const std::string&, const sdbusplus::message::object_path&, |
| ManagedStoreCb&&), |
| (override)); |
| MOCK_METHOD(void, getManagedPropertiesMapFromDbusService, |
| (const std::string&, const sdbusplus::message::object_path&, |
| const std::string&, ManagedStoreCb&&), |
| (override)); |
| }; |
| |
| ValueType GenerateTestValue( |
| ManagedType type = ManagedType::kManagedObject, |
| std::chrono::milliseconds tfixedTestDuration = kTestShortDuration) |
| { |
| const auto now = clockNow(); |
| if (type == ManagedType::kManagedPropertyMap) |
| { |
| return ValueType( |
| dbus::utility::DBusPropertiesMap{}, |
| boost::system::errc::make_error_code(boost::system::errc::success), |
| now, now + tfixedTestDuration); |
| } |
| return ValueType( |
| dbus::utility::ManagedObjectType{}, |
| boost::system::errc::make_error_code(boost::system::errc::success), now, |
| now + tfixedTestDuration); |
| } |
| |
| TEST(ManagedStoreTest, TestPendingDbusCallsDoNotExceedThreshold) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, kPendingDbusResponseLow /*pendingDbusResponsesMax*/, |
| kTestShortDuration /*tfixed*/, kTestShortDuration /*tgrace*/, |
| kTestLongDuration /*tLRU*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| auto testVal = GenerateTestValue(); |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService(_, _, _)) |
| .Times(5) // 3 High priority + 2 low priority pending dbus calls allowed |
| .WillOnce( |
| ::testing::InvokeArgument<2>(testVal)) // High priority, upsert! |
| .WillOnce( |
| ::testing::InvokeArgument<2>(testVal)) // High priority, upsert! |
| .WillOnce( |
| ::testing::InvokeArgument<2>(testVal)) // High priority, upsert! |
| .WillOnce(Return()) // No response sent to ensure pending state |
| .WillOnce(Return()); // No response sent to ensure pending state |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "abc", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "efg", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| io->run_for(kTestShortDuration); |
| EXPECT_EQ(mockManagedStore->getStoreTracker().countPendingDbusResponses, 2); |
| } |
| |
| TEST(ManagedStoreTest, TestHighPriorityDbusCallsNotBlocked) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, |
| kPendingDbusResponseLow /*pendingDbusResponsesMax*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| int executionCount = 5; |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService).Times(1); |
| for (int i = 0; i < executionCount; ++i) |
| { |
| mockManagedStore->getManagedObjects( |
| "", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| } |
| } |
| |
| TEST(ManagedStoreTest, TestGetManagedObjectCallbackInvoked) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, |
| kPendingDbusResponseLow /*pendingDbusResponsesMax*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| size_t callbackInvoked = 0; |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService(_, _, _)) |
| .WillOnce(::testing::InvokeArgument<2>( |
| GenerateTestValue())); // High priority, upsert! |
| mockManagedStore->getManagedObjects( |
| "", {}, |
| [&callbackInvoked](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) { |
| ++callbackInvoked; |
| }); |
| EXPECT_EQ(callbackInvoked, 1); |
| } |
| |
| TEST(ManagedStoreTest, TestManagedStoreRefreshesObjectsInStoreAfterTfixed) |
| { |
| // Setup |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, |
| kPendingDbusResponseHigh /*pendingDbusResponsesMax*/, |
| kTestMediumDuration /*tfixed*/, kTestMediumDuration /*tgrace*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| const auto& objects = mockManagedStore->getManagedObjectsFromStore(); |
| |
| // Configure mock to repeatedly respond to refresh requests. |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService(_, _, _)) |
| .WillRepeatedly(::testing::InvokeArgument<2>(GenerateTestValue( |
| ManagedType::kManagedObject, kTestMediumDuration))); |
| |
| // First call for each object adds an entry in the store. |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "abc", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "efg", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| // Run scheduler for time less than tfixed to make sure no refreshes |
| io->run_for(kTestShortDuration); |
| |
| // No refresh until scheduled time |
| for (const auto& iter : objects) |
| { |
| EXPECT_EQ(0, iter.second.numRefreshesDone); |
| } |
| |
| // Now run the scheduler to make sure time just goes beyond tfixed and |
| // refresh occurs. |
| io->run_for(kTestMediumDuration); |
| // Refresh occurs when time > tfixed. |
| for (const auto& iter : objects) |
| { |
| EXPECT_GT(iter.second.numRefreshesDone, 1); |
| EXPECT_EQ(1, iter.second.numTimesUsed); |
| } |
| auto tracker = mockManagedStore->getStoreTracker(); |
| EXPECT_EQ(3, tracker.countGetManagedObjectsCacheMiss); |
| EXPECT_EQ(0, tracker.countGetManagedObjectsCacheHit); |
| EXPECT_EQ(0, tracker.countGetManagedObjectsCacheMissMaxAge); |
| |
| // second call: |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "abc", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "efg", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| // numTimesUsed is now at 2 |
| for (const auto& iter : mockManagedStore->getManagedObjectsFromStore()) |
| { |
| EXPECT_EQ(2, iter.second.numTimesUsed); |
| EXPECT_GT(iter.second.lastUsedAges->size(), 0); |
| } |
| tracker = mockManagedStore->getStoreTracker(); |
| EXPECT_EQ(3, tracker.countGetManagedObjectsCacheMiss); |
| EXPECT_EQ(3, tracker.countGetManagedObjectsCacheHit); |
| EXPECT_EQ(0, tracker.countGetManagedObjectsCacheMissMaxAge); |
| } |
| |
| TEST(ManagedStoreTest, TestObjectsInStoreAreEvictedAfterLruThreshold) |
| { |
| // Setup |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, |
| kPendingDbusResponseHigh /*pendingDbusResponsesMax*/, |
| std::chrono::milliseconds(kTestShortDuration) /*tfixed*/, |
| std::chrono::milliseconds(kTestShortDuration) /*tgrace*/, |
| std::chrono::milliseconds(kTestMediumDuration) /*tLRU*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| const auto& objects = mockManagedStore->getManagedObjectsFromStore(); |
| |
| // Configure mock to repeatedly respond to refresh requests. |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService(_, _, _)) |
| .WillRepeatedly(::testing::InvokeArgument<2>(GenerateTestValue())); |
| |
| // First call for each object adds an entry in the store. |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| // Run scheduler till the first refresh is due |
| // At this point the object is still fresh. |
| io->run_for(kTestShortDuration); |
| |
| // We expect the objects will be in store. |
| EXPECT_EQ(1, objects.size()); |
| |
| // Now run the scheduler to make sure time exceeds LRU threshold |
| io->run_for(kTestMediumDuration); |
| |
| // We expect the object to be evicted. |
| EXPECT_EQ(0, objects.size()); |
| } |
| |
| TEST(ManagedStoreTest, TestManagedStoreRefreshesOverdueObjects) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, 5 /*thresholdPendingDbusResponses*/, |
| kTestShortDuration /*tfixed*/, kTestShortDuration /*tgrace*/, |
| kTestLongDuration /*tLRU*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| auto testVal = GenerateTestValue(); |
| managedStore::ManagedObjectStore::ManagedStoreCb managedStoreCb; |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService(_, _, _)) |
| .Times(6) // 3 cache miss + 3 refresh call |
| .WillOnce( |
| ::testing::InvokeArgument<2>(testVal)) // High priority, upsert! |
| .WillOnce( |
| ::testing::InvokeArgument<2>(testVal)) // High priority, upsert! |
| .WillOnce( |
| ::testing::InvokeArgument<2>(testVal)) // High priority, upsert! |
| .WillOnce(Return()) // Refresh |
| .WillOnce(Return()) // Refresh |
| .WillOnce(Return()); // Refresh |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "abc", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| mockManagedStore->getManagedObjects( |
| "efg", {}, |
| [](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| // At this point the Cache is warm with 3 ManagedObjects. |
| // One object from the queue which has the earliest deadline will be pop'd |
| // to schedule a refresh after tfixed. So priority queue will only have 2 |
| // elements. |
| EXPECT_EQ(2, mockManagedStore->getManagedStorePriorityQueue().size()); |
| |
| // If we run io event loop now, all overdue objects should refresh and |
| // priority queue should get empty. |
| io->run_for(kTestShortDuration); |
| |
| // Check there are no objects in PQ |
| EXPECT_EQ(0, mockManagedStore->getManagedStorePriorityQueue().size()); |
| } |
| |
| TEST(ManagedStoreTest, TestManagedStorePreventsDuplicateReadThrough) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, 5 /*thresholdPendingDbusResponses*/, |
| kTestShortDuration /*tfixed*/, kTestShortDuration /*tgrace*/, |
| kTestLongDuration /*tLRU*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| auto testVal = GenerateTestValue(); |
| EXPECT_CALL(*mockManagedStore, getManagedObjectsFromDbusService(_, _, _)) |
| .Times(2) |
| .WillRepeatedly( |
| ::testing::InvokeArgument<2>(testVal)); // High priority, upsert! |
| size_t callbackInvoked = 0; |
| mockManagedStore->getManagedObjects( |
| "abc", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) { ++callbackInvoked; }); |
| // 2 Similar requests back to back to create the test scenario where |
| // managed store will have to block redundant request. |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) { ++callbackInvoked; }); |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) { ++callbackInvoked; }); |
| |
| // abc is scheduled and 1 xyz in priority queue waiting to be scheduled |
| // Note, we don't expect 2 xyz in priority queue since the store does not |
| // allow multiple read through requests. |
| EXPECT_EQ(1, mockManagedStore->getManagedStorePriorityQueue().size()); |
| // Make sure the callback is invoked for duplicate xyz request. |
| EXPECT_EQ(callbackInvoked, 3); |
| } |
| |
| TEST(ManagedStoreTest, TestGetAllProperties) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, |
| kPendingDbusResponseLow /*thresholdPendingDbusResponses*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| size_t callbackInvoked = 0; |
| EXPECT_CALL(*mockManagedStore, |
| getManagedPropertiesMapFromDbusService(_, _, _, _)) |
| .Times(1) |
| .WillRepeatedly(::testing::InvokeArgument<3>(GenerateTestValue( |
| ManagedType::kManagedPropertyMap))); // High priority, upsert! |
| std::shared_ptr<bmcweb::AsyncResp> asyncResp = |
| std::make_shared<bmcweb::AsyncResp>(); |
| managedStore::ManagedObjectStoreContext context(asyncResp); |
| mockManagedStore->getAllProperties( |
| "ObjectPath", "Interface", "Property", context, |
| [&callbackInvoked](const boost::system::error_code&, |
| const dbus::utility::DBusPropertiesMap&) { |
| ++callbackInvoked; |
| }); |
| EXPECT_EQ(callbackInvoked, 1); |
| |
| // At this point GetAllProperty should be stored and any further requests |
| // should be served from store. |
| mockManagedStore->getAllProperties( |
| "ObjectPath", "Interface", "Property", context, |
| [&callbackInvoked](const boost::system::error_code&, |
| const dbus::utility::DBusPropertiesMap&) { |
| ++callbackInvoked; |
| }); |
| EXPECT_EQ(callbackInvoked, 2); |
| } |
| |
| TEST(ManagedStoreTest, TestManagedStoreAvoidsDuplicatePqEntries) |
| { |
| auto io = std::make_shared<boost::asio::io_context>(); |
| managedStore::ManagedObjectStoreConfig config( |
| true /*isEnabled*/, 1 /*thresholdPendingDbusResponses*/, |
| kTestShortDuration /*tfixed*/, 0ms /*tgrace*/, |
| kTestLongDuration /*tLRU*/); |
| auto mockManagedStore = |
| std::make_shared<MockManagedObjectStore>(config, *io); |
| { |
| testing::InSequence s; |
| EXPECT_CALL(*mockManagedStore, |
| getManagedObjectsFromDbusService(_, _, _)) |
| .WillOnce(::testing::InvokeArgument<2>(GenerateTestValue())); |
| EXPECT_CALL(*mockManagedStore, |
| getManagedObjectsFromDbusService(_, _, _)) |
| .WillOnce(::testing::InvokeArgument<2>(GenerateTestValue())); |
| EXPECT_CALL(*mockManagedStore, |
| getManagedObjectsFromDbusService(_, _, _)) |
| .WillOnce(::testing::InvokeArgument<2>(GenerateTestValue())); |
| EXPECT_CALL(*mockManagedStore, |
| getManagedObjectsFromDbusService(_, _, _)) |
| .WillOnce(::testing::InvokeArgument<2>(GenerateTestValue())); |
| EXPECT_CALL(*mockManagedStore, |
| getManagedObjectsFromDbusService(_, _, _)) |
| .WillOnce(::testing::InvokeArgument<2>(GenerateTestValue())); |
| } |
| |
| mockManagedStore->getManagedObjects( |
| "abc", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| // Expect PQ size to be 0 since the store has only 1 entry at this point |
| // which will always be scheduled for refresh. |
| EXPECT_EQ(0, mockManagedStore->getManagedStorePriorityQueue().size()); |
| |
| // Add another entry. This time the entry should wait in PQ to be scheduled. |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| EXPECT_EQ(1, mockManagedStore->getManagedStorePriorityQueue().size()); |
| |
| // Make the entries in the PQ stale |
| std::this_thread::sleep_for(kTestShortDuration); |
| |
| mockManagedStore->getManagedObjects( |
| "def", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| EXPECT_EQ(2, mockManagedStore->getManagedStorePriorityQueue().size()); |
| |
| // Now send a request for object that has become stale in managedStore. |
| // We want to make sure pq size does not grow and the old pq entry for xyz |
| // is pop'd and gets requeued after "def" |
| mockManagedStore->getManagedObjects( |
| "xyz", {}, |
| [&](const boost::system::error_code&, |
| const dbus::utility::ManagedObjectType&) {}); |
| |
| // Expect PQ size to sill be 2 as managed store makes sure duplicate entries |
| // don't go into the priority queue. |
| EXPECT_EQ(2, mockManagedStore->getManagedStorePriorityQueue().size()); |
| |
| // Run io loop once to trigger scheduler to fetch new entries from pq. |
| io->run_one(); |
| |
| auto pq = mockManagedStore->getManagedStorePriorityQueue(); |
| |
| // We want to make sure that the old pq entry for xyz is pop'd and gets |
| // requeued after "def". "def" will be picked by the scheduler in that case |
| // and we should see "xyz" as the top entry in pq (which in error case |
| // would have been "abc") |
| EXPECT_EQ("0|xyz|", pq.top().managedStoreKey.toString()); |
| } |
| |
| } // namespace |
| } // namespace managedStore |