buffet: Fix issues in StateChangeQueue Addressed additional code review issues brought after the original code has been published. - NotifyPropertiesUpdated() now takes timestamp and property set as separate parameters to simplify implementation of StateManager. - StateChangeQueue::max_queue_size_ is made 'const' - StateChangeQueue now maintains a time-to-event map instead of a vector of StateChange. This allows to keep the events sorted by time stamp. Also adding discrete events with the same time stamp coalesces the property changes. - added a unit test for coalescing property set changes based on the timestamp. BUG=None TEST=FEATURES=test emerge-link buffet Change-Id: I309816d1f040558620fa68a844b05251d0e4319b Reviewed-on: https://chromium-review.googlesource.com/226300 Reviewed-by: Christopher Wiley <wiley@chromium.org> Reviewed-by: Anton Muhin <antonm@chromium.org> Commit-Queue: Alex Vakulenko <avakulenko@chromium.org> Tested-by: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/manager.cc b/buffet/manager.cc index d82faf6..bc9fdf8 100644 --- a/buffet/manager.cc +++ b/buffet/manager.cc
@@ -126,7 +126,10 @@ void Manager::HandleUpdateState( chromeos::ErrorPtr* error, const chromeos::VariantDictionary& property_set) { - state_manager_->UpdateProperties(property_set, error); + base::Time timestamp = base::Time::Now(); + for (const auto& pair : property_set) { + state_manager_->SetPropertyValue(pair.first, pair.second, timestamp, error); + } } void Manager::HandleAddCommand(
diff --git a/buffet/states/mock_state_change_queue_interface.h b/buffet/states/mock_state_change_queue_interface.h index eca3720..11a84b4 100644 --- a/buffet/states/mock_state_change_queue_interface.h +++ b/buffet/states/mock_state_change_queue_interface.h
@@ -16,7 +16,9 @@ class MockStateChangeQueueInterface : public StateChangeQueueInterface { public: MOCK_CONST_METHOD0(IsEmpty, bool()); - MOCK_METHOD1(NotifyPropertiesUpdated, bool(const StateChange&)); + MOCK_METHOD2(NotifyPropertiesUpdated, + bool(base::Time timestamp, + chromeos::VariantDictionary changed_properties)); MOCK_METHOD0(GetAndClearRecordedStateChanges, std::vector<StateChange>()); };
diff --git a/buffet/states/state_change_queue.cc b/buffet/states/state_change_queue.cc index a288c0f..663d799 100644 --- a/buffet/states/state_change_queue.cc +++ b/buffet/states/state_change_queue.cc
@@ -13,9 +13,21 @@ CHECK_GT(max_queue_size_, 0) << "Max queue size must not be zero"; } -bool StateChangeQueue::NotifyPropertiesUpdated(const StateChange& change) { +bool StateChangeQueue::NotifyPropertiesUpdated( + base::Time timestamp, + chromeos::VariantDictionary changed_properties) { DCHECK(thread_checker_.CalledOnValidThread()); - state_changes_.push_back(change); + auto it = state_changes_.lower_bound(timestamp); + if (it == state_changes_.end() || it->first != timestamp) { + // This timestamp doesn't exist. Insert a new element. + state_changes_.emplace_hint(it, timestamp, std::move(changed_properties)); + } else { + // Merge the old property set and the new one. + // For properties that exist in both old and new property sets, keep the + // new values. + changed_properties.insert(it->second.begin(), it->second.end()); + it->second = std::move(changed_properties); + } while (state_changes_.size() > max_queue_size_) { // Queue is full. // Merge the two oldest records into one. The merge strategy is: @@ -26,8 +38,8 @@ auto element_old = state_changes_.begin(); auto element_new = std::next(element_old); // This will skip elements that exist in both [old] and [new]. - element_new->property_set.insert(element_old->property_set.begin(), - element_old->property_set.end()); + element_new->second.insert(element_old->second.begin(), + element_old->second.end()); state_changes_.erase(element_old); } return true; @@ -35,8 +47,13 @@ std::vector<StateChange> StateChangeQueue::GetAndClearRecordedStateChanges() { DCHECK(thread_checker_.CalledOnValidThread()); - // Return the accumulated state changes and clear the current queue. - return std::move(state_changes_); + std::vector<StateChange> changes; + changes.reserve(state_changes_.size()); + for (const auto& pair : state_changes_) { + changes.emplace_back(pair.first, std::move(pair.second)); + } + state_changes_.clear(); + return changes; } } // namespace buffet
diff --git a/buffet/states/state_change_queue.h b/buffet/states/state_change_queue.h index a5cde95..2ca46e6 100644 --- a/buffet/states/state_change_queue.h +++ b/buffet/states/state_change_queue.h
@@ -5,6 +5,7 @@ #ifndef BUFFET_STATES_STATE_CHANGE_QUEUE_H_ #define BUFFET_STATES_STATE_CHANGE_QUEUE_H_ +#include <map> #include <vector> #include <base/macros.h> @@ -21,7 +22,9 @@ // Overrides from StateChangeQueueInterface. bool IsEmpty() const override { return state_changes_.empty(); } - bool NotifyPropertiesUpdated(const StateChange& change) override; + bool NotifyPropertiesUpdated( + base::Time timestamp, + chromeos::VariantDictionary changed_properties) override; std::vector<StateChange> GetAndClearRecordedStateChanges() override; private: @@ -32,10 +35,10 @@ // Maximum queue size. If it is full, the oldest state update records are // merged together until the queue size is within the size limit. - size_t max_queue_size_; + const size_t max_queue_size_; // Accumulated list of device state change notifications. - std::vector<StateChange> state_changes_; + std::map<base::Time, chromeos::VariantDictionary> state_changes_; DISALLOW_COPY_AND_ASSIGN(StateChangeQueue); };
diff --git a/buffet/states/state_change_queue_interface.h b/buffet/states/state_change_queue_interface.h index 5810bff..84704dd 100644 --- a/buffet/states/state_change_queue_interface.h +++ b/buffet/states/state_change_queue_interface.h
@@ -14,12 +14,14 @@ // A simple notification record event to track device state changes. // The |timestamp| records the time of the state change. -// |property_set| contains a property set with the new property values. -// The property set contains only the properties updated at the time the event -// was recorded. +// |changed_properties| contains a property set with the new property values +// which were updated at the time the event was recorded. struct StateChange { + StateChange(base::Time time, + chromeos::VariantDictionary properties) + : timestamp(time), changed_properties(std::move(properties)) {} base::Time timestamp; - chromeos::VariantDictionary property_set; + chromeos::VariantDictionary changed_properties; }; // An abstract interface to StateChangeQueue to record and retrieve state @@ -30,7 +32,9 @@ virtual bool IsEmpty() const = 0; // Called by StateManager when device state properties are updated. - virtual bool NotifyPropertiesUpdated(const StateChange& change) = 0; + virtual bool NotifyPropertiesUpdated( + base::Time timestamp, + chromeos::VariantDictionary changed_properties) = 0; // Returns the recorded state changes since last time this method was called. virtual std::vector<StateChange> GetAndClearRecordedStateChanges() = 0;
diff --git a/buffet/states/state_change_queue_unittest.cc b/buffet/states/state_change_queue_unittest.cc index 8205f77..7aa6c9f 100644 --- a/buffet/states/state_change_queue_unittest.cc +++ b/buffet/states/state_change_queue_unittest.cc
@@ -28,63 +28,111 @@ } TEST_F(StateChangeQueueTest, UpdateOne) { - StateChange change; - change.timestamp = base::Time::Now(); - change.property_set.emplace("prop.name", int{23}); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change)); + StateChange change{ + base::Time::Now(), + chromeos::VariantDictionary{{"prop.name", int{23}}} + }; + ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change.timestamp, + change.changed_properties)); EXPECT_FALSE(queue_->IsEmpty()); auto changes = queue_->GetAndClearRecordedStateChanges(); ASSERT_EQ(1, changes.size()); EXPECT_EQ(change.timestamp, changes.front().timestamp); - EXPECT_EQ(change.property_set, changes.front().property_set); + EXPECT_EQ(change.changed_properties, changes.front().changed_properties); EXPECT_TRUE(queue_->IsEmpty()); EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty()); } TEST_F(StateChangeQueueTest, UpdateMany) { - StateChange change1; - change1.timestamp = base::Time::Now(); - change1.property_set.emplace("prop.name1", int{23}); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change1)); - StateChange change2; - change2.timestamp = base::Time::Now(); - change2.property_set.emplace("prop.name1", int{17}); - change2.property_set.emplace("prop.name2", double{1.0}); - change2.property_set.emplace("prop.name3", bool{false}); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change2)); + StateChange change1{ + base::Time::Now(), + chromeos::VariantDictionary{{"prop.name1", int{23}}} + }; + ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change1.timestamp, + change1.changed_properties)); + StateChange change2{ + base::Time::Now(), + chromeos::VariantDictionary{ + {"prop.name1", int{17}}, + {"prop.name2", double{1.0}}, + {"prop.name3", bool{false}}, + } + }; + ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change2.timestamp, + change2.changed_properties)); EXPECT_FALSE(queue_->IsEmpty()); auto changes = queue_->GetAndClearRecordedStateChanges(); ASSERT_EQ(2, changes.size()); - EXPECT_EQ(change1.timestamp, changes.front().timestamp); - EXPECT_EQ(change1.property_set, changes.front().property_set); - EXPECT_EQ(change2.timestamp, changes.back().timestamp); - EXPECT_EQ(change2.property_set, changes.back().property_set); + EXPECT_EQ(change1.timestamp, changes[0].timestamp); + EXPECT_EQ(change1.changed_properties, changes[0].changed_properties); + EXPECT_EQ(change2.timestamp, changes[1].timestamp); + EXPECT_EQ(change2.changed_properties, changes[1].changed_properties); EXPECT_TRUE(queue_->IsEmpty()); EXPECT_TRUE(queue_->GetAndClearRecordedStateChanges().empty()); } +TEST_F(StateChangeQueueTest, GroupByTimestamp) { + base::Time timestamp = base::Time::Now(); + base::TimeDelta time_delta = base::TimeDelta::FromMinutes(1); + + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + timestamp, + chromeos::VariantDictionary{{"prop.name1", int{1}}})); + + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + timestamp, + chromeos::VariantDictionary{{"prop.name2", int{2}}})); + + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + timestamp, + chromeos::VariantDictionary{{"prop.name1", int{3}}})); + + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + timestamp + time_delta, + chromeos::VariantDictionary{{"prop.name1", int{4}}})); + + auto changes = queue_->GetAndClearRecordedStateChanges(); + ASSERT_EQ(2, changes.size()); + + chromeos::VariantDictionary expected1{ + {"prop.name1", int{3}}, + {"prop.name2", int{2}}, + }; + chromeos::VariantDictionary expected2{ + {"prop.name1", int{4}}, + }; + EXPECT_EQ(timestamp, changes[0].timestamp); + EXPECT_EQ(expected1, changes[0].changed_properties); + EXPECT_EQ(timestamp + time_delta, changes[1].timestamp); + EXPECT_EQ(expected2, changes[1].changed_properties); +} + TEST_F(StateChangeQueueTest, MaxQueueSize) { queue_.reset(new StateChangeQueue(2)); base::Time start_time = base::Time::Now(); - base::TimeDelta time_delta = base::TimeDelta::FromMinutes(1); + base::TimeDelta time_delta1 = base::TimeDelta::FromMinutes(1); + base::TimeDelta time_delta2 = base::TimeDelta::FromMinutes(3); - StateChange change; - change.timestamp = start_time; - change.property_set.emplace("prop.name1", int{1}); - change.property_set.emplace("prop.name2", int{2}); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change)); + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + start_time, + chromeos::VariantDictionary{ + {"prop.name1", int{1}}, + {"prop.name2", int{2}}, + })); - change.timestamp += time_delta; - change.property_set.clear(); - change.property_set.emplace("prop.name1", int{3}); - change.property_set.emplace("prop.name3", int{4}); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change)); + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + start_time + time_delta1, + chromeos::VariantDictionary{ + {"prop.name1", int{3}}, + {"prop.name3", int{4}}, + })); - change.timestamp += time_delta; - change.property_set.clear(); - change.property_set.emplace("prop.name10", int{10}); - change.property_set.emplace("prop.name11", int{11}); - ASSERT_TRUE(queue_->NotifyPropertiesUpdated(change)); + ASSERT_TRUE(queue_->NotifyPropertiesUpdated( + start_time + time_delta2, + chromeos::VariantDictionary{ + {"prop.name10", int{10}}, + {"prop.name11", int{11}}, + })); auto changes = queue_->GetAndClearRecordedStateChanges(); ASSERT_EQ(2, changes.size()); @@ -94,15 +142,15 @@ {"prop.name2", int{2}}, {"prop.name3", int{4}}, }; - EXPECT_EQ(start_time + time_delta, changes.front().timestamp); - EXPECT_EQ(expected1, changes.front().property_set); + EXPECT_EQ(start_time + time_delta1, changes[0].timestamp); + EXPECT_EQ(expected1, changes[0].changed_properties); chromeos::VariantDictionary expected2{ {"prop.name10", int{10}}, {"prop.name11", int{11}}, }; - EXPECT_EQ(start_time + 2 * time_delta, changes.back().timestamp); - EXPECT_EQ(expected2, changes.back().property_set); + EXPECT_EQ(start_time + time_delta2, changes[1].timestamp); + EXPECT_EQ(expected2, changes[1].changed_properties); } } // namespace buffet
diff --git a/buffet/states/state_manager.cc b/buffet/states/state_manager.cc index f31801f..7511928 100644 --- a/buffet/states/state_manager.cc +++ b/buffet/states/state_manager.cc
@@ -79,9 +79,10 @@ return dict; } -bool StateManager::UpdatePropertyValue(const std::string& full_property_name, - const chromeos::Any& value, - chromeos::ErrorPtr* error) { +bool StateManager::SetPropertyValue(const std::string& full_property_name, + const chromeos::Any& value, + const base::Time& timestamp, + chromeos::ErrorPtr* error) { std::string package_name; std::string property_name; bool split = chromeos::string_utils::SplitAtFirst( @@ -106,34 +107,11 @@ package_name.c_str()); return false; } - return package->SetPropertyValue(property_name, value, error); -} - -bool StateManager::SetPropertyValue(const std::string& full_property_name, - const chromeos::Any& value, - chromeos::ErrorPtr* error) { - if (!UpdatePropertyValue(full_property_name, value, error)) + if (!package->SetPropertyValue(property_name, value, error)) return false; - StateChange change; - change.timestamp = base::Time::Now(); - change.property_set.emplace(full_property_name, value); - state_change_queue_->NotifyPropertiesUpdated(change); - return true; -} - -bool StateManager::UpdateProperties( - const chromeos::VariantDictionary& property_set, - chromeos::ErrorPtr* error) { - for (const auto& pair : property_set) { - if (!UpdatePropertyValue(pair.first, pair.second, error)) - return false; - } - - StateChange change; - change.timestamp = base::Time::Now(); - change.property_set = property_set; - state_change_queue_->NotifyPropertiesUpdated(change); + chromeos::VariantDictionary prop_set{{full_property_name, value}}; + state_change_queue_->NotifyPropertiesUpdated(timestamp, prop_set); return true; }
diff --git a/buffet/states/state_manager.h b/buffet/states/state_manager.h index f17e8a6..53695e2 100644 --- a/buffet/states/state_manager.h +++ b/buffet/states/state_manager.h
@@ -21,6 +21,7 @@ namespace base { class DictionaryValue; class FilePath; +class Time; } // namespace base namespace buffet { @@ -45,11 +46,7 @@ // name of the property to update in format "package.property". bool SetPropertyValue(const std::string& full_property_name, const chromeos::Any& value, - chromeos::ErrorPtr* error); - - // Updates a number of state properties in one shot. - // |property_set| is a (full_property_name)-to-(property_value) map. - bool UpdateProperties(const chromeos::VariantDictionary& property_set, + const base::Time& timestamp, chromeos::ErrorPtr* error); // Returns all the categories the state properties are registered from. @@ -64,11 +61,6 @@ std::vector<StateChange> GetAndClearRecordedStateChanges(); private: - // Helper method to be used with SetPropertyValue() and UpdateProperties() - bool UpdatePropertyValue(const std::string& full_property_name, - const chromeos::Any& value, - chromeos::ErrorPtr* error); - // Loads a device state fragment from a JSON object. |category| represents // a device daemon providing the state fragment or empty string for the // base state fragment.
diff --git a/buffet/states/state_manager_unittest.cc b/buffet/states/state_manager_unittest.cc index b4c3094..df2260a 100644 --- a/buffet/states/state_manager_unittest.cc +++ b/buffet/states/state_manager_unittest.cc
@@ -44,11 +44,6 @@ })"); } -MATCHER_P(IsStateChange, prop_set, "") { - return arg.property_set == prop_set && - std::abs((arg.timestamp - base::Time::Now()).InSeconds()) < 2; -} - } // anonymous namespace class StateManagerTest : public ::testing::Test { @@ -56,7 +51,8 @@ void SetUp() override { // Initial expectations. EXPECT_CALL(mock_state_change_queue_, IsEmpty()).Times(0); - EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_)).Times(0); + EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_, _)) + .Times(0); EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges()) .Times(0); mgr_.reset(new StateManager(&mock_state_change_queue_)); @@ -109,33 +105,22 @@ chromeos::VariantDictionary expected_prop_set{ {"terminator.target", std::string{"John Connor"}}, }; + base::Time timestamp = base::Time::Now(); EXPECT_CALL(mock_state_change_queue_, - NotifyPropertiesUpdated(IsStateChange(expected_prop_set))) + NotifyPropertiesUpdated(timestamp, expected_prop_set)) .WillOnce(Return(true)); ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target", - std::string{"John Connor"}, nullptr)); + std::string{"John Connor"}, + timestamp, + nullptr)); EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000'}," "'terminator':{'target':'John Connor'}}", ValueToString(mgr_->GetStateValuesAsJson(nullptr).get())); } -TEST_F(StateManagerTest, UpdateProperties) { - chromeos::VariantDictionary prop_set{ - {"base.serialNumber", std::string{"T1000.1"}}, - {"terminator.target", std::string{"Sarah Connor"}}, - }; - EXPECT_CALL(mock_state_change_queue_, - NotifyPropertiesUpdated(IsStateChange(prop_set))) - .WillOnce(Return(true)); - ASSERT_TRUE(mgr_->UpdateProperties(prop_set, nullptr)); - EXPECT_EQ("{'base':{'manufacturer':'Skynet','serialNumber':'T1000.1'}," - "'terminator':{'target':'Sarah Connor'}}", - ValueToString(mgr_->GetStateValuesAsJson(nullptr).get())); -} - TEST_F(StateManagerTest, SetPropertyValue_Error_NoName) { chromeos::ErrorPtr error; - ASSERT_FALSE(mgr_->SetPropertyValue("", int{0}, &error)); + ASSERT_FALSE(mgr_->SetPropertyValue("", int{0}, base::Time::Now(), &error)); EXPECT_EQ(errors::state::kDomain, error->GetDomain()); EXPECT_EQ(errors::state::kPropertyNameMissing, error->GetCode()); EXPECT_EQ("Property name is missing", error->GetMessage()); @@ -143,7 +128,8 @@ TEST_F(StateManagerTest, SetPropertyValue_Error_NoPackage) { chromeos::ErrorPtr error; - ASSERT_FALSE(mgr_->SetPropertyValue("target", int{0}, &error)); + ASSERT_FALSE(mgr_->SetPropertyValue("target", int{0}, base::Time::Now(), + &error)); EXPECT_EQ(errors::state::kDomain, error->GetDomain()); EXPECT_EQ(errors::state::kPackageNameMissing, error->GetCode()); EXPECT_EQ("Package name is missing in the property name", @@ -152,7 +138,8 @@ TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownPackage) { chromeos::ErrorPtr error; - ASSERT_FALSE(mgr_->SetPropertyValue("power.level", int{0}, &error)); + ASSERT_FALSE(mgr_->SetPropertyValue("power.level", int{0}, base::Time::Now(), + &error)); EXPECT_EQ(errors::state::kDomain, error->GetDomain()); EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); EXPECT_EQ("Unknown state property package 'power'", error->GetMessage()); @@ -160,28 +147,33 @@ TEST_F(StateManagerTest, SetPropertyValue_Error_UnknownProperty) { chromeos::ErrorPtr error; - ASSERT_FALSE(mgr_->SetPropertyValue("base.level", int{0}, &error)); + ASSERT_FALSE(mgr_->SetPropertyValue("base.level", int{0}, base::Time::Now(), + &error)); EXPECT_EQ(errors::state::kDomain, error->GetDomain()); EXPECT_EQ(errors::state::kPropertyNotDefined, error->GetCode()); EXPECT_EQ("State property 'base.level' is not defined", error->GetMessage()); } -TEST_F(StateManagerTest, RetrievePropertyChanges) { - EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(_)) +TEST_F(StateManagerTest, GetAndClearRecordedStateChanges) { + base::Time timestamp = base::Time::Now(); + EXPECT_CALL(mock_state_change_queue_, NotifyPropertiesUpdated(timestamp, _)) .WillOnce(Return(true)); ASSERT_TRUE(mgr_->SetPropertyValue("terminator.target", - std::string{"John Connor"}, nullptr)); + std::string{"John Connor"}, + timestamp, + nullptr)); std::vector<StateChange> expected_val; - expected_val.emplace_back(); - expected_val.back().timestamp = base::Time::Now(); - expected_val.back().property_set.emplace("terminator.target", - std::string{"John Connor"}); + expected_val.emplace_back( + timestamp, + chromeos::VariantDictionary{{"terminator.target", + std::string{"John Connor"}}}); EXPECT_CALL(mock_state_change_queue_, GetAndClearRecordedStateChanges()) .WillOnce(Return(expected_val)); auto changes = mgr_->GetAndClearRecordedStateChanges(); ASSERT_EQ(1, changes.size()); EXPECT_EQ(expected_val.back().timestamp, changes.back().timestamp); - EXPECT_EQ(expected_val.back().property_set, changes.back().property_set); + EXPECT_EQ(expected_val.back().changed_properties, + changes.back().changed_properties); }