From 2ff7a0dd5480b315e02d5f189714e32695d6b20a Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 29 May 2019 13:44:57 -0400 Subject: [PATCH 01/27] initial WatchMap snapshot Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 177 +++++++++++++++++++++++++++ source/common/config/watch_map.h | 100 +++++++++++++++ test/common/config/watch_map_test.cc | 64 ++++++++++ 3 files changed, 341 insertions(+) create mode 100644 source/common/config/watch_map.cc create mode 100644 source/common/config/watch_map.h create mode 100644 test/common/config/watch_map_test.cc diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc new file mode 100644 index 0000000000000..6823006789796 --- /dev/null +++ b/source/common/config/watch_map.cc @@ -0,0 +1,177 @@ +#include "common/config/watch_map.h" + +namespace Envoy { +namespace Config { + +Token WatchMap::addWatch(SubscriptionCallbacks& callbacks) { + Token next_watch = next_watch_++; + watches_.emplace(next_watch, Watch(callbacks)); +} + +bool WatchMap::removeWatch(Token token) { + watches_.erase(token); + return watches_.empty(); +} + +std::pair, std::set> +WatchMap::updateWatchInterest(Token token, const std::set& update_to_these_names) { + auto watches_entry = watches_.find(token); + if (watches_entry == watches_.end()) { + ENVOY_LOG(error, "updateWatchInterest() called on nonexistent token!"); + return; + } + auto& watch = watches_entry.second; + + std::vector newly_added_to_watch; + std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), + watch.resource_names_.begin(), watch.resource_names_.end(), + std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); + + std::vector newly_removed_from_watch; + std::set_difference(watch.resource_names_.begin(), watch.resource_names_.end(), + update_to_these_names.begin(), update_to_these_names.end(), + std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); + + watch.resource_names_ = update_to_these_names; + + return std::make_pair(findAdditions(newly_added_to_watch, token), + findRemovals(newly_removed_from_watch, token)); +} + +const absl::flat_hash_set& WatchMap::tokensInterestedIn(const std::string& resource_name) { + auto entry = watch_interest_.find(resource_name); + if (entry == watch_interest_.end()) { + return {}; + } + return entry.second; +} + +void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) { + if (watches_.empty()) { + ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); + return; + } + GrpcMuxCallbacks& name_getter = watches_.front()->callbacks_; + + // Build a map from watches, to the set of updated resources that each watch cares about. Each + // entry in the map is then a nice little bundle that can be fed directly into the individual + // onConfigUpdate()s. + absl::flat_hash_map> per_watch_updates; + for (const auto& r : resources) { + const absl::flat_hash_set& interested_in_r = + tokensInterestedIn(name_getter.resourceName(r)); + for (interested_token : interested_in_r) { + per_watch_updates[interested_token].Add()->CopyFrom(r); + } + } + + // We just bundled up the updates into nice per-watch packages. Now, deliver them. + for (const auto& updated : per_watch_updates) { + auto entry = watches_.find(updated.first); + if (entry == watches_.end()) { + ENVOY_LOG(error, "A token referred to by watch_interest_ is not present in watches_!"); + continue; + } + entry.second.callbacks_.onConfigUpdate(updated.second, version_info); + } +} + +void WatchMap::tryDeliverConfigUpdate( + Token token, const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) { + auto entry = watches_.find(token); + if (entry == watches_.end()) { + ENVOY_LOG(error, "A token referred to by watch_interest_ is not present in watches_!"); + return; + } + entry.second.callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); +} + +void WatchMap::onConfigUpdate( + const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) { + if (watches_.empty()) { + ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); + return; + } + // Build two maps: from watches, to the set of resources {added,removed} that each watch cares + // about. Each entry in the map-pair is then a nice little bundle that can be fed directly into + // the individual onConfigUpdate()s. + absl::flat_hash_map> per_watch_added; + absl::flat_hash_map> per_watch_removed; + for (const auto& r : added_resources) { + const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r.name()); + for (interested_token : interested_in_r) { + per_watch_added[interested_token].Add()->CopyFrom(r); + } + } + for (const auto& r : removed_resources) { + const absl::flat_hash_set& tokens_interested_in_r = tokensInterestedIn(r); + for (interested_token : tokens_interested_in_r.second) { + *per_watch_removed[interested_token].Add() = r; + } + } + + // We just bundled up the updates into nice per-watch packages. Now, deliver them. + for (const auto& added : per_watch_added) { + auto removed = per_watch_removed.find(added.first); + if (removed == per_watch_removed.end()) { + // additions only, no removals + tryDeliverConfigUpdate(added.first, added.second, {}, system_version_info); + } else { + // both additions and removals + tryDeliverConfigUpdate(added.first, added.second, removed.second, system_version_info); + // Drop the removals now, so the final removals-only pass won't use them. + per_watch_removed.erase(removed); + } + } + // Any removals-only updates will not have been picked up in the per_watch_added loop. + for (const auto& removed : per_watch_removed) { + tryDeliverConfigUpdate(removed.first, {}, removed.second, system_version_info); + } +} + +void WatchMap::onConfigUpdateFailed(const EnvoyException* e) { + for (auto& watch : watches_) { + watch.second.onConfigUpdateFailed(e); + } +} + +std::set WatchMap::findAdditions(const std::vector& newly_added_to_watch, + Token token) { + std::set newly_added_to_subscription; + for (const auto& name : newly_added_to_watch) { + auto entry = watch_interest_.find(name); + if (entry == watch_interest_.end()) { + newly_added_to_subscription.insert(name); + watch_interest_.emplace(name, {token}) + } else { + entry.second.insert(token); + } + } + return newly_added_to_subscription; +} + +std::set +WatchMap::findRemovals(const std::vector& newly_removed_from_watch, Token token) { + std::set newly_removed_from_subscription; + for (const auto& name : newly_removed_from_watch) { + auto entry = watch_interest_.find(name); + if (entry == watch_interest_.end()) { + ENVOY_LOG(warn, "WatchMap: tried to remove a watch from untracked resource {}", name); + continue; + } + entry.second.erase(token); + if (entry.second.empty()) { + watch_interest_.erase(entry); + } + newly_removed_from_subscription.insert(name); + } + return newly_removed_from_subscription; +} + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h new file mode 100644 index 0000000000000..49126b8ead0cd --- /dev/null +++ b/source/common/config/watch_map.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include + +namespace Envoy { +namespace Config { + +// Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same +// resource name "X". The xDS machinery must return to each their very own subscription to X. +// The xDS machinery's "watch" concept accomplishes that, while avoiding parallel reduntant xDS +// requests for X. Each of those subscriptions is viewed as a "watch" on X, while behind the scenes +// there is just a single real subscription to that resource name. +// This class maintains the mapping between those two: it +// 1) delivers updates to all interested watches, and +// 2) adds/removes resource names to/from the subscription when first/last watch is added/removed. +// +// #1 is accomplished by WatchMap's implementation of the SubscriptionCallbacks interface. +// This interface allows the xDS client to just throw each xDS update message it receives directly +// into WatchMap::onConfigUpdate, rather than having to track the various watches' callbacks. +// +// A WatchMap is assumed to be dedicated to a single type_url type of resource (EDS, CDS, etc). +class WatchMap : public SubscriptionCallbacks { +public: + // An opaque token given out to users of WatchMap, to identify a given watch. + using Token = uint64_t; + + // Adds 'callbacks' to the WatchMap, with no resource names being watched. (Use + // updateWatchInterest() to add some names). + Token addWatch(SubscriptionCallbacks& callbacks); + + // Returns true if this was the very last watch in the map. + // Expects that the watch to be removed has already had all of its resource names removed via + // updateWatchInterest(). + bool removeWatch(Token token); + + // Updates the set of resource names that the given watch should watch. + // Returns any resource name additions/removals that are unique across all watches. That is: + // 1) if 'resources' contains X and no other watch cares about X, X will be in pair.first. + // 2) if 'resources' does not contain Y, and this watch was the only one that cared about Y, + // Y will be in pair.second. + std::pair, std::set> + updateWatchInterest(Token token, const std::set& update_to_these_names); + + // SubscriptionCallbacks + virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + + virtual void + onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; + + virtual void onConfigUpdateFailed(const EnvoyException* e) override; + + virtual std::string resourceName(const ProtobufWkt::Any& resource) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + +private: + struct Watch { + std::set resource_names_; // must be sorted set, for set_difference. + GrpcMuxCallbacks& callbacks_; + }; + + // Given a list of names that are new to an individual watch, returns those names that are in fact + // new to the entire subscription. + std::set findAdditions(const std::vector& newly_added_to_watch, + Token token); + + // Given a list of names that an individual watch no longer cares about, returns those names that + // in fact the entire subscription no longer cares about. + std::set findRemovals(const std::vector& newly_removed_from_watch, + Token token); + + // Calls watches_[token].callbacks_.onConfigUpdate(), or logs an error if token isn't in watches_. + void tryDeliverConfigUpdate( + Token token, const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info); + + // Does a lookup in watch_interest_, returning empty set if not found. + const absl::flat_hash_set& tokensInterestedIn(const std::string& resource_name); + + absl::flat_hash_map watches_; + + // Maps a resource name to the set of watches interested in that resource. Has two purposes: + // 1) Acts as a reference count; no watches care anymore ==> the resource can be removed. + // 2) Enables efficient lookup of all interested watches when a resource has been updated. + absl::flat_hash_map> watch_interest_; + + Token next_watch_{0}; + + WatchMap(const WatchMap&) = delete; + WatchMap& operator=(const WatchMap&) = delete; +}; + +} // namespace Config +} // namespace Envoy diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc new file mode 100644 index 0000000000000..32d81b2534ec2 --- /dev/null +++ b/test/common/config/watch_map_test.cc @@ -0,0 +1,64 @@ +#include + +#include "envoy/api/v2/eds.pb.h" +#include "envoy/common/exception.h" +#include "envoy/stats/scope.h" + +#include "common/config/watch_map.h" + +#include "test/mocks/config/mocks.h" +#include "test/mocks/event/mocks.h" +#include "test/mocks/filesystem/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/stats/mocks.h" +#include "test/mocks/upstream/mocks.h" +#include "test/test_common/environment.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using ::testing::_; +using ::testing::Invoke; +using ::testing::Return; + +namespace Envoy { +namespace Config { +namespace { + +TEST(WatchMapTest, Basic) { + MockSubscriptionCallbacks callbacks; + WatchMap watch_map; + WatchToken token = watch_map.addWatch(callbacks); + std::set update_to({"alice", "bob"}); + std::pair, std::set> added_removed = + updateWatchInterest(token, update_to); + EXPECT_EQ(update_to, added_removed.first); + EXPECT_TRUE(added_removed.second.empty()); + + Protobuf::RepeatedPtrField expected_resources; + + // TODO TODO can it be this? EXPECT_CALL(callbacks, + // onConfigUpdate(RepeatedProtoEq(expected_resources), "version1")); + + EXPECT_CALL(callbacks, onConfigUpdate(_, "version1")) + .WillOnce(Invoke( + [](const Protobuf::RepeatedPtrField& resources, const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment expected_assignment; + resources[0].UnpackTo(&expected_assignment); + EXPECT_TRUE(TestUtility::protoEqual(expected_assignment, load_assignment)); + })); + + watch_map.onConfigUpdate(expected_resources, "version1"); + + std::pair, std::set> added_removed2 = + updateWatchInterest(token, {}); + + EXPECT_TRUE(watch_map.removeWatch(token)); +} + +} // namespace +} // namespace Config +} // namespace Envoy From 3816de1d36cd996e7c0c23f8e3ebe244f286a9db Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 29 May 2019 14:14:24 -0400 Subject: [PATCH 02/27] cleanup and comments Signed-off-by: Fred Douglas --- include/envoy/config/grpc_mux.h | 1 + source/common/config/watch_map.cc | 15 ++++++++------- source/common/config/watch_map.h | 17 +++++++++++------ 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/include/envoy/config/grpc_mux.h b/include/envoy/config/grpc_mux.h index fb66a78abdcbb..ebfe7dab526f5 100644 --- a/include/envoy/config/grpc_mux.h +++ b/include/envoy/config/grpc_mux.h @@ -26,6 +26,7 @@ struct ControlPlaneStats { ALL_CONTROL_PLANE_STATS(GENERATE_COUNTER_STRUCT,GENERATE_GAUGE_STRUCT) }; +// TODO(fredlas) reduntant to SubscriptionCallbacks; remove this one. class GrpcMuxCallbacks { public: virtual ~GrpcMuxCallbacks() {} diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 6823006789796..2dd2a2ff588f9 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -52,7 +52,7 @@ void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); return; } - GrpcMuxCallbacks& name_getter = watches_.front()->callbacks_; + SubscriptionCallbacks& name_getter = watches_.front()->callbacks_; // Build a map from watches, to the set of updated resources that each watch cares about. Each // entry in the map is then a nice little bundle that can be fed directly into the individual @@ -97,9 +97,9 @@ void WatchMap::onConfigUpdate( ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); return; } - // Build two maps: from watches, to the set of resources {added,removed} that each watch cares - // about. Each entry in the map-pair is then a nice little bundle that can be fed directly into - // the individual onConfigUpdate()s. + // Build a pair of maps: from watches, to the set of resources {added,removed} that each watch + // cares about. Each entry in the map-pair is then a nice little bundle that can be fed directly + // into the individual onConfigUpdate()s. absl::flat_hash_map> per_watch_added; absl::flat_hash_map> per_watch_removed; for (const auto& r : added_resources) { @@ -117,13 +117,14 @@ void WatchMap::onConfigUpdate( // We just bundled up the updates into nice per-watch packages. Now, deliver them. for (const auto& added : per_watch_added) { - auto removed = per_watch_removed.find(added.first); + const Token& cur_token = added.first; + auto removed = per_watch_removed.find(cur_token); if (removed == per_watch_removed.end()) { // additions only, no removals - tryDeliverConfigUpdate(added.first, added.second, {}, system_version_info); + tryDeliverConfigUpdate(cur_token, added.second, {}, system_version_info); } else { // both additions and removals - tryDeliverConfigUpdate(added.first, added.second, removed.second, system_version_info); + tryDeliverConfigUpdate(cur_token, added.second, removed.second, system_version_info); // Drop the removals now, so the final removals-only pass won't use them. per_watch_removed.erase(removed); } diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 49126b8ead0cd..b0975463af8d1 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -12,22 +12,28 @@ namespace Config { // The xDS machinery's "watch" concept accomplishes that, while avoiding parallel reduntant xDS // requests for X. Each of those subscriptions is viewed as a "watch" on X, while behind the scenes // there is just a single real subscription to that resource name. -// This class maintains the mapping between those two: it +// +// This class maintains the watches<-->subscription mapping: it // 1) delivers updates to all interested watches, and -// 2) adds/removes resource names to/from the subscription when first/last watch is added/removed. +// 2) tracks which resource names should be {added to,removed from} the subscription when the +// {first,last} watch on a resource name is {added,removed}. // // #1 is accomplished by WatchMap's implementation of the SubscriptionCallbacks interface. // This interface allows the xDS client to just throw each xDS update message it receives directly // into WatchMap::onConfigUpdate, rather than having to track the various watches' callbacks. // +// The information for #2 is returned by updateWatchInterest(); the caller should use it to +// update the subscription accordingly. +// // A WatchMap is assumed to be dedicated to a single type_url type of resource (EDS, CDS, etc). class WatchMap : public SubscriptionCallbacks { public: // An opaque token given out to users of WatchMap, to identify a given watch. using Token = uint64_t; - // Adds 'callbacks' to the WatchMap, with no resource names being watched. (Use - // updateWatchInterest() to add some names). + // Adds 'callbacks' to the WatchMap, with no resource names being watched. + // (Use updateWatchInterest() to add some names). + // Returns a new token identifying the newly added watch. Token addWatch(SubscriptionCallbacks& callbacks); // Returns true if this was the very last watch in the map. @@ -46,7 +52,6 @@ class WatchMap : public SubscriptionCallbacks { // SubscriptionCallbacks virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) override; - virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, const Protobuf::RepeatedPtrField& removed_resources, @@ -61,7 +66,7 @@ class WatchMap : public SubscriptionCallbacks { private: struct Watch { std::set resource_names_; // must be sorted set, for set_difference. - GrpcMuxCallbacks& callbacks_; + SubscriptionCallbacks& callbacks_; }; // Given a list of names that are new to an individual watch, returns those names that are in fact From 2d7a2470f2d780264c8cc1a15e0aaf35d20f097b Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 29 May 2019 17:08:24 -0400 Subject: [PATCH 03/27] compiles and a basic test passes Signed-off-by: Fred Douglas --- source/common/config/BUILD | 12 +++++ source/common/config/watch_map.cc | 71 +++++++++++++++------------- source/common/config/watch_map.h | 18 ++++++- test/common/config/BUILD | 11 +++++ test/common/config/watch_map_test.cc | 38 +++++++-------- 5 files changed, 96 insertions(+), 54 deletions(-) diff --git a/source/common/config/BUILD b/source/common/config/BUILD index b35a9598bb07d..2ef6511ebe347 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -386,3 +386,15 @@ envoy_cc_library( "//source/common/protobuf", ], ) + +envoy_cc_library( + name = "watch_map_lib", + srcs = ["watch_map.cc"], + hdrs = ["watch_map.h"], + deps = [ + "//include/envoy/config:subscription_interface", + "//source/common/common:assert_lib", + "//source/common/common:minimal_logger_lib", + "//source/common/protobuf", + ], +) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 2dd2a2ff588f9..ba21ba9d86a81 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -3,24 +3,26 @@ namespace Envoy { namespace Config { -Token WatchMap::addWatch(SubscriptionCallbacks& callbacks) { - Token next_watch = next_watch_++; - watches_.emplace(next_watch, Watch(callbacks)); +WatchMap::Token WatchMap::addWatch(SubscriptionCallbacks& callbacks) { + WatchMap::Token next_watch = next_watch_++; + watches_.emplace(next_watch, WatchMap::Watch(callbacks)); + return next_watch; } -bool WatchMap::removeWatch(Token token) { +bool WatchMap::removeWatch(WatchMap::Token token) { watches_.erase(token); return watches_.empty(); } std::pair, std::set> -WatchMap::updateWatchInterest(Token token, const std::set& update_to_these_names) { +WatchMap::updateWatchInterest(WatchMap::Token token, + const std::set& update_to_these_names) { auto watches_entry = watches_.find(token); if (watches_entry == watches_.end()) { ENVOY_LOG(error, "updateWatchInterest() called on nonexistent token!"); - return; + return std::make_pair(std::set(), std::set()); } - auto& watch = watches_entry.second; + auto& watch = watches_entry->second; std::vector newly_added_to_watch; std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), @@ -38,12 +40,13 @@ WatchMap::updateWatchInterest(Token token, const std::set& update_t findRemovals(newly_removed_from_watch, token)); } -const absl::flat_hash_set& WatchMap::tokensInterestedIn(const std::string& resource_name) { +const absl::flat_hash_set& +WatchMap::tokensInterestedIn(const std::string& resource_name) { auto entry = watch_interest_.find(resource_name); if (entry == watch_interest_.end()) { - return {}; + return empty_token_set_; } - return entry.second; + return entry->second; } void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, @@ -52,16 +55,17 @@ void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); return; } - SubscriptionCallbacks& name_getter = watches_.front()->callbacks_; + SubscriptionCallbacks& name_getter = watches_.begin()->second.callbacks_; // Build a map from watches, to the set of updated resources that each watch cares about. Each // entry in the map is then a nice little bundle that can be fed directly into the individual // onConfigUpdate()s. - absl::flat_hash_map> per_watch_updates; + absl::flat_hash_map> + per_watch_updates; for (const auto& r : resources) { - const absl::flat_hash_set& interested_in_r = + const absl::flat_hash_set& interested_in_r = tokensInterestedIn(name_getter.resourceName(r)); - for (interested_token : interested_in_r) { + for (const auto& interested_token : interested_in_r) { per_watch_updates[interested_token].Add()->CopyFrom(r); } } @@ -73,12 +77,13 @@ void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField ENVOY_LOG(error, "A token referred to by watch_interest_ is not present in watches_!"); continue; } - entry.second.callbacks_.onConfigUpdate(updated.second, version_info); + entry->second.callbacks_.onConfigUpdate(updated.second, version_info); } } void WatchMap::tryDeliverConfigUpdate( - Token token, const Protobuf::RepeatedPtrField& added_resources, + WatchMap::Token token, + const Protobuf::RepeatedPtrField& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& system_version_info) { auto entry = watches_.find(token); @@ -86,7 +91,7 @@ void WatchMap::tryDeliverConfigUpdate( ENVOY_LOG(error, "A token referred to by watch_interest_ is not present in watches_!"); return; } - entry.second.callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); + entry->second.callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); } void WatchMap::onConfigUpdate( @@ -100,31 +105,32 @@ void WatchMap::onConfigUpdate( // Build a pair of maps: from watches, to the set of resources {added,removed} that each watch // cares about. Each entry in the map-pair is then a nice little bundle that can be fed directly // into the individual onConfigUpdate()s. - absl::flat_hash_map> per_watch_added; - absl::flat_hash_map> per_watch_removed; + absl::flat_hash_map> + per_watch_added; + absl::flat_hash_map> per_watch_removed; for (const auto& r : added_resources) { - const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r.name()); - for (interested_token : interested_in_r) { + const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r.name()); + for (const auto& interested_token : interested_in_r) { per_watch_added[interested_token].Add()->CopyFrom(r); } } for (const auto& r : removed_resources) { - const absl::flat_hash_set& tokens_interested_in_r = tokensInterestedIn(r); - for (interested_token : tokens_interested_in_r.second) { + const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r); + for (const auto& interested_token : interested_in_r) { *per_watch_removed[interested_token].Add() = r; } } // We just bundled up the updates into nice per-watch packages. Now, deliver them. for (const auto& added : per_watch_added) { - const Token& cur_token = added.first; + const WatchMap::Token& cur_token = added.first; auto removed = per_watch_removed.find(cur_token); if (removed == per_watch_removed.end()) { // additions only, no removals tryDeliverConfigUpdate(cur_token, added.second, {}, system_version_info); } else { // both additions and removals - tryDeliverConfigUpdate(cur_token, added.second, removed.second, system_version_info); + tryDeliverConfigUpdate(cur_token, added.second, removed->second, system_version_info); // Drop the removals now, so the final removals-only pass won't use them. per_watch_removed.erase(removed); } @@ -137,27 +143,28 @@ void WatchMap::onConfigUpdate( void WatchMap::onConfigUpdateFailed(const EnvoyException* e) { for (auto& watch : watches_) { - watch.second.onConfigUpdateFailed(e); + watch.second.callbacks_.onConfigUpdateFailed(e); } } std::set WatchMap::findAdditions(const std::vector& newly_added_to_watch, - Token token) { + WatchMap::Token token) { std::set newly_added_to_subscription; for (const auto& name : newly_added_to_watch) { auto entry = watch_interest_.find(name); if (entry == watch_interest_.end()) { newly_added_to_subscription.insert(name); - watch_interest_.emplace(name, {token}) + watch_interest_[name] = {token}; } else { - entry.second.insert(token); + entry->second.insert(token); } } return newly_added_to_subscription; } std::set -WatchMap::findRemovals(const std::vector& newly_removed_from_watch, Token token) { +WatchMap::findRemovals(const std::vector& newly_removed_from_watch, + WatchMap::Token token) { std::set newly_removed_from_subscription; for (const auto& name : newly_removed_from_watch) { auto entry = watch_interest_.find(name); @@ -165,8 +172,8 @@ WatchMap::findRemovals(const std::vector& newly_removed_from_watch, ENVOY_LOG(warn, "WatchMap: tried to remove a watch from untracked resource {}", name); continue; } - entry.second.erase(token); - if (entry.second.empty()) { + entry->second.erase(token); + if (entry->second.empty()) { watch_interest_.erase(entry); } newly_removed_from_subscription.insert(name); diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index b0975463af8d1..94c734c4779c9 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -4,6 +4,14 @@ #include #include +#include "envoy/config/subscription.h" + +#include "common/common/assert.h" +#include "common/common/logger.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" + namespace Envoy { namespace Config { @@ -26,8 +34,10 @@ namespace Config { // update the subscription accordingly. // // A WatchMap is assumed to be dedicated to a single type_url type of resource (EDS, CDS, etc). -class WatchMap : public SubscriptionCallbacks { +class WatchMap : public SubscriptionCallbacks, public Logger::Loggable { public: + WatchMap() {} + // An opaque token given out to users of WatchMap, to identify a given watch. using Token = uint64_t; @@ -59,12 +69,13 @@ class WatchMap : public SubscriptionCallbacks { virtual void onConfigUpdateFailed(const EnvoyException* e) override; - virtual std::string resourceName(const ProtobufWkt::Any& resource) override { + virtual std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } private: struct Watch { + Watch(SubscriptionCallbacks& callbacks) : callbacks_(callbacks) {} std::set resource_names_; // must be sorted set, for set_difference. SubscriptionCallbacks& callbacks_; }; @@ -97,6 +108,9 @@ class WatchMap : public SubscriptionCallbacks { Token next_watch_{0}; + // A little hack to allow tokensInterestedIn() to return a ref, rather than a copy. + const absl::flat_hash_set empty_token_set_{}; + WatchMap(const WatchMap&) = delete; WatchMap& operator=(const WatchMap&) = delete; }; diff --git a/test/common/config/BUILD b/test/common/config/BUILD index e913c8d035514..f4841e5a3f2c7 100644 --- a/test/common/config/BUILD +++ b/test/common/config/BUILD @@ -261,6 +261,17 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "watch_map_test", + srcs = ["watch_map_test.cc"], + deps = [ + "//source/common/config:watch_map_lib", + "//test/mocks/config:config_mocks", + "//test/test_common:utility_lib", + "@envoy_api//envoy/api/v2:eds_cc", + ], +) + envoy_cc_test( name = "filter_json_test", srcs = ["filter_json_test.cc"], diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 32d81b2534ec2..cbf979be97bcb 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -7,13 +7,6 @@ #include "common/config/watch_map.h" #include "test/mocks/config/mocks.h" -#include "test/mocks/event/mocks.h" -#include "test/mocks/filesystem/mocks.h" -#include "test/mocks/local_info/mocks.h" -#include "test/mocks/runtime/mocks.h" -#include "test/mocks/stats/mocks.h" -#include "test/mocks/upstream/mocks.h" -#include "test/test_common/environment.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" @@ -21,40 +14,45 @@ using ::testing::_; using ::testing::Invoke; -using ::testing::Return; +using ::testing::NiceMock; namespace Envoy { namespace Config { namespace { TEST(WatchMapTest, Basic) { - MockSubscriptionCallbacks callbacks; + NiceMock> callbacks; WatchMap watch_map; - WatchToken token = watch_map.addWatch(callbacks); + WatchMap::Token token = watch_map.addWatch(callbacks); std::set update_to({"alice", "bob"}); std::pair, std::set> added_removed = - updateWatchInterest(token, update_to); + watch_map.updateWatchInterest(token, update_to); EXPECT_EQ(update_to, added_removed.first); EXPECT_TRUE(added_removed.second.empty()); - Protobuf::RepeatedPtrField expected_resources; + envoy::api::v2::ClusterLoadAssignment expected_assignment; + expected_assignment.set_cluster_name("bob"); + envoy::api::v2::ClusterLoadAssignment unexpected_assignment; + unexpected_assignment.set_cluster_name("carol"); - // TODO TODO can it be this? EXPECT_CALL(callbacks, - // onConfigUpdate(RepeatedProtoEq(expected_resources), "version1")); + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(expected_assignment); + updated_resources.Add()->PackFrom(unexpected_assignment); EXPECT_CALL(callbacks, onConfigUpdate(_, "version1")) .WillOnce(Invoke( - [](const Protobuf::RepeatedPtrField& resources, const std::string&) { + [expected_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment expected_assignment; - resources[0].UnpackTo(&expected_assignment); - EXPECT_TRUE(TestUtility::protoEqual(expected_assignment, load_assignment)); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, expected_assignment)); })); - watch_map.onConfigUpdate(expected_resources, "version1"); + watch_map.onConfigUpdate(updated_resources, "version1"); std::pair, std::set> added_removed2 = - updateWatchInterest(token, {}); + watch_map.updateWatchInterest(token, {}); EXPECT_TRUE(watch_map.removeWatch(token)); } From d30e2296989c591419b3cb6137f8827aaac29686 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 29 May 2019 18:06:51 -0400 Subject: [PATCH 04/27] add two more tests, and fix a bug, thanks unit testing Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 3 +- test/common/config/watch_map_test.cc | 284 ++++++++++++++++++++++++--- 2 files changed, 258 insertions(+), 29 deletions(-) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index ba21ba9d86a81..0aad8a31c934e 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -172,11 +172,12 @@ WatchMap::findRemovals(const std::vector& newly_removed_from_watch, ENVOY_LOG(warn, "WatchMap: tried to remove a watch from untracked resource {}", name); continue; } + entry->second.erase(token); if (entry->second.empty()) { watch_interest_.erase(entry); + newly_removed_from_subscription.insert(name); } - newly_removed_from_subscription.insert(name); } return newly_removed_from_subscription; } diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index cbf979be97bcb..bc464ff192f92 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -20,43 +20,271 @@ namespace Envoy { namespace Config { namespace { +// Tests the simple case of a single watch. Checks that the watch will not be told of updates to +// resources it doesn't care about. Checks that the watch can later decide it does care about them, +// and then receive subsequent updates to them. TEST(WatchMapTest, Basic) { NiceMock> callbacks; WatchMap watch_map; WatchMap::Token token = watch_map.addWatch(callbacks); - std::set update_to({"alice", "bob"}); + + { + // The watch is interested in Alice and Bob. + std::set update_to({"alice", "bob"}); + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token, update_to); + EXPECT_EQ(update_to, added_removed.first); + EXPECT_TRUE(added_removed.second.empty()); + + // The update is going to contain Bob and Carol. + envoy::api::v2::ClusterLoadAssignment expected_assignment; + expected_assignment.set_cluster_name("bob"); + envoy::api::v2::ClusterLoadAssignment unexpected_assignment; + unexpected_assignment.set_cluster_name("carol"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(expected_assignment); + updated_resources.Add()->PackFrom(unexpected_assignment); + + EXPECT_CALL(callbacks, onConfigUpdate(_, "version1")) + .WillOnce(Invoke( + [expected_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, expected_assignment)); + })); + + watch_map.onConfigUpdate(updated_resources, "version1"); // TODO add delta + } + + { + // The watch is interested in Bob, Carol, Dave, Eve. + std::set update_to({"bob", "carol", "dave", "eve"}); + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token, update_to); + EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.first); + EXPECT_EQ(std::set({"alice"}), added_removed.second); + + // The update is going to contain Alice, Carol, Dave. + envoy::api::v2::ClusterLoadAssignment alice_assignment; + alice_assignment.set_cluster_name("alice"); + envoy::api::v2::ClusterLoadAssignment carol_assignment; + carol_assignment.set_cluster_name("carol"); + envoy::api::v2::ClusterLoadAssignment dave_assignment; + dave_assignment.set_cluster_name("dave"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(alice_assignment); + updated_resources.Add()->PackFrom(carol_assignment); + updated_resources.Add()->PackFrom(dave_assignment); + + EXPECT_CALL(callbacks, onConfigUpdate(_, "version2")) + .WillOnce(Invoke( + [carol_assignment, dave_assignment]( + const Protobuf::RepeatedPtrField& resources, const std::string&) { + EXPECT_EQ(2, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, carol_assignment)); + resources[1].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, dave_assignment)); + })); + + watch_map.onConfigUpdate(updated_resources, "version2"); // TODO add delta + } + std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token, update_to); - EXPECT_EQ(update_to, added_removed.first); - EXPECT_TRUE(added_removed.second.empty()); - - envoy::api::v2::ClusterLoadAssignment expected_assignment; - expected_assignment.set_cluster_name("bob"); - envoy::api::v2::ClusterLoadAssignment unexpected_assignment; - unexpected_assignment.set_cluster_name("carol"); - - Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(expected_assignment); - updated_resources.Add()->PackFrom(unexpected_assignment); - - EXPECT_CALL(callbacks, onConfigUpdate(_, "version1")) - .WillOnce(Invoke( - [expected_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, expected_assignment)); - })); - - watch_map.onConfigUpdate(updated_resources, "version1"); - - std::pair, std::set> added_removed2 = watch_map.updateWatchInterest(token, {}); - + EXPECT_EQ(std::set({"bob", "carol", "dave", "eve"}), added_removed.second); EXPECT_TRUE(watch_map.removeWatch(token)); } +// Checks the following: +// First watch on a resource name ==> updateWatchInterest() returns "add it to subscription" +// Second watch on that name ==> updateWatchInterest() returns nothing about that name +// Original watch loses interest ==> nothing +// Second watch also loses interest ==> "remove it from subscription" +TEST(WatchMapTest, Overlap) { + NiceMock> callbacks1; + NiceMock> callbacks2; + WatchMap watch_map; + WatchMap::Token token1 = watch_map.addWatch(callbacks1); + WatchMap::Token token2 = watch_map.addWatch(callbacks2); + + // First watch becomes interested. + { + std::set update_to({"alice"}); + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token1, update_to); + EXPECT_EQ(update_to, added_removed.first); // add to subscription + EXPECT_TRUE(added_removed.second.empty()); + + // First watch receives update. + envoy::api::v2::ClusterLoadAssignment alice_assignment; + alice_assignment.set_cluster_name("alice"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(alice_assignment); + + EXPECT_CALL(callbacks1, onConfigUpdate(_, "version1")) + .WillOnce( + Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); + })); + watch_map.onConfigUpdate(updated_resources, "version1"); // TODO add delta + } + // Second watch becomes interested. + { + std::set update_to({"alice"}); + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token2, update_to); + EXPECT_TRUE(added_removed.first.empty()); // nothing happens + EXPECT_TRUE(added_removed.second.empty()); + + // Both watches receive update. + envoy::api::v2::ClusterLoadAssignment alice_assignment; + alice_assignment.set_cluster_name("alice"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(alice_assignment); + + EXPECT_CALL(callbacks1, onConfigUpdate(_, "version2")) + .WillOnce( + Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); + })); + EXPECT_CALL(callbacks2, onConfigUpdate(_, "version2")) + .WillOnce( + Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); + })); + watch_map.onConfigUpdate(updated_resources, "version2"); // TODO add delta + } + // First watch loses interest. + { + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token1, {}); + EXPECT_TRUE(added_removed.first.empty()); // nothing happens + EXPECT_TRUE(added_removed.second.empty()); + + // *Only* second watch receives update. + envoy::api::v2::ClusterLoadAssignment alice_assignment; + alice_assignment.set_cluster_name("alice"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(alice_assignment); + + EXPECT_CALL(callbacks1, onConfigUpdate(_, "version3")).Times(0); + EXPECT_CALL(callbacks2, onConfigUpdate(_, "version3")) + .WillOnce( + Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); + })); + watch_map.onConfigUpdate(updated_resources, "version3"); // TODO add delta + } + // Second watch loses interest. + { + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token2, {}); + EXPECT_TRUE(added_removed.first.empty()); + EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription + } +} + +// Checks the following: +// First watch on a resource name ==> updateWatchInterest() returns "add it to subscription" +// Watch loses interest ==> "remove it from subscription" +// Second watch on that name ==> "add it to subscription" +TEST(WatchMapTest, AddRemoveAdd) { + NiceMock> callbacks1; + NiceMock> callbacks2; + WatchMap watch_map; + WatchMap::Token token1 = watch_map.addWatch(callbacks1); + WatchMap::Token token2 = watch_map.addWatch(callbacks2); + + // First watch becomes interested. + { + std::set update_to({"alice"}); + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token1, update_to); + EXPECT_EQ(update_to, added_removed.first); // add to subscription + EXPECT_TRUE(added_removed.second.empty()); + + // First watch receives update. + envoy::api::v2::ClusterLoadAssignment alice_assignment; + alice_assignment.set_cluster_name("alice"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(alice_assignment); + + EXPECT_CALL(callbacks1, onConfigUpdate(_, "version1")) + .WillOnce( + Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); + })); + watch_map.onConfigUpdate(updated_resources, "version1"); // TODO add delta + } + // First watch loses interest. + { + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token1, {}); + EXPECT_TRUE(added_removed.first.empty()); + EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription + + // (The xDS client should have responded to updateWatchInterest()'s return value by removing + // alice from the subscription, so onConfigUpdate() calls should be impossible right now.) + } + // Second watch becomes interested. + { + std::set update_to({"alice"}); + std::pair, std::set> added_removed = + watch_map.updateWatchInterest(token2, update_to); + EXPECT_EQ(update_to, added_removed.first); // add to subscription + EXPECT_TRUE(added_removed.second.empty()); + + // *Only* second watch receives update. + envoy::api::v2::ClusterLoadAssignment alice_assignment; + alice_assignment.set_cluster_name("alice"); + + Protobuf::RepeatedPtrField updated_resources; + updated_resources.Add()->PackFrom(alice_assignment); + + EXPECT_CALL(callbacks1, onConfigUpdate(_, "version2")).Times(0); + EXPECT_CALL(callbacks2, onConfigUpdate(_, "version2")) + .WillOnce( + Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, + const std::string&) { + EXPECT_EQ(1, resources.size()); + envoy::api::v2::ClusterLoadAssignment gotten_assignment; + resources[0].UnpackTo(&gotten_assignment); + EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); + })); + watch_map.onConfigUpdate(updated_resources, "version2"); // TODO add delta + } +} + } // namespace } // namespace Config } // namespace Envoy From 2ce83f4d97eec52ae65198d8da171d5710444aad Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 30 May 2019 11:40:09 -0400 Subject: [PATCH 05/27] test delta too Signed-off-by: Fred Douglas --- test/common/config/watch_map_test.cc | 298 ++++++++++++++------------- 1 file changed, 160 insertions(+), 138 deletions(-) diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index bc464ff192f92..8a4e25ecfd867 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -20,6 +20,77 @@ namespace Envoy { namespace Config { namespace { +// expectDeltaAndSotwUpdate() EXPECTs two birds with one function call: we want to cover both SotW +// and delta, which, while mechanically different, can behave identically for our testing purposes. +// Specifically, as a simplification for these tests, every still-present resource is updated in +// every update. Therefore, a resource can never show up in the SotW update but not the delta +// update. We can therefore use the same expected_resources for both. +void expectDeltaAndSotwUpdate( + MockSubscriptionCallbacks& callbacks, + std::vector expected_resources, + std::vector expected_removals, const std::string& version) { + EXPECT_CALL(callbacks, onConfigUpdate(_, version)) + .WillOnce(Invoke( + [expected_resources](const Protobuf::RepeatedPtrField& gotten_resources, + const std::string&) { + EXPECT_EQ(expected_resources.size(), gotten_resources.size()); + for (size_t i = 0; i < expected_resources.size(); i++) { + envoy::api::v2::ClusterLoadAssignment cur_gotten_resource; + gotten_resources[i].UnpackTo(&cur_gotten_resource); + EXPECT_TRUE(TestUtility::protoEqual(cur_gotten_resource, expected_resources[i])); + } + })); + EXPECT_CALL(callbacks, onConfigUpdate(_, _, _)) + .WillOnce( + Invoke([expected_resources, expected_removals, version]( + const Protobuf::RepeatedPtrField& gotten_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string&) { + EXPECT_EQ(expected_resources.size(), gotten_resources.size()); + for (size_t i = 0; i < expected_resources.size(); i++) { + EXPECT_EQ(gotten_resources[i].version(), version); + envoy::api::v2::ClusterLoadAssignment cur_gotten_resource; + gotten_resources[i].resource().UnpackTo(&cur_gotten_resource); + EXPECT_TRUE(TestUtility::protoEqual(cur_gotten_resource, expected_resources[i])); + } + EXPECT_EQ(expected_removals.size(), removed_resources.size()); + for (size_t i = 0; i < expected_removals.size(); i++) { + EXPECT_EQ(expected_removals[i], removed_resources[i]); + } + })); +} + +Protobuf::RepeatedPtrField +wrapInResource(const Protobuf::RepeatedPtrField& anys, + const std::string& version) { + Protobuf::RepeatedPtrField ret; + for (const auto& a : anys) { + envoy::api::v2::ClusterLoadAssignment cur_endpoint; + a.UnpackTo(&cur_endpoint); + auto* cur_resource = ret.Add(); + cur_resource->set_name(cur_endpoint.cluster_name()); + cur_resource->mutable_resource()->CopyFrom(a); + cur_resource->set_version(version); + } + return ret; +} + +// Similar to expectDeltaAndSotwUpdate(), but making the onConfigUpdate() happen, rather than +// EXPECTing it. +void doDeltaAndSotwUpdate(WatchMap& watch_map, + Protobuf::RepeatedPtrField sotw_resources, + std::vector removed_names, std::string version) { + watch_map.onConfigUpdate(sotw_resources, version); + + Protobuf::RepeatedPtrField delta_resources = + wrapInResource(sotw_resources, version); + Protobuf::RepeatedPtrField removed_names_proto; + for (auto n : removed_names) { + *removed_names_proto.Add() = n; + } + watch_map.onConfigUpdate(delta_resources, removed_names_proto, "version1"); +} + // Tests the simple case of a single watch. Checks that the watch will not be told of updates to // resources it doesn't care about. Checks that the watch can later decide it does care about them, // and then receive subsequent updates to them. @@ -29,72 +100,60 @@ TEST(WatchMapTest, Basic) { WatchMap::Token token = watch_map.addWatch(callbacks); { - // The watch is interested in Alice and Bob. + // The watch is interested in Alice and Bob... std::set update_to({"alice", "bob"}); std::pair, std::set> added_removed = watch_map.updateWatchInterest(token, update_to); EXPECT_EQ(update_to, added_removed.first); EXPECT_TRUE(added_removed.second.empty()); - // The update is going to contain Bob and Carol. - envoy::api::v2::ClusterLoadAssignment expected_assignment; - expected_assignment.set_cluster_name("bob"); - envoy::api::v2::ClusterLoadAssignment unexpected_assignment; - unexpected_assignment.set_cluster_name("carol"); - + // ...the update is going to contain Bob and Carol... Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(expected_assignment); - updated_resources.Add()->PackFrom(unexpected_assignment); - - EXPECT_CALL(callbacks, onConfigUpdate(_, "version1")) - .WillOnce(Invoke( - [expected_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, expected_assignment)); - })); - - watch_map.onConfigUpdate(updated_resources, "version1"); // TODO add delta + envoy::api::v2::ClusterLoadAssignment bob; + bob.set_cluster_name("bob"); + updated_resources.Add()->PackFrom(bob); + envoy::api::v2::ClusterLoadAssignment carol; + carol.set_cluster_name("carol"); + updated_resources.Add()->PackFrom(carol); + + // ...so the watch should receive only Bob. + std::vector expected_resources; + expected_resources.push_back(bob); + + expectDeltaAndSotwUpdate(callbacks, expected_resources, {}, "version1"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); } { - // The watch is interested in Bob, Carol, Dave, Eve. + // The watch is now interested in Bob, Carol, Dave, Eve... std::set update_to({"bob", "carol", "dave", "eve"}); std::pair, std::set> added_removed = watch_map.updateWatchInterest(token, update_to); EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.first); EXPECT_EQ(std::set({"alice"}), added_removed.second); - // The update is going to contain Alice, Carol, Dave. - envoy::api::v2::ClusterLoadAssignment alice_assignment; - alice_assignment.set_cluster_name("alice"); - envoy::api::v2::ClusterLoadAssignment carol_assignment; - carol_assignment.set_cluster_name("carol"); - envoy::api::v2::ClusterLoadAssignment dave_assignment; - dave_assignment.set_cluster_name("dave"); - + // ...the update is going to contain Alice, Carol, Dave... Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(alice_assignment); - updated_resources.Add()->PackFrom(carol_assignment); - updated_resources.Add()->PackFrom(dave_assignment); - - EXPECT_CALL(callbacks, onConfigUpdate(_, "version2")) - .WillOnce(Invoke( - [carol_assignment, dave_assignment]( - const Protobuf::RepeatedPtrField& resources, const std::string&) { - EXPECT_EQ(2, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, carol_assignment)); - resources[1].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, dave_assignment)); - })); - - watch_map.onConfigUpdate(updated_resources, "version2"); // TODO add delta + envoy::api::v2::ClusterLoadAssignment alice; + alice.set_cluster_name("alice"); + updated_resources.Add()->PackFrom(alice); + envoy::api::v2::ClusterLoadAssignment carol; + carol.set_cluster_name("carol"); + updated_resources.Add()->PackFrom(carol); + envoy::api::v2::ClusterLoadAssignment dave; + dave.set_cluster_name("dave"); + updated_resources.Add()->PackFrom(dave); + + // ...so the watch should receive only Carol and Dave. + std::vector expected_resources; + expected_resources.push_back(carol); + expected_resources.push_back(dave); + + expectDeltaAndSotwUpdate(callbacks, expected_resources, {"bob"}, "version2"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {"bob"}, "version2"); } + // Clean removal of the watch: first update to "interested in nothing", then remove. std::pair, std::set> added_removed = watch_map.updateWatchInterest(token, {}); EXPECT_EQ(std::set({"bob", "carol", "dave", "eve"}), added_removed.second); @@ -113,6 +172,11 @@ TEST(WatchMapTest, Overlap) { WatchMap::Token token1 = watch_map.addWatch(callbacks1); WatchMap::Token token2 = watch_map.addWatch(callbacks2); + Protobuf::RepeatedPtrField updated_resources; + envoy::api::v2::ClusterLoadAssignment alice; + alice.set_cluster_name("alice"); + updated_resources.Add()->PackFrom(alice); + // First watch becomes interested. { std::set update_to({"alice"}); @@ -122,22 +186,8 @@ TEST(WatchMapTest, Overlap) { EXPECT_TRUE(added_removed.second.empty()); // First watch receives update. - envoy::api::v2::ClusterLoadAssignment alice_assignment; - alice_assignment.set_cluster_name("alice"); - - Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(alice_assignment); - - EXPECT_CALL(callbacks1, onConfigUpdate(_, "version1")) - .WillOnce( - Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); - })); - watch_map.onConfigUpdate(updated_resources, "version1"); // TODO add delta + expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); } // Second watch becomes interested. { @@ -148,31 +198,9 @@ TEST(WatchMapTest, Overlap) { EXPECT_TRUE(added_removed.second.empty()); // Both watches receive update. - envoy::api::v2::ClusterLoadAssignment alice_assignment; - alice_assignment.set_cluster_name("alice"); - - Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(alice_assignment); - - EXPECT_CALL(callbacks1, onConfigUpdate(_, "version2")) - .WillOnce( - Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); - })); - EXPECT_CALL(callbacks2, onConfigUpdate(_, "version2")) - .WillOnce( - Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); - })); - watch_map.onConfigUpdate(updated_resources, "version2"); // TODO add delta + expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version2"); + expectDeltaAndSotwUpdate(callbacks2, {alice}, {}, "version2"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version2"); } // First watch loses interest. { @@ -182,23 +210,9 @@ TEST(WatchMapTest, Overlap) { EXPECT_TRUE(added_removed.second.empty()); // *Only* second watch receives update. - envoy::api::v2::ClusterLoadAssignment alice_assignment; - alice_assignment.set_cluster_name("alice"); - - Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(alice_assignment); - - EXPECT_CALL(callbacks1, onConfigUpdate(_, "version3")).Times(0); - EXPECT_CALL(callbacks2, onConfigUpdate(_, "version3")) - .WillOnce( - Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); - })); - watch_map.onConfigUpdate(updated_resources, "version3"); // TODO add delta + EXPECT_CALL(callbacks1, onConfigUpdate(_, _)).Times(0); + expectDeltaAndSotwUpdate(callbacks2, {alice}, {}, "version3"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version3"); } // Second watch loses interest. { @@ -220,6 +234,11 @@ TEST(WatchMapTest, AddRemoveAdd) { WatchMap::Token token1 = watch_map.addWatch(callbacks1); WatchMap::Token token2 = watch_map.addWatch(callbacks2); + Protobuf::RepeatedPtrField updated_resources; + envoy::api::v2::ClusterLoadAssignment alice; + alice.set_cluster_name("alice"); + updated_resources.Add()->PackFrom(alice); + // First watch becomes interested. { std::set update_to({"alice"}); @@ -229,22 +248,8 @@ TEST(WatchMapTest, AddRemoveAdd) { EXPECT_TRUE(added_removed.second.empty()); // First watch receives update. - envoy::api::v2::ClusterLoadAssignment alice_assignment; - alice_assignment.set_cluster_name("alice"); - - Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(alice_assignment); - - EXPECT_CALL(callbacks1, onConfigUpdate(_, "version1")) - .WillOnce( - Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); - })); - watch_map.onConfigUpdate(updated_resources, "version1"); // TODO add delta + expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); } // First watch loses interest. { @@ -265,26 +270,43 @@ TEST(WatchMapTest, AddRemoveAdd) { EXPECT_TRUE(added_removed.second.empty()); // *Only* second watch receives update. - envoy::api::v2::ClusterLoadAssignment alice_assignment; - alice_assignment.set_cluster_name("alice"); - - Protobuf::RepeatedPtrField updated_resources; - updated_resources.Add()->PackFrom(alice_assignment); - - EXPECT_CALL(callbacks1, onConfigUpdate(_, "version2")).Times(0); - EXPECT_CALL(callbacks2, onConfigUpdate(_, "version2")) - .WillOnce( - Invoke([alice_assignment](const Protobuf::RepeatedPtrField& resources, - const std::string&) { - EXPECT_EQ(1, resources.size()); - envoy::api::v2::ClusterLoadAssignment gotten_assignment; - resources[0].UnpackTo(&gotten_assignment); - EXPECT_TRUE(TestUtility::protoEqual(gotten_assignment, alice_assignment)); - })); - watch_map.onConfigUpdate(updated_resources, "version2"); // TODO add delta + EXPECT_CALL(callbacks1, onConfigUpdate(_, _)).Times(0); + expectDeltaAndSotwUpdate(callbacks2, {alice}, {}, "version2"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version2"); } } +// Tests that nothing breaks if an update arrives that we entirely do not care about. +TEST(WatchMapTest, UninterestingUpdate) { + NiceMock> callbacks; + WatchMap watch_map; + WatchMap::Token token = watch_map.addWatch(callbacks); + watch_map.updateWatchInterest(token, {"alice"}); + + Protobuf::RepeatedPtrField alice_update; + envoy::api::v2::ClusterLoadAssignment alice; + alice.set_cluster_name("alice"); + alice_update.Add()->PackFrom(alice); + + Protobuf::RepeatedPtrField bob_update; + envoy::api::v2::ClusterLoadAssignment bob; + bob.set_cluster_name("bob"); + bob_update.Add()->PackFrom(bob); + + EXPECT_CALL(callbacks, onConfigUpdate(_, _)).Times(0); + doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version1"); + + expectDeltaAndSotwUpdate(callbacks, {alice}, {}, "version2"); + doDeltaAndSotwUpdate(watch_map, alice_update, {}, "version2"); + + EXPECT_CALL(callbacks, onConfigUpdate(_, _)).Times(0); + doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version3"); + + // Clean removal of the watch: first update to "interested in nothing", then remove. + watch_map.updateWatchInterest(token, {}); + EXPECT_TRUE(watch_map.removeWatch(token)); +} + } // namespace } // namespace Config } // namespace Envoy From 33aff0fc2ca8f6678beaf945bdd66cdf3945ef43 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 30 May 2019 11:51:29 -0400 Subject: [PATCH 06/27] tiny rearrangement Signed-off-by: Fred Douglas --- source/common/config/watch_map.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 94c734c4779c9..0f17d83bdce2c 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -98,6 +98,8 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable& tokensInterestedIn(const std::string& resource_name); + // A little hack to allow tokensInterestedIn() to return a ref, rather than a copy. + const absl::flat_hash_set empty_token_set_{}; absl::flat_hash_map watches_; @@ -108,9 +110,6 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable empty_token_set_{}; - WatchMap(const WatchMap&) = delete; WatchMap& operator=(const WatchMap&) = delete; }; From 67dcc672f73ac139a44325e261c971b58cba6a9a Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 30 May 2019 11:59:50 -0400 Subject: [PATCH 07/27] spellcheck Signed-off-by: Fred Douglas --- include/envoy/config/grpc_mux.h | 2 +- source/common/config/watch_map.h | 2 +- test/common/config/watch_map_test.cc | 2 +- tools/spelling_dictionary.txt | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/include/envoy/config/grpc_mux.h b/include/envoy/config/grpc_mux.h index ebfe7dab526f5..405943afd53b6 100644 --- a/include/envoy/config/grpc_mux.h +++ b/include/envoy/config/grpc_mux.h @@ -26,7 +26,7 @@ struct ControlPlaneStats { ALL_CONTROL_PLANE_STATS(GENERATE_COUNTER_STRUCT,GENERATE_GAUGE_STRUCT) }; -// TODO(fredlas) reduntant to SubscriptionCallbacks; remove this one. +// TODO(fredlas) redundant to SubscriptionCallbacks; remove this one. class GrpcMuxCallbacks { public: virtual ~GrpcMuxCallbacks() {} diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 0f17d83bdce2c..d8bb112b23ee3 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -17,7 +17,7 @@ namespace Config { // Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same // resource name "X". The xDS machinery must return to each their very own subscription to X. -// The xDS machinery's "watch" concept accomplishes that, while avoiding parallel reduntant xDS +// The xDS machinery's "watch" concept accomplishes that, while avoiding parallel redundant xDS // requests for X. Each of those subscriptions is viewed as a "watch" on X, while behind the scenes // there is just a single real subscription to that resource name. // diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 8a4e25ecfd867..aa16e302ccb8b 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -259,7 +259,7 @@ TEST(WatchMapTest, AddRemoveAdd) { EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription // (The xDS client should have responded to updateWatchInterest()'s return value by removing - // alice from the subscription, so onConfigUpdate() calls should be impossible right now.) + // Alice from the subscription, so onConfigUpdate() calls should be impossible right now.) } // Second watch becomes interested. { diff --git a/tools/spelling_dictionary.txt b/tools/spelling_dictionary.txt index 4d4a1099ae41c..e76ac7efcf645 100644 --- a/tools/spelling_dictionary.txt +++ b/tools/spelling_dictionary.txt @@ -82,6 +82,7 @@ EVAL EVLOOP EVP EWOULDBLOCK +EXPECTs EXPR FAQ FDs @@ -446,6 +447,7 @@ evthread evwatch exe execlp +expectDeltaAndSotwUpdate facto favicon fd From 2ac1518ff45ca04ee89a3f73081cc72e07903b2f Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 30 May 2019 14:46:08 -0400 Subject: [PATCH 08/27] unmock resourceName to enable switching off of NiceMock Signed-off-by: Fred Douglas --- test/common/config/watch_map_test.cc | 29 +++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index aa16e302ccb8b..c67dc854c0c76 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -14,21 +14,28 @@ using ::testing::_; using ::testing::Invoke; -using ::testing::NiceMock; namespace Envoy { namespace Config { namespace { +class NamedMockSubscriptionCallbacks + : public MockSubscriptionCallbacks { +public: + std::string resourceName(const ProtobufWkt::Any& resource) override { + return MessageUtil::anyConvert(resource).cluster_name(); + } +}; + // expectDeltaAndSotwUpdate() EXPECTs two birds with one function call: we want to cover both SotW // and delta, which, while mechanically different, can behave identically for our testing purposes. // Specifically, as a simplification for these tests, every still-present resource is updated in // every update. Therefore, a resource can never show up in the SotW update but not the delta // update. We can therefore use the same expected_resources for both. -void expectDeltaAndSotwUpdate( - MockSubscriptionCallbacks& callbacks, - std::vector expected_resources, - std::vector expected_removals, const std::string& version) { +void expectDeltaAndSotwUpdate(NamedMockSubscriptionCallbacks& callbacks, + std::vector expected_resources, + std::vector expected_removals, + const std::string& version) { EXPECT_CALL(callbacks, onConfigUpdate(_, version)) .WillOnce(Invoke( [expected_resources](const Protobuf::RepeatedPtrField& gotten_resources, @@ -95,7 +102,7 @@ void doDeltaAndSotwUpdate(WatchMap& watch_map, // resources it doesn't care about. Checks that the watch can later decide it does care about them, // and then receive subsequent updates to them. TEST(WatchMapTest, Basic) { - NiceMock> callbacks; + NamedMockSubscriptionCallbacks callbacks; WatchMap watch_map; WatchMap::Token token = watch_map.addWatch(callbacks); @@ -166,8 +173,8 @@ TEST(WatchMapTest, Basic) { // Original watch loses interest ==> nothing // Second watch also loses interest ==> "remove it from subscription" TEST(WatchMapTest, Overlap) { - NiceMock> callbacks1; - NiceMock> callbacks2; + NamedMockSubscriptionCallbacks callbacks1; + NamedMockSubscriptionCallbacks callbacks2; WatchMap watch_map; WatchMap::Token token1 = watch_map.addWatch(callbacks1); WatchMap::Token token2 = watch_map.addWatch(callbacks2); @@ -228,8 +235,8 @@ TEST(WatchMapTest, Overlap) { // Watch loses interest ==> "remove it from subscription" // Second watch on that name ==> "add it to subscription" TEST(WatchMapTest, AddRemoveAdd) { - NiceMock> callbacks1; - NiceMock> callbacks2; + NamedMockSubscriptionCallbacks callbacks1; + NamedMockSubscriptionCallbacks callbacks2; WatchMap watch_map; WatchMap::Token token1 = watch_map.addWatch(callbacks1); WatchMap::Token token2 = watch_map.addWatch(callbacks2); @@ -278,7 +285,7 @@ TEST(WatchMapTest, AddRemoveAdd) { // Tests that nothing breaks if an update arrives that we entirely do not care about. TEST(WatchMapTest, UninterestingUpdate) { - NiceMock> callbacks; + NamedMockSubscriptionCallbacks callbacks; WatchMap watch_map; WatchMap::Token token = watch_map.addWatch(callbacks); watch_map.updateWatchInterest(token, {"alice"}); From fdb0a185523b51d4e11ec27747887716dc4fc6ea Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 30 May 2019 16:28:05 -0400 Subject: [PATCH 09/27] add a test, fix another bug, thanks again unit testing Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 18 ++++++---- test/common/config/watch_map_test.cc | 52 +++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 0aad8a31c934e..7d93f685fef51 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -71,13 +71,16 @@ void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField } // We just bundled up the updates into nice per-watch packages. Now, deliver them. - for (const auto& updated : per_watch_updates) { - auto entry = watches_.find(updated.first); - if (entry == watches_.end()) { - ENVOY_LOG(error, "A token referred to by watch_interest_ is not present in watches_!"); - continue; + for (auto& watch : watches_) { + auto this_watch_updates = per_watch_updates.find(watch.first); + if (this_watch_updates == per_watch_updates.end()) { + // This update included no resources this watch cares about - so we do an empty + // onConfigUpdate(), to notify the watch that its resources - if they existed before this - + // were dropped. + watch.second.callbacks_.onConfigUpdate({}, version_info); + } else { + watch.second.callbacks_.onConfigUpdate(this_watch_updates->second, version_info); } - entry->second.callbacks_.onConfigUpdate(updated.second, version_info); } } @@ -102,18 +105,19 @@ void WatchMap::onConfigUpdate( ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); return; } + // Build a pair of maps: from watches, to the set of resources {added,removed} that each watch // cares about. Each entry in the map-pair is then a nice little bundle that can be fed directly // into the individual onConfigUpdate()s. absl::flat_hash_map> per_watch_added; - absl::flat_hash_map> per_watch_removed; for (const auto& r : added_resources) { const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r.name()); for (const auto& interested_token : interested_in_r) { per_watch_added[interested_token].Add()->CopyFrom(r); } } + absl::flat_hash_map> per_watch_removed; for (const auto& r : removed_resources) { const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r); for (const auto& interested_token : interested_in_r) { diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index c67dc854c0c76..f6f37ea806d8b 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -67,6 +67,15 @@ void expectDeltaAndSotwUpdate(NamedMockSubscriptionCallbacks& callbacks, })); } +// Sometimes we want to verify that a delta onConfigUpdate simply doesn't happen. However, for SotW, +// every update triggers all onConfigUpdate()s, so we should still expect empty calls for that. +void expectNoDeltaUpdate(NamedMockSubscriptionCallbacks& callbacks, const std::string& version) { + EXPECT_CALL(callbacks, onConfigUpdate(_, version)) + .WillOnce(Invoke([](const Protobuf::RepeatedPtrField& gotten_resources, + const std::string&) { EXPECT_EQ(0, gotten_resources.size()); })); + EXPECT_CALL(callbacks, onConfigUpdate(_, _, _)).Times(0); +} + Protobuf::RepeatedPtrField wrapInResource(const Protobuf::RepeatedPtrField& anys, const std::string& version) { @@ -75,6 +84,7 @@ wrapInResource(const Protobuf::RepeatedPtrField& anys, envoy::api::v2::ClusterLoadAssignment cur_endpoint; a.UnpackTo(&cur_endpoint); auto* cur_resource = ret.Add(); + std::cerr << "wrapping " << cur_endpoint.cluster_name() << std::endl; cur_resource->set_name(cur_endpoint.cluster_name()); cur_resource->mutable_resource()->CopyFrom(a); cur_resource->set_version(version); @@ -95,6 +105,10 @@ void doDeltaAndSotwUpdate(WatchMap& watch_map, for (auto n : removed_names) { *removed_names_proto.Add() = n; } + std::cerr << "removed names:" << std::endl; + for (auto n : removed_names_proto) { + std::cerr << n << std::endl; + } watch_map.onConfigUpdate(delta_resources, removed_names_proto, "version1"); } @@ -194,6 +208,7 @@ TEST(WatchMapTest, Overlap) { // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); + expectNoDeltaUpdate(callbacks2, "version1"); doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); } // Second watch becomes interested. @@ -217,7 +232,7 @@ TEST(WatchMapTest, Overlap) { EXPECT_TRUE(added_removed.second.empty()); // *Only* second watch receives update. - EXPECT_CALL(callbacks1, onConfigUpdate(_, _)).Times(0); + expectNoDeltaUpdate(callbacks1, "version3"); expectDeltaAndSotwUpdate(callbacks2, {alice}, {}, "version3"); doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version3"); } @@ -256,6 +271,7 @@ TEST(WatchMapTest, AddRemoveAdd) { // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); + expectNoDeltaUpdate(callbacks2, "version1"); doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); } // First watch loses interest. @@ -277,7 +293,7 @@ TEST(WatchMapTest, AddRemoveAdd) { EXPECT_TRUE(added_removed.second.empty()); // *Only* second watch receives update. - EXPECT_CALL(callbacks1, onConfigUpdate(_, _)).Times(0); + expectNoDeltaUpdate(callbacks1, "version2"); expectDeltaAndSotwUpdate(callbacks2, {alice}, {}, "version2"); doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version2"); } @@ -300,13 +316,13 @@ TEST(WatchMapTest, UninterestingUpdate) { bob.set_cluster_name("bob"); bob_update.Add()->PackFrom(bob); - EXPECT_CALL(callbacks, onConfigUpdate(_, _)).Times(0); + expectNoDeltaUpdate(callbacks, "version1"); doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version1"); expectDeltaAndSotwUpdate(callbacks, {alice}, {}, "version2"); doDeltaAndSotwUpdate(watch_map, alice_update, {}, "version2"); - EXPECT_CALL(callbacks, onConfigUpdate(_, _)).Times(0); + expectNoDeltaUpdate(callbacks, "version3"); doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version3"); // Clean removal of the watch: first update to "interested in nothing", then remove. @@ -314,6 +330,34 @@ TEST(WatchMapTest, UninterestingUpdate) { EXPECT_TRUE(watch_map.removeWatch(token)); } +// Delta onConfigUpdate has some slightly subtle details with how it handles the three cases where a +// watch receives {only updates, updates+removals, only removals} to its resources. This test +// exercise those cases. Also, the removal-only case tests that SotW does call a watch's +// onConfigUpdate even if none of the watch's interested resources are among the updated resources. +// (Which ensures we deliver empty config updates when a resource is dropped.) +TEST(WatchMapTest, DeltaOnConfigUpdate) { + NamedMockSubscriptionCallbacks callbacks1; + NamedMockSubscriptionCallbacks callbacks2; + NamedMockSubscriptionCallbacks callbacks3; + WatchMap watch_map; + WatchMap::Token token1 = watch_map.addWatch(callbacks1); + WatchMap::Token token2 = watch_map.addWatch(callbacks2); + WatchMap::Token token3 = watch_map.addWatch(callbacks3); + watch_map.updateWatchInterest(token1, {"updated"}); + watch_map.updateWatchInterest(token2, {"updated", "removed"}); + watch_map.updateWatchInterest(token3, {"removed"}); + + Protobuf::RepeatedPtrField update; + envoy::api::v2::ClusterLoadAssignment updated; + updated.set_cluster_name("updated"); + update.Add()->PackFrom(updated); + + expectDeltaAndSotwUpdate(callbacks1, {updated}, {}, "version1"); // only update + expectDeltaAndSotwUpdate(callbacks2, {updated}, {"removed"}, "version1"); // update+remove + expectDeltaAndSotwUpdate(callbacks3, {}, {"removed"}, "version1"); // only remove + doDeltaAndSotwUpdate(watch_map, update, {"removed"}, "version1"); +} + } // namespace } // namespace Config } // namespace Envoy From 38f13cc8ea5fe33ad44a3b9795644245f8359cfb Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 6 Jun 2019 15:59:08 -0400 Subject: [PATCH 10/27] support watches that want to watch everything by providing no names Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 22 +++++++--- source/common/config/watch_map.h | 9 ++-- test/common/config/watch_map_test.cc | 63 +++++++++++++++++++--------- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 7d93f685fef51..76ec5fb0f3b1c 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -6,11 +6,13 @@ namespace Config { WatchMap::Token WatchMap::addWatch(SubscriptionCallbacks& callbacks) { WatchMap::Token next_watch = next_watch_++; watches_.emplace(next_watch, WatchMap::Watch(callbacks)); + wildcard_watches_.insert(next_watch); return next_watch; } bool WatchMap::removeWatch(WatchMap::Token token) { watches_.erase(token); + wildcard_watches_.erase(token); // may or may not be in there, but we want it gone. return watches_.empty(); } @@ -22,6 +24,12 @@ WatchMap::updateWatchInterest(WatchMap::Token token, ENVOY_LOG(error, "updateWatchInterest() called on nonexistent token!"); return std::make_pair(std::set(), std::set()); } + if (update_to_these_names.empty()) { + wildcard_watches_.insert(token); + } else { + wildcard_watches_.erase(token); + } + auto& watch = watches_entry->second; std::vector newly_added_to_watch; @@ -40,13 +48,17 @@ WatchMap::updateWatchInterest(WatchMap::Token token, findRemovals(newly_removed_from_watch, token)); } -const absl::flat_hash_set& +absl::flat_hash_set WatchMap::tokensInterestedIn(const std::string& resource_name) { - auto entry = watch_interest_.find(resource_name); - if (entry == watch_interest_.end()) { - return empty_token_set_; + // Note that std::set_union needs sorted sets. Better to do it ourselves with insert(). + absl::flat_hash_set ret = wildcard_watches_; + auto watches_interested = watch_interest_.find(resource_name); + if (watches_interested != watch_interest_.end()) { + for (const auto& watch : watches_interested->second) { + ret.insert(watch); + } } - return entry->second; + return ret; } void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index d8bb112b23ee3..61dd667cf84e7 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -96,13 +96,14 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable& removed_resources, const std::string& system_version_info); - // Does a lookup in watch_interest_, returning empty set if not found. - const absl::flat_hash_set& tokensInterestedIn(const std::string& resource_name); - // A little hack to allow tokensInterestedIn() to return a ref, rather than a copy. - const absl::flat_hash_set empty_token_set_{}; + // Returns the union of watch_interest_[resource_name] and wildcard_watches_. + absl::flat_hash_set tokensInterestedIn(const std::string& resource_name); absl::flat_hash_map watches_; + // Watches whose interest set is currently empty, which is interpreted as "everything". + absl::flat_hash_set wildcard_watches_; + // Maps a resource name to the set of watches interested in that resource. Has two purposes: // 1) Acts as a reference count; no watches care anymore ==> the resource can be removed. // 2) Enables efficient lookup of all interested watches when a resource has been updated. diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index f6f37ea806d8b..58af5e39b842f 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -84,7 +84,6 @@ wrapInResource(const Protobuf::RepeatedPtrField& anys, envoy::api::v2::ClusterLoadAssignment cur_endpoint; a.UnpackTo(&cur_endpoint); auto* cur_resource = ret.Add(); - std::cerr << "wrapping " << cur_endpoint.cluster_name() << std::endl; cur_resource->set_name(cur_endpoint.cluster_name()); cur_resource->mutable_resource()->CopyFrom(a); cur_resource->set_version(version); @@ -105,10 +104,6 @@ void doDeltaAndSotwUpdate(WatchMap& watch_map, for (auto n : removed_names) { *removed_names_proto.Add() = n; } - std::cerr << "removed names:" << std::endl; - for (auto n : removed_names_proto) { - std::cerr << n << std::endl; - } watch_map.onConfigUpdate(delta_resources, removed_names_proto, "version1"); } @@ -144,7 +139,6 @@ TEST(WatchMapTest, Basic) { expectDeltaAndSotwUpdate(callbacks, expected_resources, {}, "version1"); doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); } - { // The watch is now interested in Bob, Carol, Dave, Eve... std::set update_to({"bob", "carol", "dave", "eve"}); @@ -173,11 +167,6 @@ TEST(WatchMapTest, Basic) { expectDeltaAndSotwUpdate(callbacks, expected_resources, {"bob"}, "version2"); doDeltaAndSotwUpdate(watch_map, updated_resources, {"bob"}, "version2"); } - - // Clean removal of the watch: first update to "interested in nothing", then remove. - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token, {}); - EXPECT_EQ(std::set({"bob", "carol", "dave", "eve"}), added_removed.second); EXPECT_TRUE(watch_map.removeWatch(token)); } @@ -186,6 +175,8 @@ TEST(WatchMapTest, Basic) { // Second watch on that name ==> updateWatchInterest() returns nothing about that name // Original watch loses interest ==> nothing // Second watch also loses interest ==> "remove it from subscription" +// NOTE: we need the resource name "dummy" to keep either watch from ever having no names watched, +// which is treated as interest in all names. TEST(WatchMapTest, Overlap) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; @@ -200,11 +191,12 @@ TEST(WatchMapTest, Overlap) { // First watch becomes interested. { - std::set update_to({"alice"}); + std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = watch_map.updateWatchInterest(token1, update_to); EXPECT_EQ(update_to, added_removed.first); // add to subscription EXPECT_TRUE(added_removed.second.empty()); + watch_map.updateWatchInterest(token2, {"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -213,7 +205,7 @@ TEST(WatchMapTest, Overlap) { } // Second watch becomes interested. { - std::set update_to({"alice"}); + std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = watch_map.updateWatchInterest(token2, update_to); EXPECT_TRUE(added_removed.first.empty()); // nothing happens @@ -227,7 +219,7 @@ TEST(WatchMapTest, Overlap) { // First watch loses interest. { std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token1, {}); + watch_map.updateWatchInterest(token1, {"dummy"}); EXPECT_TRUE(added_removed.first.empty()); // nothing happens EXPECT_TRUE(added_removed.second.empty()); @@ -239,7 +231,7 @@ TEST(WatchMapTest, Overlap) { // Second watch loses interest. { std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token2, {}); + watch_map.updateWatchInterest(token2, {"dummy"}); EXPECT_TRUE(added_removed.first.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription } @@ -249,6 +241,8 @@ TEST(WatchMapTest, Overlap) { // First watch on a resource name ==> updateWatchInterest() returns "add it to subscription" // Watch loses interest ==> "remove it from subscription" // Second watch on that name ==> "add it to subscription" +// NOTE: we need the resource name "dummy" to keep either watch from ever having no names watched, +// which is treated as interest in all names. TEST(WatchMapTest, AddRemoveAdd) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; @@ -263,11 +257,12 @@ TEST(WatchMapTest, AddRemoveAdd) { // First watch becomes interested. { - std::set update_to({"alice"}); + std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = watch_map.updateWatchInterest(token1, update_to); EXPECT_EQ(update_to, added_removed.first); // add to subscription EXPECT_TRUE(added_removed.second.empty()); + watch_map.updateWatchInterest(token2, {"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -277,7 +272,7 @@ TEST(WatchMapTest, AddRemoveAdd) { // First watch loses interest. { std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token1, {}); + watch_map.updateWatchInterest(token1, {"dummy"}); EXPECT_TRUE(added_removed.first.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription @@ -286,10 +281,10 @@ TEST(WatchMapTest, AddRemoveAdd) { } // Second watch becomes interested. { - std::set update_to({"alice"}); + std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = watch_map.updateWatchInterest(token2, update_to); - EXPECT_EQ(update_to, added_removed.first); // add to subscription + EXPECT_EQ(std::set({"alice"}), added_removed.first); // add to subscription EXPECT_TRUE(added_removed.second.empty()); // *Only* second watch receives update. @@ -330,6 +325,36 @@ TEST(WatchMapTest, UninterestingUpdate) { EXPECT_TRUE(watch_map.removeWatch(token)); } +// Tests that a watch that specifies no particular resource interest is treated as interested in +// everything. +TEST(WatchMapTest, WatchingEverything) { + NamedMockSubscriptionCallbacks callbacks1; + NamedMockSubscriptionCallbacks callbacks2; + WatchMap watch_map; + watch_map.addWatch(callbacks1); + WatchMap::Token token2 = watch_map.addWatch(callbacks2); + // token1 never specifies any names, and so is treated as interested in everything. + watch_map.updateWatchInterest(token2, {"alice"}); + + Protobuf::RepeatedPtrField updated_resources; + envoy::api::v2::ClusterLoadAssignment alice; + alice.set_cluster_name("alice"); + updated_resources.Add()->PackFrom(alice); + envoy::api::v2::ClusterLoadAssignment bob; + bob.set_cluster_name("bob"); + updated_resources.Add()->PackFrom(bob); + + std::vector expected_resources1; + expected_resources1.push_back(alice); + expected_resources1.push_back(bob); + std::vector expected_resources2; + expected_resources2.push_back(alice); + + expectDeltaAndSotwUpdate(callbacks1, expected_resources1, {}, "version1"); + expectDeltaAndSotwUpdate(callbacks2, expected_resources2, {}, "version1"); + doDeltaAndSotwUpdate(watch_map, updated_resources, {}, "version1"); +} + // Delta onConfigUpdate has some slightly subtle details with how it handles the three cases where a // watch receives {only updates, updates+removals, only removals} to its resources. This test // exercise those cases. Also, the removal-only case tests that SotW does call a watch's From eb0f380d55e79e224beee568735a51d390bf1937 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Thu, 6 Jun 2019 16:05:06 -0400 Subject: [PATCH 11/27] fix compile after merge Signed-off-by: Fred Douglas --- test/common/config/watch_map_test.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 58af5e39b842f..9e707a1aa218c 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -23,7 +23,7 @@ class NamedMockSubscriptionCallbacks : public MockSubscriptionCallbacks { public: std::string resourceName(const ProtobufWkt::Any& resource) override { - return MessageUtil::anyConvert(resource).cluster_name(); + return TestUtility::anyConvert(resource).cluster_name(); } }; From 00f7f554df0421e092072c69af09091ec2f54371 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Fri, 7 Jun 2019 16:39:19 -0400 Subject: [PATCH 12/27] snapshot Signed-off-by: Fred Douglas --- source/common/config/.watch_map.h.kate-swp | Bin 0 -> 1163 bytes source/common/config/watch_map.cc | 125 ++++++++------------- source/common/config/watch_map.h | 55 +++++---- test/common/config/watch_map_test.cc | 62 +++++----- 4 files changed, 103 insertions(+), 139 deletions(-) create mode 100644 source/common/config/.watch_map.h.kate-swp diff --git a/source/common/config/.watch_map.h.kate-swp b/source/common/config/.watch_map.h.kate-swp new file mode 100644 index 0000000000000000000000000000000000000000..490ebdf75fe3df6bf1a8fa63b5358cdd76f0b2b3 GIT binary patch literal 1163 zcmZ9L%SyvQ6ox0YUhB2C-dp3Y3l(u$OTk425jV|3a8p7PNb`${ zz8xJFf*YL_^{>_nT7-2C3UI0KC4j!Xv9JeDgmd7L@D%t_I1la$Pdo3=fPKkl!RNwr z;7EAh;RSFkdBMpS!EMQxocYV(d&yV8L*Z5Mlkl4JKJ3}`L7?3a9C|OA%`)gUlZ~vL zdPkrzix2vq^Nkb?P_o$sFeRH!08_G&1g7MAwv8B}=7|xCw$`L*BNA^=wDk>&ZlofR zQ?&IyMO*VJ+Nz17Cn})oi4n@S)}(A}P0BWh4K+}9UuvT4fiUKHJ>d;-Q+N|>dENqt zl5c~p9(Ekw1zXPcoV)_=$a+=qS+@PCZa)oC7_R?a#{hP{GESm;eH^})+8>UP!+c0My%r?xK2eS=x=D}=3YsfwZu1evIt>xKe G)cOPMD|%A^ literal 0 HcmV?d00001 diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 76ec5fb0f3b1c..beda8f2ae4249 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -3,55 +3,45 @@ namespace Envoy { namespace Config { -WatchMap::Token WatchMap::addWatch(SubscriptionCallbacks& callbacks) { - WatchMap::Token next_watch = next_watch_++; - watches_.emplace(next_watch, WatchMap::Watch(callbacks)); - wildcard_watches_.insert(next_watch); - return next_watch; +WatchPtr WatchMap::addWatch(SubscriptionCallbacks& callbacks) { + auto watch = std::make_unique(*this, callbacks); + wildcard_watches_.insert(watch.get()); + watches_.insert(watch.get()); + return watch; } -bool WatchMap::removeWatch(WatchMap::Token token) { - watches_.erase(token); - wildcard_watches_.erase(token); // may or may not be in there, but we want it gone. - return watches_.empty(); +void WatchMap::removeWatch(Watch* watch) { + wildcard_watches_.erase(watch); // may or may not be in there, but we want it gone. + watches_.erase(watch); } std::pair, std::set> -WatchMap::updateWatchInterest(WatchMap::Token token, - const std::set& update_to_these_names) { - auto watches_entry = watches_.find(token); - if (watches_entry == watches_.end()) { - ENVOY_LOG(error, "updateWatchInterest() called on nonexistent token!"); - return std::make_pair(std::set(), std::set()); - } +WatchMap::updateWatchInterest(Watch* watch, const std::set& update_to_these_names) { if (update_to_these_names.empty()) { - wildcard_watches_.insert(token); + wildcard_watches_.insert(watch); } else { - wildcard_watches_.erase(token); + wildcard_watches_.erase(watch); } - auto& watch = watches_entry->second; - std::vector newly_added_to_watch; std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), - watch.resource_names_.begin(), watch.resource_names_.end(), + watch->resource_names_.begin(), watch->resource_names_.end(), std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); std::vector newly_removed_from_watch; - std::set_difference(watch.resource_names_.begin(), watch.resource_names_.end(), + std::set_difference(watch->resource_names_.begin(), watch->resource_names_.end(), update_to_these_names.begin(), update_to_these_names.end(), std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); - watch.resource_names_ = update_to_these_names; + watch->resource_names_ = update_to_these_names; - return std::make_pair(findAdditions(newly_added_to_watch, token), - findRemovals(newly_removed_from_watch, token)); + return std::make_pair(findAdditions(newly_added_to_watch, watch), + findRemovals(newly_removed_from_watch, watch)); } -absl::flat_hash_set -WatchMap::tokensInterestedIn(const std::string& resource_name) { +absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& resource_name) { // Note that std::set_union needs sorted sets. Better to do it ourselves with insert(). - absl::flat_hash_set ret = wildcard_watches_; + absl::flat_hash_set ret = wildcard_watches_; auto watches_interested = watch_interest_.find(resource_name); if (watches_interested != watch_interest_.end()) { for (const auto& watch : watches_interested->second) { @@ -67,120 +57,99 @@ void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); return; } - SubscriptionCallbacks& name_getter = watches_.begin()->second.callbacks_; + SubscriptionCallbacks& name_getter = (*watches_.begin())->callbacks_; // Build a map from watches, to the set of updated resources that each watch cares about. Each // entry in the map is then a nice little bundle that can be fed directly into the individual // onConfigUpdate()s. - absl::flat_hash_map> - per_watch_updates; + absl::flat_hash_map> per_watch_updates; for (const auto& r : resources) { - const absl::flat_hash_set& interested_in_r = - tokensInterestedIn(name_getter.resourceName(r)); - for (const auto& interested_token : interested_in_r) { - per_watch_updates[interested_token].Add()->CopyFrom(r); + const absl::flat_hash_set& interested_in_r = + watchesInterestedIn(name_getter.resourceName(r)); + for (const auto& interested_watch : interested_in_r) { + per_watch_updates[interested_watch].Add()->CopyFrom(r); } } // We just bundled up the updates into nice per-watch packages. Now, deliver them. for (auto& watch : watches_) { - auto this_watch_updates = per_watch_updates.find(watch.first); + auto this_watch_updates = per_watch_updates.find(watch); if (this_watch_updates == per_watch_updates.end()) { // This update included no resources this watch cares about - so we do an empty // onConfigUpdate(), to notify the watch that its resources - if they existed before this - // were dropped. - watch.second.callbacks_.onConfigUpdate({}, version_info); + watch->callbacks_.onConfigUpdate({}, version_info); } else { - watch.second.callbacks_.onConfigUpdate(this_watch_updates->second, version_info); + watch->callbacks_.onConfigUpdate(this_watch_updates->second, version_info); } } } -void WatchMap::tryDeliverConfigUpdate( - WatchMap::Token token, - const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) { - auto entry = watches_.find(token); - if (entry == watches_.end()) { - ENVOY_LOG(error, "A token referred to by watch_interest_ is not present in watches_!"); - return; - } - entry->second.callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); -} - void WatchMap::onConfigUpdate( const Protobuf::RepeatedPtrField& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& system_version_info) { - if (watches_.empty()) { - ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); - return; - } - // Build a pair of maps: from watches, to the set of resources {added,removed} that each watch // cares about. Each entry in the map-pair is then a nice little bundle that can be fed directly // into the individual onConfigUpdate()s. - absl::flat_hash_map> - per_watch_added; + absl::flat_hash_map> per_watch_added; for (const auto& r : added_resources) { - const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r.name()); - for (const auto& interested_token : interested_in_r) { - per_watch_added[interested_token].Add()->CopyFrom(r); + const absl::flat_hash_set& interested_in_r = watchesInterestedIn(r.name()); + for (const auto& interested_watch : interested_in_r) { + per_watch_added[interested_watch].Add()->CopyFrom(r); } } - absl::flat_hash_map> per_watch_removed; + absl::flat_hash_map> per_watch_removed; for (const auto& r : removed_resources) { - const absl::flat_hash_set& interested_in_r = tokensInterestedIn(r); - for (const auto& interested_token : interested_in_r) { - *per_watch_removed[interested_token].Add() = r; + const absl::flat_hash_set& interested_in_r = watchesInterestedIn(r); + for (const auto& interested_watch : interested_in_r) { + *per_watch_removed[interested_watch].Add() = r; } } // We just bundled up the updates into nice per-watch packages. Now, deliver them. for (const auto& added : per_watch_added) { - const WatchMap::Token& cur_token = added.first; - auto removed = per_watch_removed.find(cur_token); + const Watch* cur_watch = added.first; + auto removed = per_watch_removed.find(cur_watch); if (removed == per_watch_removed.end()) { // additions only, no removals - tryDeliverConfigUpdate(cur_token, added.second, {}, system_version_info); + cur_watch->callbacks_.onConfigUpdate(added.second, {}, system_version_info); } else { // both additions and removals - tryDeliverConfigUpdate(cur_token, added.second, removed->second, system_version_info); + cur_watch->callbacks_.onConfigUpdate(added.second, removed->second, system_version_info); // Drop the removals now, so the final removals-only pass won't use them. per_watch_removed.erase(removed); } } // Any removals-only updates will not have been picked up in the per_watch_added loop. - for (const auto& removed : per_watch_removed) { - tryDeliverConfigUpdate(removed.first, {}, removed.second, system_version_info); + for (auto& removed : per_watch_removed) { + removed.first->callbacks_.onConfigUpdate({}, removed.second, system_version_info); } } void WatchMap::onConfigUpdateFailed(const EnvoyException* e) { for (auto& watch : watches_) { - watch.second.callbacks_.onConfigUpdateFailed(e); + watch->callbacks_.onConfigUpdateFailed(e); } } std::set WatchMap::findAdditions(const std::vector& newly_added_to_watch, - WatchMap::Token token) { + Watch* watch) { std::set newly_added_to_subscription; for (const auto& name : newly_added_to_watch) { auto entry = watch_interest_.find(name); if (entry == watch_interest_.end()) { newly_added_to_subscription.insert(name); - watch_interest_[name] = {token}; + watch_interest_[name] = {watch}; } else { - entry->second.insert(token); + entry->second.insert(watch); } } return newly_added_to_subscription; } std::set -WatchMap::findRemovals(const std::vector& newly_removed_from_watch, - WatchMap::Token token) { +WatchMap::findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) { std::set newly_removed_from_subscription; for (const auto& name : newly_removed_from_watch) { auto entry = watch_interest_.find(name); @@ -189,7 +158,7 @@ WatchMap::findRemovals(const std::vector& newly_removed_from_watch, continue; } - entry->second.erase(token); + entry->second.erase(watch); if (entry->second.empty()) { watch_interest_.erase(entry); newly_removed_from_subscription.insert(name); diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 61dd667cf84e7..87f419f4b3fd6 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -15,6 +15,9 @@ namespace Envoy { namespace Config { +struct Watch; +using WatchPtr = std::unique_ptr; + // Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same // resource name "X". The xDS machinery must return to each their very own subscription to X. // The xDS machinery's "watch" concept accomplishes that, while avoiding parallel redundant xDS @@ -38,18 +41,10 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable, std::set> - updateWatchInterest(Token token, const std::set& update_to_these_names); + updateWatchInterest(Watch* watch, const std::set& update_to_these_names); // SubscriptionCallbacks virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, @@ -74,46 +69,46 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable resource_names_; // must be sorted set, for set_difference. - SubscriptionCallbacks& callbacks_; - }; + friend struct Watch; + // Expects that the watch to be removed has already had all of its resource names removed via + // updateWatchInterest(). + void removeWatch(Watch* watch); // Given a list of names that are new to an individual watch, returns those names that are in fact // new to the entire subscription. std::set findAdditions(const std::vector& newly_added_to_watch, - Token token); + Watch* watch); // Given a list of names that an individual watch no longer cares about, returns those names that // in fact the entire subscription no longer cares about. std::set findRemovals(const std::vector& newly_removed_from_watch, - Token token); - - // Calls watches_[token].callbacks_.onConfigUpdate(), or logs an error if token isn't in watches_. - void tryDeliverConfigUpdate( - Token token, const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info); + Watch* watch); // Returns the union of watch_interest_[resource_name] and wildcard_watches_. - absl::flat_hash_set tokensInterestedIn(const std::string& resource_name); + absl::flat_hash_set watchesInterestedIn(const std::string& resource_name); - absl::flat_hash_map watches_; + absl::flat_hash_set watches_; // Watches whose interest set is currently empty, which is interpreted as "everything". - absl::flat_hash_set wildcard_watches_; + absl::flat_hash_set wildcard_watches_; // Maps a resource name to the set of watches interested in that resource. Has two purposes: // 1) Acts as a reference count; no watches care anymore ==> the resource can be removed. // 2) Enables efficient lookup of all interested watches when a resource has been updated. - absl::flat_hash_map> watch_interest_; - - Token next_watch_{0}; + absl::flat_hash_map> watch_interest_; WatchMap(const WatchMap&) = delete; WatchMap& operator=(const WatchMap&) = delete; }; +struct Watch { + Watch(WatchMap& owning_map, SubscriptionCallbacks& callbacks) + : owning_map_(owning_map), callbacks_(callbacks) {} + ~Watch() { owning_map_.removeWatch(this); } + WatchMap& owning_map_; + SubscriptionCallbacks& callbacks_; + std::set resource_names_; // must be sorted set, for set_difference. +}; + } // namespace Config } // namespace Envoy diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 9e707a1aa218c..0aec3897c8d1f 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -113,13 +113,13 @@ void doDeltaAndSotwUpdate(WatchMap& watch_map, TEST(WatchMapTest, Basic) { NamedMockSubscriptionCallbacks callbacks; WatchMap watch_map; - WatchMap::Token token = watch_map.addWatch(callbacks); + WatchPtr watch = watch_map.addWatch(callbacks); { // The watch is interested in Alice and Bob... std::set update_to({"alice", "bob"}); std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token, update_to); + watch_map.updateWatchInterest(watch.get(), update_to); EXPECT_EQ(update_to, added_removed.first); EXPECT_TRUE(added_removed.second.empty()); @@ -143,7 +143,7 @@ TEST(WatchMapTest, Basic) { // The watch is now interested in Bob, Carol, Dave, Eve... std::set update_to({"bob", "carol", "dave", "eve"}); std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token, update_to); + watch_map.updateWatchInterest(watch.get(), update_to); EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.first); EXPECT_EQ(std::set({"alice"}), added_removed.second); @@ -167,7 +167,7 @@ TEST(WatchMapTest, Basic) { expectDeltaAndSotwUpdate(callbacks, expected_resources, {"bob"}, "version2"); doDeltaAndSotwUpdate(watch_map, updated_resources, {"bob"}, "version2"); } - EXPECT_TRUE(watch_map.removeWatch(token)); + watch.reset(); } // Checks the following: @@ -181,8 +181,8 @@ TEST(WatchMapTest, Overlap) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; WatchMap watch_map; - WatchMap::Token token1 = watch_map.addWatch(callbacks1); - WatchMap::Token token2 = watch_map.addWatch(callbacks2); + WatchPtr watch1 = watch_map.addWatch(callbacks1); + WatchPtr watch2 = watch_map.addWatch(callbacks2); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -193,10 +193,10 @@ TEST(WatchMapTest, Overlap) { { std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token1, update_to); + watch_map.updateWatchInterest(watch1.get(), update_to); EXPECT_EQ(update_to, added_removed.first); // add to subscription EXPECT_TRUE(added_removed.second.empty()); - watch_map.updateWatchInterest(token2, {"dummy"}); + watch_map.updateWatchInterest(watch2.get(), {"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -207,7 +207,7 @@ TEST(WatchMapTest, Overlap) { { std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token2, update_to); + watch_map.updateWatchInterest(watch2.get(), update_to); EXPECT_TRUE(added_removed.first.empty()); // nothing happens EXPECT_TRUE(added_removed.second.empty()); @@ -219,7 +219,7 @@ TEST(WatchMapTest, Overlap) { // First watch loses interest. { std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token1, {"dummy"}); + watch_map.updateWatchInterest(watch1.get(), {"dummy"}); EXPECT_TRUE(added_removed.first.empty()); // nothing happens EXPECT_TRUE(added_removed.second.empty()); @@ -231,7 +231,7 @@ TEST(WatchMapTest, Overlap) { // Second watch loses interest. { std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token2, {"dummy"}); + watch_map.updateWatchInterest(watch2.get(), {"dummy"}); EXPECT_TRUE(added_removed.first.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription } @@ -247,8 +247,8 @@ TEST(WatchMapTest, AddRemoveAdd) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; WatchMap watch_map; - WatchMap::Token token1 = watch_map.addWatch(callbacks1); - WatchMap::Token token2 = watch_map.addWatch(callbacks2); + WatchPtr watch1 = watch_map.addWatch(callbacks1); + WatchPtr watch2 = watch_map.addWatch(callbacks2); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -259,10 +259,10 @@ TEST(WatchMapTest, AddRemoveAdd) { { std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token1, update_to); + watch_map.updateWatchInterest(watch1.get(), update_to); EXPECT_EQ(update_to, added_removed.first); // add to subscription EXPECT_TRUE(added_removed.second.empty()); - watch_map.updateWatchInterest(token2, {"dummy"}); + watch_map.updateWatchInterest(watch2.get(), {"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -272,7 +272,7 @@ TEST(WatchMapTest, AddRemoveAdd) { // First watch loses interest. { std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token1, {"dummy"}); + watch_map.updateWatchInterest(watch1.get(), {"dummy"}); EXPECT_TRUE(added_removed.first.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription @@ -283,7 +283,7 @@ TEST(WatchMapTest, AddRemoveAdd) { { std::set update_to({"alice", "dummy"}); std::pair, std::set> added_removed = - watch_map.updateWatchInterest(token2, update_to); + watch_map.updateWatchInterest(watch2.get(), update_to); EXPECT_EQ(std::set({"alice"}), added_removed.first); // add to subscription EXPECT_TRUE(added_removed.second.empty()); @@ -298,8 +298,8 @@ TEST(WatchMapTest, AddRemoveAdd) { TEST(WatchMapTest, UninterestingUpdate) { NamedMockSubscriptionCallbacks callbacks; WatchMap watch_map; - WatchMap::Token token = watch_map.addWatch(callbacks); - watch_map.updateWatchInterest(token, {"alice"}); + WatchPtr watch = watch_map.addWatch(callbacks); + watch_map.updateWatchInterest(watch.get(), {"alice"}); Protobuf::RepeatedPtrField alice_update; envoy::api::v2::ClusterLoadAssignment alice; @@ -321,8 +321,8 @@ TEST(WatchMapTest, UninterestingUpdate) { doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version3"); // Clean removal of the watch: first update to "interested in nothing", then remove. - watch_map.updateWatchInterest(token, {}); - EXPECT_TRUE(watch_map.removeWatch(token)); + watch_map.updateWatchInterest(watch.get(), {}); + watch.reset(); } // Tests that a watch that specifies no particular resource interest is treated as interested in @@ -331,10 +331,10 @@ TEST(WatchMapTest, WatchingEverything) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; WatchMap watch_map; - watch_map.addWatch(callbacks1); - WatchMap::Token token2 = watch_map.addWatch(callbacks2); - // token1 never specifies any names, and so is treated as interested in everything. - watch_map.updateWatchInterest(token2, {"alice"}); + WatchPtr watch1 = watch_map.addWatch(callbacks1); + WatchPtr watch2 = watch_map.addWatch(callbacks2); + // watch1 never specifies any names, and so is treated as interested in everything. + watch_map.updateWatchInterest(watch2.get(), {"alice"}); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -365,12 +365,12 @@ TEST(WatchMapTest, DeltaOnConfigUpdate) { NamedMockSubscriptionCallbacks callbacks2; NamedMockSubscriptionCallbacks callbacks3; WatchMap watch_map; - WatchMap::Token token1 = watch_map.addWatch(callbacks1); - WatchMap::Token token2 = watch_map.addWatch(callbacks2); - WatchMap::Token token3 = watch_map.addWatch(callbacks3); - watch_map.updateWatchInterest(token1, {"updated"}); - watch_map.updateWatchInterest(token2, {"updated", "removed"}); - watch_map.updateWatchInterest(token3, {"removed"}); + WatchPtr watch1 = watch_map.addWatch(callbacks1); + WatchPtr watch2 = watch_map.addWatch(callbacks2); + WatchPtr watch3 = watch_map.addWatch(callbacks3); + watch_map.updateWatchInterest(watch1.get(), {"updated"}); + watch_map.updateWatchInterest(watch2.get(), {"updated", "removed"}); + watch_map.updateWatchInterest(watch3.get(), {"removed"}); Protobuf::RepeatedPtrField update; envoy::api::v2::ClusterLoadAssignment updated; From 943949aecf919a7a4c30aac01825f91118b75cb0 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Fri, 7 Jun 2019 16:44:02 -0400 Subject: [PATCH 13/27] change std pair to AddedRemoved Signed-off-by: Fred Douglas --- source/common/config/.watch_map.h.kate-swp | Bin 1163 -> 0 bytes source/common/config/watch_map.cc | 8 +-- source/common/config/watch_map.h | 15 +++-- test/common/config/watch_map_test.cc | 63 +++++++++------------ 4 files changed, 42 insertions(+), 44 deletions(-) delete mode 100644 source/common/config/.watch_map.h.kate-swp diff --git a/source/common/config/.watch_map.h.kate-swp b/source/common/config/.watch_map.h.kate-swp deleted file mode 100644 index 490ebdf75fe3df6bf1a8fa63b5358cdd76f0b2b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1163 zcmZ9L%SyvQ6ox0YUhB2C-dp3Y3l(u$OTk425jV|3a8p7PNb`${ zz8xJFf*YL_^{>_nT7-2C3UI0KC4j!Xv9JeDgmd7L@D%t_I1la$Pdo3=fPKkl!RNwr z;7EAh;RSFkdBMpS!EMQxocYV(d&yV8L*Z5Mlkl4JKJ3}`L7?3a9C|OA%`)gUlZ~vL zdPkrzix2vq^Nkb?P_o$sFeRH!08_G&1g7MAwv8B}=7|xCw$`L*BNA^=wDk>&ZlofR zQ?&IyMO*VJ+Nz17Cn})oi4n@S)}(A}P0BWh4K+}9UuvT4fiUKHJ>d;-Q+N|>dENqt zl5c~p9(Ekw1zXPcoV)_=$a+=qS+@PCZa)oC7_R?a#{hP{GESm;eH^})+8>UP!+c0My%r?xK2eS=x=D}=3YsfwZu1evIt>xKe G)cOPMD|%A^ diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index beda8f2ae4249..c8c0e11e2bb0c 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -15,8 +15,8 @@ void WatchMap::removeWatch(Watch* watch) { watches_.erase(watch); } -std::pair, std::set> -WatchMap::updateWatchInterest(Watch* watch, const std::set& update_to_these_names) { +AddedRemoved WatchMap::updateWatchInterest(Watch* watch, + const std::set& update_to_these_names) { if (update_to_these_names.empty()) { wildcard_watches_.insert(watch); } else { @@ -35,8 +35,8 @@ WatchMap::updateWatchInterest(Watch* watch, const std::set& update_ watch->resource_names_ = update_to_these_names; - return std::make_pair(findAdditions(newly_added_to_watch, watch), - findRemovals(newly_removed_from_watch, watch)); + return AddedRemoved(findAdditions(newly_added_to_watch, watch), + findRemovals(newly_removed_from_watch, watch)); } absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& resource_name) { diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 87f419f4b3fd6..2c0107a2b0e24 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -18,6 +18,13 @@ namespace Config { struct Watch; using WatchPtr = std::unique_ptr; +struct AddedRemoved { + AddedRemoved(std::set added, std::set removed) + : added_(added), removed_(removed) {} + std::set added_; + std::set removed_; +}; + // Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same // resource name "X". The xDS machinery must return to each their very own subscription to X. // The xDS machinery's "watch" concept accomplishes that, while avoiding parallel redundant xDS @@ -48,11 +55,11 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable, std::set> - updateWatchInterest(Watch* watch, const std::set& update_to_these_names); + // Y will be in removed_. + AddedRemoved updateWatchInterest(Watch* watch, + const std::set& update_to_these_names); // SubscriptionCallbacks virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 0aec3897c8d1f..b5c3e6a5bd7ab 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -118,10 +118,9 @@ TEST(WatchMapTest, Basic) { { // The watch is interested in Alice and Bob... std::set update_to({"alice", "bob"}); - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch.get(), update_to); - EXPECT_EQ(update_to, added_removed.first); - EXPECT_TRUE(added_removed.second.empty()); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch.get(), update_to); + EXPECT_EQ(update_to, added_removed.added_); + EXPECT_TRUE(added_removed.removed_.empty()); // ...the update is going to contain Bob and Carol... Protobuf::RepeatedPtrField updated_resources; @@ -142,10 +141,9 @@ TEST(WatchMapTest, Basic) { { // The watch is now interested in Bob, Carol, Dave, Eve... std::set update_to({"bob", "carol", "dave", "eve"}); - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch.get(), update_to); - EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.first); - EXPECT_EQ(std::set({"alice"}), added_removed.second); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch.get(), update_to); + EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.added_); + EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // ...the update is going to contain Alice, Carol, Dave... Protobuf::RepeatedPtrField updated_resources; @@ -192,10 +190,9 @@ TEST(WatchMapTest, Overlap) { // First watch becomes interested. { std::set update_to({"alice", "dummy"}); - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch1.get(), update_to); - EXPECT_EQ(update_to, added_removed.first); // add to subscription - EXPECT_TRUE(added_removed.second.empty()); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), update_to); + EXPECT_EQ(update_to, added_removed.added_); // add to subscription + EXPECT_TRUE(added_removed.removed_.empty()); watch_map.updateWatchInterest(watch2.get(), {"dummy"}); // First watch receives update. @@ -206,10 +203,9 @@ TEST(WatchMapTest, Overlap) { // Second watch becomes interested. { std::set update_to({"alice", "dummy"}); - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch2.get(), update_to); - EXPECT_TRUE(added_removed.first.empty()); // nothing happens - EXPECT_TRUE(added_removed.second.empty()); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch2.get(), update_to); + EXPECT_TRUE(added_removed.added_.empty()); // nothing happens + EXPECT_TRUE(added_removed.removed_.empty()); // Both watches receive update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version2"); @@ -218,10 +214,9 @@ TEST(WatchMapTest, Overlap) { } // First watch loses interest. { - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch1.get(), {"dummy"}); - EXPECT_TRUE(added_removed.first.empty()); // nothing happens - EXPECT_TRUE(added_removed.second.empty()); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), {"dummy"}); + EXPECT_TRUE(added_removed.added_.empty()); // nothing happens + EXPECT_TRUE(added_removed.removed_.empty()); // *Only* second watch receives update. expectNoDeltaUpdate(callbacks1, "version3"); @@ -230,10 +225,9 @@ TEST(WatchMapTest, Overlap) { } // Second watch loses interest. { - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch2.get(), {"dummy"}); - EXPECT_TRUE(added_removed.first.empty()); - EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription + AddedRemoved added_removed = watch_map.updateWatchInterest(watch2.get(), {"dummy"}); + EXPECT_TRUE(added_removed.added_.empty()); + EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription } } @@ -258,10 +252,9 @@ TEST(WatchMapTest, AddRemoveAdd) { // First watch becomes interested. { std::set update_to({"alice", "dummy"}); - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch1.get(), update_to); - EXPECT_EQ(update_to, added_removed.first); // add to subscription - EXPECT_TRUE(added_removed.second.empty()); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), update_to); + EXPECT_EQ(update_to, added_removed.added_); // add to subscription + EXPECT_TRUE(added_removed.removed_.empty()); watch_map.updateWatchInterest(watch2.get(), {"dummy"}); // First watch receives update. @@ -271,10 +264,9 @@ TEST(WatchMapTest, AddRemoveAdd) { } // First watch loses interest. { - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch1.get(), {"dummy"}); - EXPECT_TRUE(added_removed.first.empty()); - EXPECT_EQ(std::set({"alice"}), added_removed.second); // remove from subscription + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), {"dummy"}); + EXPECT_TRUE(added_removed.added_.empty()); + EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription // (The xDS client should have responded to updateWatchInterest()'s return value by removing // Alice from the subscription, so onConfigUpdate() calls should be impossible right now.) @@ -282,10 +274,9 @@ TEST(WatchMapTest, AddRemoveAdd) { // Second watch becomes interested. { std::set update_to({"alice", "dummy"}); - std::pair, std::set> added_removed = - watch_map.updateWatchInterest(watch2.get(), update_to); - EXPECT_EQ(std::set({"alice"}), added_removed.first); // add to subscription - EXPECT_TRUE(added_removed.second.empty()); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch2.get(), update_to); + EXPECT_EQ(std::set({"alice"}), added_removed.added_); // add to subscription + EXPECT_TRUE(added_removed.removed_.empty()); // *Only* second watch receives update. expectNoDeltaUpdate(callbacks1, "version2"); From 5582614e44eeb5e5efa5b1aaaaac0f82968069c9 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Fri, 14 Jun 2019 17:55:56 -0400 Subject: [PATCH 14/27] remove virtual Signed-off-by: Fred Douglas --- source/common/config/watch_map.h | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 2c0107a2b0e24..5067f58645b7c 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -62,18 +62,15 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable& update_to_these_names); // SubscriptionCallbacks - virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) override; - virtual void - onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) override; - - virtual void onConfigUpdateFailed(const EnvoyException* e) override; - - virtual std::string resourceName(const ProtobufWkt::Any&) override { - NOT_IMPLEMENTED_GCOVR_EXCL_LINE; - } + void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; + + void onConfigUpdateFailed(const EnvoyException* e) override; + + std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } private: friend struct Watch; From a8d53c211ce559186b79e0ac2f5c1c88d24f3cf3 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Mon, 17 Jun 2019 14:21:41 -0400 Subject: [PATCH 15/27] add PNG diagram of intended usage of WatchMap Signed-off-by: Fred Douglas --- source/common/config/xDS_code_diagram.png | Bin 0 -> 97291 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 source/common/config/xDS_code_diagram.png diff --git a/source/common/config/xDS_code_diagram.png b/source/common/config/xDS_code_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..ef4df79cc1e30f59f865d5c29abc91ca05b19f72 GIT binary patch literal 97291 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%0A07Moj7KS0o@imd+yv_n<&&CTakng_yb3_4Oj{ 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 L(bFhWv%dB}cK|?= literal 0 HcmV?d00001 From e571c536b2ba16edd3da687ff0ab7d0331a16956 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Mon, 17 Jun 2019 14:50:11 -0400 Subject: [PATCH 16/27] add interface, move WatchMap to WatchMapImpl Signed-off-by: Fred Douglas --- include/envoy/config/BUILD | 8 +++ include/envoy/config/watch_map.h | 55 +++++++++++++++++ source/common/config/BUILD | 5 +- .../{watch_map.cc => watch_map_impl.cc} | 30 ++++----- .../config/{watch_map.h => watch_map_impl.h} | 61 +++++++------------ test/common/config/watch_map_test.cc | 28 ++++----- 6 files changed, 118 insertions(+), 69 deletions(-) create mode 100644 include/envoy/config/watch_map.h rename source/common/config/{watch_map.cc => watch_map_impl.cc} (84%) rename source/common/config/{watch_map.h => watch_map_impl.h} (70%) diff --git a/include/envoy/config/BUILD b/include/envoy/config/BUILD index ab8c3a0899f67..effbf5f41de6c 100644 --- a/include/envoy/config/BUILD +++ b/include/envoy/config/BUILD @@ -65,6 +65,14 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "watch_map_interface", + hdrs = ["watch_map.h"], + deps = [ + ":subscription_interface", + ], +) + envoy_cc_library( name = "xds_grpc_context_interface", hdrs = ["xds_grpc_context.h"], diff --git a/include/envoy/config/watch_map.h b/include/envoy/config/watch_map.h new file mode 100644 index 0000000000000..bbee092b942d2 --- /dev/null +++ b/include/envoy/config/watch_map.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" +#include "envoy/config/subscription.h" + +namespace Envoy { +namespace Config { + +struct Watch; +using WatchPtr = std::unique_ptr; + +struct AddedRemoved { + AddedRemoved(std::set added, std::set removed) + : added_(added), removed_(removed) {} + std::set added_; + std::set removed_; +}; + +class WatchMap { +public: + virtual ~WatchMap() = default; + + // Adds 'callbacks' to the WatchMap, with no resource names being watched. + // (Use updateWatchInterest() to add some names). + // Returns the newly added watch, to be used for updateWatchInterest. Destroy to remove from map. + virtual WatchPtr addWatch(SubscriptionCallbacks& callbacks) PURE; + + // Updates the set of resource names that the given watch should watch. + // Returns any resource name additions/removals that are unique across all watches. That is: + // 1) if 'resources' contains X and no other watch cares about X, X will be in added_. + // 2) if 'resources' does not contain Y, and this watch was the only one that cared about Y, + // Y will be in removed_. + virtual AddedRemoved updateWatchInterest(Watch* watch, + const std::set& update_to_these_names) PURE; + + // Intended to be called by the Watch's destructor. + // Expects that the watch to be removed has already had all of its resource names removed via + // updateWatchInterest(). + virtual void removeWatch(Watch* watch) PURE; +}; + +struct Watch { + Watch(WatchMap& owning_map, SubscriptionCallbacks& callbacks) + : owning_map_(owning_map), callbacks_(callbacks) {} + ~Watch() { owning_map_.removeWatch(this); } + WatchMap& owning_map_; + SubscriptionCallbacks& callbacks_; + std::set resource_names_; // must be sorted set, for set_difference. +}; + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/BUILD b/source/common/config/BUILD index cf260beae55ed..b3963e866d89a 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -415,10 +415,11 @@ envoy_cc_library( envoy_cc_library( name = "watch_map_lib", - srcs = ["watch_map.cc"], - hdrs = ["watch_map.h"], + srcs = ["watch_map_impl.cc"], + hdrs = ["watch_map_impl.h"], deps = [ "//include/envoy/config:subscription_interface", + "//include/envoy/config:watch_map_interface", "//source/common/common:assert_lib", "//source/common/common:minimal_logger_lib", "//source/common/protobuf", diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map_impl.cc similarity index 84% rename from source/common/config/watch_map.cc rename to source/common/config/watch_map_impl.cc index c8c0e11e2bb0c..bdc49afc4136c 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map_impl.cc @@ -1,22 +1,22 @@ -#include "common/config/watch_map.h" +#include "common/config/watch_map_impl.h" namespace Envoy { namespace Config { -WatchPtr WatchMap::addWatch(SubscriptionCallbacks& callbacks) { +WatchPtr WatchMapImpl::addWatch(SubscriptionCallbacks& callbacks) { auto watch = std::make_unique(*this, callbacks); wildcard_watches_.insert(watch.get()); watches_.insert(watch.get()); return watch; } -void WatchMap::removeWatch(Watch* watch) { +void WatchMapImpl::removeWatch(Watch* watch) { wildcard_watches_.erase(watch); // may or may not be in there, but we want it gone. watches_.erase(watch); } -AddedRemoved WatchMap::updateWatchInterest(Watch* watch, - const std::set& update_to_these_names) { +AddedRemoved WatchMapImpl::updateWatchInterest(Watch* watch, + const std::set& update_to_these_names) { if (update_to_these_names.empty()) { wildcard_watches_.insert(watch); } else { @@ -39,7 +39,7 @@ AddedRemoved WatchMap::updateWatchInterest(Watch* watch, findRemovals(newly_removed_from_watch, watch)); } -absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& resource_name) { +absl::flat_hash_set WatchMapImpl::watchesInterestedIn(const std::string& resource_name) { // Note that std::set_union needs sorted sets. Better to do it ourselves with insert(). absl::flat_hash_set ret = wildcard_watches_; auto watches_interested = watch_interest_.find(resource_name); @@ -51,10 +51,10 @@ absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& res return ret; } -void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) { +void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) { if (watches_.empty()) { - ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); + ENVOY_LOG(warn, "WatchMapImpl::onConfigUpdate: there are no watches!"); return; } SubscriptionCallbacks& name_getter = (*watches_.begin())->callbacks_; @@ -85,7 +85,7 @@ void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField } } -void WatchMap::onConfigUpdate( +void WatchMapImpl::onConfigUpdate( const Protobuf::RepeatedPtrField& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& system_version_info) { @@ -127,14 +127,14 @@ void WatchMap::onConfigUpdate( } } -void WatchMap::onConfigUpdateFailed(const EnvoyException* e) { +void WatchMapImpl::onConfigUpdateFailed(const EnvoyException* e) { for (auto& watch : watches_) { watch->callbacks_.onConfigUpdateFailed(e); } } -std::set WatchMap::findAdditions(const std::vector& newly_added_to_watch, - Watch* watch) { +std::set +WatchMapImpl::findAdditions(const std::vector& newly_added_to_watch, Watch* watch) { std::set newly_added_to_subscription; for (const auto& name : newly_added_to_watch) { auto entry = watch_interest_.find(name); @@ -149,12 +149,12 @@ std::set WatchMap::findAdditions(const std::vector& ne } std::set -WatchMap::findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) { +WatchMapImpl::findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) { std::set newly_removed_from_subscription; for (const auto& name : newly_removed_from_watch) { auto entry = watch_interest_.find(name); if (entry == watch_interest_.end()) { - ENVOY_LOG(warn, "WatchMap: tried to remove a watch from untracked resource {}", name); + ENVOY_LOG(warn, "WatchMapImpl: tried to remove a watch from untracked resource {}", name); continue; } diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map_impl.h similarity index 70% rename from source/common/config/watch_map.h rename to source/common/config/watch_map_impl.h index 5067f58645b7c..268193165dc3d 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map_impl.h @@ -4,7 +4,7 @@ #include #include -#include "envoy/config/subscription.h" +#include "envoy/config/watch_map.h" #include "common/common/assert.h" #include "common/common/logger.h" @@ -15,16 +15,6 @@ namespace Envoy { namespace Config { -struct Watch; -using WatchPtr = std::unique_ptr; - -struct AddedRemoved { - AddedRemoved(std::set added, std::set removed) - : added_(added), removed_(removed) {} - std::set added_; - std::set removed_; -}; - // Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same // resource name "X". The xDS machinery must return to each their very own subscription to X. // The xDS machinery's "watch" concept accomplishes that, while avoiding parallel redundant xDS @@ -44,14 +34,16 @@ struct AddedRemoved { // update the subscription accordingly. // // A WatchMap is assumed to be dedicated to a single type_url type of resource (EDS, CDS, etc). -class WatchMap : public SubscriptionCallbacks, public Logger::Loggable { +class WatchMapImpl : public WatchMap, + public SubscriptionCallbacks, + public Logger::Loggable { public: - WatchMap() {} + WatchMapImpl() = default; // Adds 'callbacks' to the WatchMap, with no resource names being watched. // (Use updateWatchInterest() to add some names). // Returns the newly added watch, to be used for updateWatchInterest. Destroy to remove from map. - WatchPtr addWatch(SubscriptionCallbacks& callbacks); + WatchPtr addWatch(SubscriptionCallbacks& callbacks) override; // Updates the set of resource names that the given watch should watch. // Returns any resource name additions/removals that are unique across all watches. That is: @@ -59,25 +51,27 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable& update_to_these_names); + const std::set& update_to_these_names) override; + + // Expects that the watch to be removed has already had all of its resource names removed via + // updateWatchInterest(). + void removeWatch(Watch* watch) override; // SubscriptionCallbacks - void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) override; - void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) override; + virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + virtual void + onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; - void onConfigUpdateFailed(const EnvoyException* e) override; + virtual void onConfigUpdateFailed(const EnvoyException* e) override; - std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + virtual std::string resourceName(const ProtobufWkt::Any&) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } private: - friend struct Watch; - // Expects that the watch to be removed has already had all of its resource names removed via - // updateWatchInterest(). - void removeWatch(Watch* watch); - // Given a list of names that are new to an individual watch, returns those names that are in fact // new to the entire subscription. std::set findAdditions(const std::vector& newly_added_to_watch, @@ -101,17 +95,8 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable> watch_interest_; - WatchMap(const WatchMap&) = delete; - WatchMap& operator=(const WatchMap&) = delete; -}; - -struct Watch { - Watch(WatchMap& owning_map, SubscriptionCallbacks& callbacks) - : owning_map_(owning_map), callbacks_(callbacks) {} - ~Watch() { owning_map_.removeWatch(this); } - WatchMap& owning_map_; - SubscriptionCallbacks& callbacks_; - std::set resource_names_; // must be sorted set, for set_difference. + WatchMapImpl(const WatchMapImpl&) = delete; + WatchMapImpl& operator=(const WatchMapImpl&) = delete; }; } // namespace Config diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index b5c3e6a5bd7ab..dffbf56aff6ea 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -4,7 +4,7 @@ #include "envoy/common/exception.h" #include "envoy/stats/scope.h" -#include "common/config/watch_map.h" +#include "common/config/watch_map_impl.h" #include "test/mocks/config/mocks.h" #include "test/test_common/utility.h" @@ -93,7 +93,7 @@ wrapInResource(const Protobuf::RepeatedPtrField& anys, // Similar to expectDeltaAndSotwUpdate(), but making the onConfigUpdate() happen, rather than // EXPECTing it. -void doDeltaAndSotwUpdate(WatchMap& watch_map, +void doDeltaAndSotwUpdate(WatchMapImpl& watch_map, Protobuf::RepeatedPtrField sotw_resources, std::vector removed_names, std::string version) { watch_map.onConfigUpdate(sotw_resources, version); @@ -110,9 +110,9 @@ void doDeltaAndSotwUpdate(WatchMap& watch_map, // Tests the simple case of a single watch. Checks that the watch will not be told of updates to // resources it doesn't care about. Checks that the watch can later decide it does care about them, // and then receive subsequent updates to them. -TEST(WatchMapTest, Basic) { +TEST(WatchMapImplTest, Basic) { NamedMockSubscriptionCallbacks callbacks; - WatchMap watch_map; + WatchMapImpl watch_map; WatchPtr watch = watch_map.addWatch(callbacks); { @@ -175,10 +175,10 @@ TEST(WatchMapTest, Basic) { // Second watch also loses interest ==> "remove it from subscription" // NOTE: we need the resource name "dummy" to keep either watch from ever having no names watched, // which is treated as interest in all names. -TEST(WatchMapTest, Overlap) { +TEST(WatchMapImplTest, Overlap) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; - WatchMap watch_map; + WatchMapImpl watch_map; WatchPtr watch1 = watch_map.addWatch(callbacks1); WatchPtr watch2 = watch_map.addWatch(callbacks2); @@ -237,10 +237,10 @@ TEST(WatchMapTest, Overlap) { // Second watch on that name ==> "add it to subscription" // NOTE: we need the resource name "dummy" to keep either watch from ever having no names watched, // which is treated as interest in all names. -TEST(WatchMapTest, AddRemoveAdd) { +TEST(WatchMapImplTest, AddRemoveAdd) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; - WatchMap watch_map; + WatchMapImpl watch_map; WatchPtr watch1 = watch_map.addWatch(callbacks1); WatchPtr watch2 = watch_map.addWatch(callbacks2); @@ -286,9 +286,9 @@ TEST(WatchMapTest, AddRemoveAdd) { } // Tests that nothing breaks if an update arrives that we entirely do not care about. -TEST(WatchMapTest, UninterestingUpdate) { +TEST(WatchMapImplTest, UninterestingUpdate) { NamedMockSubscriptionCallbacks callbacks; - WatchMap watch_map; + WatchMapImpl watch_map; WatchPtr watch = watch_map.addWatch(callbacks); watch_map.updateWatchInterest(watch.get(), {"alice"}); @@ -318,10 +318,10 @@ TEST(WatchMapTest, UninterestingUpdate) { // Tests that a watch that specifies no particular resource interest is treated as interested in // everything. -TEST(WatchMapTest, WatchingEverything) { +TEST(WatchMapImplTest, WatchingEverything) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; - WatchMap watch_map; + WatchMapImpl watch_map; WatchPtr watch1 = watch_map.addWatch(callbacks1); WatchPtr watch2 = watch_map.addWatch(callbacks2); // watch1 never specifies any names, and so is treated as interested in everything. @@ -351,11 +351,11 @@ TEST(WatchMapTest, WatchingEverything) { // exercise those cases. Also, the removal-only case tests that SotW does call a watch's // onConfigUpdate even if none of the watch's interested resources are among the updated resources. // (Which ensures we deliver empty config updates when a resource is dropped.) -TEST(WatchMapTest, DeltaOnConfigUpdate) { +TEST(WatchMapImplTest, DeltaOnConfigUpdate) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; NamedMockSubscriptionCallbacks callbacks3; - WatchMap watch_map; + WatchMapImpl watch_map; WatchPtr watch1 = watch_map.addWatch(callbacks1); WatchPtr watch2 = watch_map.addWatch(callbacks2); WatchPtr watch3 = watch_map.addWatch(callbacks3); From 3ce07f47920e999e5e99b768825f89c0dc8aa08c Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Mon, 17 Jun 2019 14:55:58 -0400 Subject: [PATCH 17/27] privatize SubscriptionCallbacks Signed-off-by: Fred Douglas --- source/common/config/watch_map_impl.h | 2 +- test/common/config/watch_map_test.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/common/config/watch_map_impl.h b/source/common/config/watch_map_impl.h index 268193165dc3d..6731a4db9f015 100644 --- a/source/common/config/watch_map_impl.h +++ b/source/common/config/watch_map_impl.h @@ -57,6 +57,7 @@ class WatchMapImpl : public WatchMap, // updateWatchInterest(). void removeWatch(Watch* watch) override; +private: // SubscriptionCallbacks virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) override; @@ -71,7 +72,6 @@ class WatchMapImpl : public WatchMap, NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } -private: // Given a list of names that are new to an individual watch, returns those names that are in fact // new to the entire subscription. std::set findAdditions(const std::vector& newly_added_to_watch, diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index dffbf56aff6ea..eb8a9fdd6c622 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -93,7 +93,7 @@ wrapInResource(const Protobuf::RepeatedPtrField& anys, // Similar to expectDeltaAndSotwUpdate(), but making the onConfigUpdate() happen, rather than // EXPECTing it. -void doDeltaAndSotwUpdate(WatchMapImpl& watch_map, +void doDeltaAndSotwUpdate(SubscriptionCallbacks& watch_map, Protobuf::RepeatedPtrField sotw_resources, std::vector removed_names, std::string version) { watch_map.onConfigUpdate(sotw_resources, version); From 34dd5cec49ec47f8889f32bfec671cfc521f7405 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Mon, 1 Jul 2019 15:55:20 -0400 Subject: [PATCH 18/27] move AddedRemoved, move png Signed-off-by: Fred Douglas --- include/envoy/config/watch_map.h | 4 ++-- source/{common/config => docs}/xDS_code_diagram.png | Bin 2 files changed, 2 insertions(+), 2 deletions(-) rename source/{common/config => docs}/xDS_code_diagram.png (100%) diff --git a/include/envoy/config/watch_map.h b/include/envoy/config/watch_map.h index bbee092b942d2..6050e03a52dcf 100644 --- a/include/envoy/config/watch_map.h +++ b/include/envoy/config/watch_map.h @@ -13,8 +13,8 @@ struct Watch; using WatchPtr = std::unique_ptr; struct AddedRemoved { - AddedRemoved(std::set added, std::set removed) - : added_(added), removed_(removed) {} + AddedRemoved(std::set&& added, std::set&& removed) + : added_(std::move(added)), removed_(std::move(removed)) {} std::set added_; std::set removed_; }; diff --git a/source/common/config/xDS_code_diagram.png b/source/docs/xDS_code_diagram.png similarity index 100% rename from source/common/config/xDS_code_diagram.png rename to source/docs/xDS_code_diagram.png From ea4a01fe7cc3a3be8844218d5d79d2de8171abba Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Tue, 9 Jul 2019 16:23:33 -0400 Subject: [PATCH 19/27] private friend Watch Signed-off-by: Fred Douglas --- source/common/config/watch_map_impl.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source/common/config/watch_map_impl.h b/source/common/config/watch_map_impl.h index 6731a4db9f015..5372cd556105e 100644 --- a/source/common/config/watch_map_impl.h +++ b/source/common/config/watch_map_impl.h @@ -53,11 +53,13 @@ class WatchMapImpl : public WatchMap, AddedRemoved updateWatchInterest(Watch* watch, const std::set& update_to_these_names) override; +private: + friend struct Watch; + // Meant to be called only by ~Watch(). // Expects that the watch to be removed has already had all of its resource names removed via // updateWatchInterest(). void removeWatch(Watch* watch) override; -private: // SubscriptionCallbacks virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) override; From 37ffff699acdca34223c94fa056545a8de4270d0 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Tue, 9 Jul 2019 16:27:47 -0400 Subject: [PATCH 20/27] clang tidy Signed-off-by: Fred Douglas --- source/common/config/watch_map_impl.h | 23 ++++++++++------------- test/common/config/watch_map_test.cc | 15 ++++++++------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/source/common/config/watch_map_impl.h b/source/common/config/watch_map_impl.h index 5372cd556105e..81f3a1d492e43 100644 --- a/source/common/config/watch_map_impl.h +++ b/source/common/config/watch_map_impl.h @@ -53,6 +53,9 @@ class WatchMapImpl : public WatchMap, AddedRemoved updateWatchInterest(Watch* watch, const std::set& update_to_these_names) override; + WatchMapImpl(const WatchMapImpl&) = delete; + WatchMapImpl& operator=(const WatchMapImpl&) = delete; + private: friend struct Watch; // Meant to be called only by ~Watch(). @@ -61,18 +64,15 @@ class WatchMapImpl : public WatchMap, void removeWatch(Watch* watch) override; // SubscriptionCallbacks - virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) override; - virtual void - onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) override; + void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; - virtual void onConfigUpdateFailed(const EnvoyException* e) override; + void onConfigUpdateFailed(const EnvoyException* e) override; - virtual std::string resourceName(const ProtobufWkt::Any&) override { - NOT_IMPLEMENTED_GCOVR_EXCL_LINE; - } + std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } // Given a list of names that are new to an individual watch, returns those names that are in fact // new to the entire subscription. @@ -96,9 +96,6 @@ class WatchMapImpl : public WatchMap, // 1) Acts as a reference count; no watches care anymore ==> the resource can be removed. // 2) Enables efficient lookup of all interested watches when a resource has been updated. absl::flat_hash_map> watch_interest_; - - WatchMapImpl(const WatchMapImpl&) = delete; - WatchMapImpl& operator=(const WatchMapImpl&) = delete; }; } // namespace Config diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index eb8a9fdd6c622..621a861bd6fd8 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -32,10 +32,10 @@ class NamedMockSubscriptionCallbacks // Specifically, as a simplification for these tests, every still-present resource is updated in // every update. Therefore, a resource can never show up in the SotW update but not the delta // update. We can therefore use the same expected_resources for both. -void expectDeltaAndSotwUpdate(NamedMockSubscriptionCallbacks& callbacks, - std::vector expected_resources, - std::vector expected_removals, - const std::string& version) { +void expectDeltaAndSotwUpdate( + NamedMockSubscriptionCallbacks& callbacks, + const std::vector& expected_resources, + const std::vector& expected_removals, const std::string& version) { EXPECT_CALL(callbacks, onConfigUpdate(_, version)) .WillOnce(Invoke( [expected_resources](const Protobuf::RepeatedPtrField& gotten_resources, @@ -94,14 +94,15 @@ wrapInResource(const Protobuf::RepeatedPtrField& anys, // Similar to expectDeltaAndSotwUpdate(), but making the onConfigUpdate() happen, rather than // EXPECTing it. void doDeltaAndSotwUpdate(SubscriptionCallbacks& watch_map, - Protobuf::RepeatedPtrField sotw_resources, - std::vector removed_names, std::string version) { + const Protobuf::RepeatedPtrField& sotw_resources, + const std::vector& removed_names, + const std::string& version) { watch_map.onConfigUpdate(sotw_resources, version); Protobuf::RepeatedPtrField delta_resources = wrapInResource(sotw_resources, version); Protobuf::RepeatedPtrField removed_names_proto; - for (auto n : removed_names) { + for (const auto& n : removed_names) { *removed_names_proto.Add() = n; } watch_map.onConfigUpdate(delta_resources, removed_names_proto, "version1"); From c238f1f85a773d274a0ac4ad0fba0c3ba86b98e7 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Mon, 15 Jul 2019 18:52:31 -0400 Subject: [PATCH 21/27] change Watch to class and interface Signed-off-by: Fred Douglas --- include/envoy/config/watch_map.h | 65 +++++++++++++++++--------- source/common/config/BUILD | 12 ++++- source/common/config/watch_impl.cc | 51 ++++++++++++++++++++ source/common/config/watch_impl.h | 35 ++++++++++++++ source/common/config/watch_map_impl.cc | 41 ++++++---------- source/common/config/watch_map_impl.h | 54 ++++----------------- test/common/config/watch_map_test.cc | 35 +++++++------- 7 files changed, 180 insertions(+), 113 deletions(-) create mode 100644 source/common/config/watch_impl.cc create mode 100644 source/common/config/watch_impl.h diff --git a/include/envoy/config/watch_map.h b/include/envoy/config/watch_map.h index 6050e03a52dcf..a8c7984d3de17 100644 --- a/include/envoy/config/watch_map.h +++ b/include/envoy/config/watch_map.h @@ -9,9 +9,17 @@ namespace Envoy { namespace Config { -struct Watch; -using WatchPtr = std::unique_ptr; +// Watch and WatchMap together manage "watches" of xDS resources. Several callers might ask +// for subscriptions to the same xDS resource "X". The xDS machinery must give each their +// very own Subscription object that receives updates on X, but we can't be sending multiple +// redundant requests to the server. Watch+WatchMap avoid that: each of those Subscriptions +// just holds a Watch on X; behind the scenes, GrpcMux (instructed by WatchMap) manages the +// actual xDS protocol requests for X. +// +// All of this is implicitly within the context of a given type_url (EDS, CDS, etc), and unaware +// of the watches for the other type_urls. +// pair, set>, but with meaningful field names. struct AddedRemoved { AddedRemoved(std::set&& added, std::set&& removed) : added_(std::move(added)), removed_(std::move(removed)) {} @@ -19,36 +27,49 @@ struct AddedRemoved { std::set removed_; }; +// A Watch object tracks the xDS resource names that some object in the wider Envoy codebase is +// interested in. The union of these names becomes the xDS subscription interest. +class Watch : public SubscriptionCallbacks { +public: + virtual ~Watch() = default; + + // Informs the parent WatchMap of an update to this Watch's set of watched resource names. + // The resource names in the returned AddedRemoved should be added to/removed from the actual + // conversation with the xDS server. + virtual AddedRemoved updateWatchInterest(const std::set& update_to_these_names) PURE; +}; +using WatchPtr = std::unique_ptr; + +// WatchMap tracks all of the Watches for a given type_url. When an individual Watch's interest +// changes, its parent WatchMap records the change, and determines what (if any) change to the +// overall xDS subscription interest is needed, based on all other Watches' interests. class WatchMap { public: virtual ~WatchMap() = default; - // Adds 'callbacks' to the WatchMap, with no resource names being watched. - // (Use updateWatchInterest() to add some names). - // Returns the newly added watch, to be used for updateWatchInterest. Destroy to remove from map. + // Adds 'callbacks' to the WatchMap as a wildcard watch. You can later call + // Watch::updateWatchInterest() to replace the wildcard matching with specific names. + // Returns ownership of the newly added watch. Destroy to remove from map. virtual WatchPtr addWatch(SubscriptionCallbacks& callbacks) PURE; - // Updates the set of resource names that the given watch should watch. - // Returns any resource name additions/removals that are unique across all watches. That is: - // 1) if 'resources' contains X and no other watch cares about X, X will be in added_. - // 2) if 'resources' does not contain Y, and this watch was the only one that cared about Y, - // Y will be in removed_. - virtual AddedRemoved updateWatchInterest(Watch* watch, - const std::set& update_to_these_names) PURE; - - // Intended to be called by the Watch's destructor. + // Intended to be called only by the Watch's destructor. // Expects that the watch to be removed has already had all of its resource names removed via // updateWatchInterest(). virtual void removeWatch(Watch* watch) PURE; -}; -struct Watch { - Watch(WatchMap& owning_map, SubscriptionCallbacks& callbacks) - : owning_map_(owning_map), callbacks_(callbacks) {} - ~Watch() { owning_map_.removeWatch(this); } - WatchMap& owning_map_; - SubscriptionCallbacks& callbacks_; - std::set resource_names_; // must be sorted set, for set_difference. + // While set to true (which is the default state of a newly added Watch), 'watch' will receive + // all resource updates in each new config update message. + virtual void setWildcardness(Watch* watch, bool is_wildcard) PURE; + + // Given a list of names that are new to an individual watch, returns those names that are in fact + // new to the entire subscription. + virtual std::set findAdditions(const std::vector& newly_added_to_watch, + Watch* watch) PURE; + + // Given a list of names that an individual watch no longer cares about, returns those names that + // in fact the entire subscription no longer cares about. + virtual std::set + findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) PURE; }; } // namespace Config diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 4601d62a17de7..8e1a579b42bc0 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -414,12 +414,22 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "watch_lib", + srcs = ["watch_impl.cc"], + hdrs = ["watch_impl.h"], + deps = [ + "//include/envoy/config:watch_map_interface", + "//source/common/protobuf", + ], +) + envoy_cc_library( name = "watch_map_lib", srcs = ["watch_map_impl.cc"], hdrs = ["watch_map_impl.h"], deps = [ - "//include/envoy/config:subscription_interface", + ":watch_lib", "//include/envoy/config:watch_map_interface", "//source/common/common:assert_lib", "//source/common/common:minimal_logger_lib", diff --git a/source/common/config/watch_impl.cc b/source/common/config/watch_impl.cc new file mode 100644 index 0000000000000..d12e533fb9aae --- /dev/null +++ b/source/common/config/watch_impl.cc @@ -0,0 +1,51 @@ +#include "common/config/watch_impl.h" + +namespace Envoy { +namespace Config { + +// The return value logic: +// 1) if update_to_these_names contains X, and no other Watch in the parent WatchMap +// cares about X, then X will be in added_. +// 2) if update_to_these_names does not contain Y, and this Watch was the only one in the +// WatchMap that cared about Y, then Y will be in removed_. +AddedRemoved WatchImpl::updateWatchInterest(const std::set& update_to_these_names) { + parent_map_.setWildcardness(this, update_to_these_names.empty()); + + std::vector newly_added_to_watch; + std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), + resource_names_.begin(), resource_names_.end(), + std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); + + std::vector newly_removed_from_watch; + std::set_difference(resource_names_.begin(), resource_names_.end(), update_to_these_names.begin(), + update_to_these_names.end(), + std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); + + resource_names_ = update_to_these_names; + + return AddedRemoved(parent_map_.findAdditions(newly_added_to_watch, this), + parent_map_.findRemovals(newly_removed_from_watch, this)); +} + +void WatchImpl::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) { + callbacks_.onConfigUpdate(resources, version_info); +} + +void WatchImpl::onConfigUpdate( + const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) { + callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); +} + +void WatchImpl::onConfigUpdateFailed(const EnvoyException* e) { + callbacks_.onConfigUpdateFailed(e); +} + +std::string WatchImpl::resourceName(const ProtobufWkt::Any& resource) { + return callbacks_.resourceName(resource); +} + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/watch_impl.h b/source/common/config/watch_impl.h new file mode 100644 index 0000000000000..e018e0cbe4dbd --- /dev/null +++ b/source/common/config/watch_impl.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +#include "envoy/config/watch_map.h" + +namespace Envoy { +namespace Config { + +class WatchImpl : public Watch { +public: + WatchImpl(WatchMap& parent_map, SubscriptionCallbacks& callbacks) + : parent_map_(parent_map), callbacks_(callbacks) {} + ~WatchImpl() override { parent_map_.removeWatch(this); } + + AddedRemoved updateWatchInterest(const std::set& update_to_these_names) override; + +private: + // SubscriptionCallbacks (all passthroughs to callbacks_) + void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; + void onConfigUpdateFailed(const EnvoyException* e) override; + std::string resourceName(const ProtobufWkt::Any&) override; + + WatchMap& parent_map_; + SubscriptionCallbacks& callbacks_; + std::set resource_names_; // must be sorted set, for set_difference. +}; + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/watch_map_impl.cc b/source/common/config/watch_map_impl.cc index bdc49afc4136c..3a45ae51df624 100644 --- a/source/common/config/watch_map_impl.cc +++ b/source/common/config/watch_map_impl.cc @@ -1,10 +1,12 @@ #include "common/config/watch_map_impl.h" +#include "common/config/watch_impl.h" + namespace Envoy { namespace Config { WatchPtr WatchMapImpl::addWatch(SubscriptionCallbacks& callbacks) { - auto watch = std::make_unique(*this, callbacks); + auto watch = std::make_unique(*this, callbacks); wildcard_watches_.insert(watch.get()); watches_.insert(watch.get()); return watch; @@ -15,28 +17,12 @@ void WatchMapImpl::removeWatch(Watch* watch) { watches_.erase(watch); } -AddedRemoved WatchMapImpl::updateWatchInterest(Watch* watch, - const std::set& update_to_these_names) { - if (update_to_these_names.empty()) { +void WatchMapImpl::setWildcardness(Watch* watch, bool is_wildcard) { + if (is_wildcard) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); } - - std::vector newly_added_to_watch; - std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), - watch->resource_names_.begin(), watch->resource_names_.end(), - std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); - - std::vector newly_removed_from_watch; - std::set_difference(watch->resource_names_.begin(), watch->resource_names_.end(), - update_to_these_names.begin(), update_to_these_names.end(), - std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); - - watch->resource_names_ = update_to_these_names; - - return AddedRemoved(findAdditions(newly_added_to_watch, watch), - findRemovals(newly_removed_from_watch, watch)); } absl::flat_hash_set WatchMapImpl::watchesInterestedIn(const std::string& resource_name) { @@ -57,7 +43,6 @@ void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrFieldcallbacks_; // Build a map from watches, to the set of updated resources that each watch cares about. Each // entry in the map is then a nice little bundle that can be fed directly into the individual @@ -65,7 +50,7 @@ void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrField> per_watch_updates; for (const auto& r : resources) { const absl::flat_hash_set& interested_in_r = - watchesInterestedIn(name_getter.resourceName(r)); + watchesInterestedIn((*watches_.begin())->resourceName(r)); for (const auto& interested_watch : interested_in_r) { per_watch_updates[interested_watch].Add()->CopyFrom(r); } @@ -78,9 +63,9 @@ void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrFieldcallbacks_.onConfigUpdate({}, version_info); + watch->onConfigUpdate({}, version_info); } else { - watch->callbacks_.onConfigUpdate(this_watch_updates->second, version_info); + watch->onConfigUpdate(this_watch_updates->second, version_info); } } } @@ -109,27 +94,27 @@ void WatchMapImpl::onConfigUpdate( // We just bundled up the updates into nice per-watch packages. Now, deliver them. for (const auto& added : per_watch_added) { - const Watch* cur_watch = added.first; + Watch* cur_watch = added.first; auto removed = per_watch_removed.find(cur_watch); if (removed == per_watch_removed.end()) { // additions only, no removals - cur_watch->callbacks_.onConfigUpdate(added.second, {}, system_version_info); + cur_watch->onConfigUpdate(added.second, {}, system_version_info); } else { // both additions and removals - cur_watch->callbacks_.onConfigUpdate(added.second, removed->second, system_version_info); + cur_watch->onConfigUpdate(added.second, removed->second, system_version_info); // Drop the removals now, so the final removals-only pass won't use them. per_watch_removed.erase(removed); } } // Any removals-only updates will not have been picked up in the per_watch_added loop. for (auto& removed : per_watch_removed) { - removed.first->callbacks_.onConfigUpdate({}, removed.second, system_version_info); + removed.first->onConfigUpdate({}, removed.second, system_version_info); } } void WatchMapImpl::onConfigUpdateFailed(const EnvoyException* e) { for (auto& watch : watches_) { - watch->callbacks_.onConfigUpdateFailed(e); + watch->onConfigUpdateFailed(e); } } diff --git a/source/common/config/watch_map_impl.h b/source/common/config/watch_map_impl.h index 81f3a1d492e43..31c27ea6a20aa 100644 --- a/source/common/config/watch_map_impl.h +++ b/source/common/config/watch_map_impl.h @@ -15,54 +15,28 @@ namespace Envoy { namespace Config { -// Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same -// resource name "X". The xDS machinery must return to each their very own subscription to X. -// The xDS machinery's "watch" concept accomplishes that, while avoiding parallel redundant xDS -// requests for X. Each of those subscriptions is viewed as a "watch" on X, while behind the scenes -// there is just a single real subscription to that resource name. -// -// This class maintains the watches<-->subscription mapping: it -// 1) delivers updates to all interested watches, and -// 2) tracks which resource names should be {added to,removed from} the subscription when the -// {first,last} watch on a resource name is {added,removed}. -// -// #1 is accomplished by WatchMap's implementation of the SubscriptionCallbacks interface. -// This interface allows the xDS client to just throw each xDS update message it receives directly -// into WatchMap::onConfigUpdate, rather than having to track the various watches' callbacks. -// -// The information for #2 is returned by updateWatchInterest(); the caller should use it to -// update the subscription accordingly. -// -// A WatchMap is assumed to be dedicated to a single type_url type of resource (EDS, CDS, etc). +// Because WatchMap implements the SubscriptionCallbacks interface, the xDS client can just +// throw xDS updates directly into WatchMap::onConfigUpdate, which then routes the right +// updates into the right Watches. class WatchMapImpl : public WatchMap, public SubscriptionCallbacks, public Logger::Loggable { public: WatchMapImpl() = default; - // Adds 'callbacks' to the WatchMap, with no resource names being watched. - // (Use updateWatchInterest() to add some names). - // Returns the newly added watch, to be used for updateWatchInterest. Destroy to remove from map. WatchPtr addWatch(SubscriptionCallbacks& callbacks) override; + void removeWatch(Watch* watch) override; + void setWildcardness(Watch* watch, bool is_wildcard) override; - // Updates the set of resource names that the given watch should watch. - // Returns any resource name additions/removals that are unique across all watches. That is: - // 1) if 'resources' contains X and no other watch cares about X, X will be in added_. - // 2) if 'resources' does not contain Y, and this watch was the only one that cared about Y, - // Y will be in removed_. - AddedRemoved updateWatchInterest(Watch* watch, - const std::set& update_to_these_names) override; + std::set findAdditions(const std::vector& newly_added_to_watch, + Watch* watch) override; + std::set findRemovals(const std::vector& newly_removed_from_watch, + Watch* watch) override; WatchMapImpl(const WatchMapImpl&) = delete; WatchMapImpl& operator=(const WatchMapImpl&) = delete; private: - friend struct Watch; - // Meant to be called only by ~Watch(). - // Expects that the watch to be removed has already had all of its resource names removed via - // updateWatchInterest(). - void removeWatch(Watch* watch) override; - // SubscriptionCallbacks void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, const std::string& version_info) override; @@ -74,16 +48,6 @@ class WatchMapImpl : public WatchMap, std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } - // Given a list of names that are new to an individual watch, returns those names that are in fact - // new to the entire subscription. - std::set findAdditions(const std::vector& newly_added_to_watch, - Watch* watch); - - // Given a list of names that an individual watch no longer cares about, returns those names that - // in fact the entire subscription no longer cares about. - std::set findRemovals(const std::vector& newly_removed_from_watch, - Watch* watch); - // Returns the union of watch_interest_[resource_name] and wildcard_watches_. absl::flat_hash_set watchesInterestedIn(const std::string& resource_name); diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 621a861bd6fd8..a43c85d998f1f 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -119,7 +119,7 @@ TEST(WatchMapImplTest, Basic) { { // The watch is interested in Alice and Bob... std::set update_to({"alice", "bob"}); - AddedRemoved added_removed = watch_map.updateWatchInterest(watch.get(), update_to); + AddedRemoved added_removed = watch->updateWatchInterest(update_to); EXPECT_EQ(update_to, added_removed.added_); EXPECT_TRUE(added_removed.removed_.empty()); @@ -142,7 +142,7 @@ TEST(WatchMapImplTest, Basic) { { // The watch is now interested in Bob, Carol, Dave, Eve... std::set update_to({"bob", "carol", "dave", "eve"}); - AddedRemoved added_removed = watch_map.updateWatchInterest(watch.get(), update_to); + AddedRemoved added_removed = watch->updateWatchInterest(update_to); EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.added_); EXPECT_EQ(std::set({"alice"}), added_removed.removed_); @@ -191,10 +191,10 @@ TEST(WatchMapImplTest, Overlap) { // First watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), update_to); + AddedRemoved added_removed = watch1->updateWatchInterest(update_to); EXPECT_EQ(update_to, added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); - watch_map.updateWatchInterest(watch2.get(), {"dummy"}); + watch2->updateWatchInterest({"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -204,7 +204,7 @@ TEST(WatchMapImplTest, Overlap) { // Second watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch_map.updateWatchInterest(watch2.get(), update_to); + AddedRemoved added_removed = watch2->updateWatchInterest(update_to); EXPECT_TRUE(added_removed.added_.empty()); // nothing happens EXPECT_TRUE(added_removed.removed_.empty()); @@ -215,7 +215,7 @@ TEST(WatchMapImplTest, Overlap) { } // First watch loses interest. { - AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), {"dummy"}); + AddedRemoved added_removed = watch1->updateWatchInterest({"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); // nothing happens EXPECT_TRUE(added_removed.removed_.empty()); @@ -226,7 +226,7 @@ TEST(WatchMapImplTest, Overlap) { } // Second watch loses interest. { - AddedRemoved added_removed = watch_map.updateWatchInterest(watch2.get(), {"dummy"}); + AddedRemoved added_removed = watch2->updateWatchInterest({"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription } @@ -253,10 +253,10 @@ TEST(WatchMapImplTest, AddRemoveAdd) { // First watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), update_to); + AddedRemoved added_removed = watch1->updateWatchInterest(update_to); EXPECT_EQ(update_to, added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); - watch_map.updateWatchInterest(watch2.get(), {"dummy"}); + watch2->updateWatchInterest({"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -265,7 +265,7 @@ TEST(WatchMapImplTest, AddRemoveAdd) { } // First watch loses interest. { - AddedRemoved added_removed = watch_map.updateWatchInterest(watch1.get(), {"dummy"}); + AddedRemoved added_removed = watch1->updateWatchInterest({"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription @@ -275,7 +275,7 @@ TEST(WatchMapImplTest, AddRemoveAdd) { // Second watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch_map.updateWatchInterest(watch2.get(), update_to); + AddedRemoved added_removed = watch2->updateWatchInterest(update_to); EXPECT_EQ(std::set({"alice"}), added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); @@ -291,7 +291,7 @@ TEST(WatchMapImplTest, UninterestingUpdate) { NamedMockSubscriptionCallbacks callbacks; WatchMapImpl watch_map; WatchPtr watch = watch_map.addWatch(callbacks); - watch_map.updateWatchInterest(watch.get(), {"alice"}); + watch->updateWatchInterest({"alice"}); Protobuf::RepeatedPtrField alice_update; envoy::api::v2::ClusterLoadAssignment alice; @@ -312,8 +312,9 @@ TEST(WatchMapImplTest, UninterestingUpdate) { expectNoDeltaUpdate(callbacks, "version3"); doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version3"); + // TODO TODO DONT NEED THIS // Clean removal of the watch: first update to "interested in nothing", then remove. - watch_map.updateWatchInterest(watch.get(), {}); + watch->updateWatchInterest({}); watch.reset(); } @@ -326,7 +327,7 @@ TEST(WatchMapImplTest, WatchingEverything) { WatchPtr watch1 = watch_map.addWatch(callbacks1); WatchPtr watch2 = watch_map.addWatch(callbacks2); // watch1 never specifies any names, and so is treated as interested in everything. - watch_map.updateWatchInterest(watch2.get(), {"alice"}); + watch2->updateWatchInterest({"alice"}); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -360,9 +361,9 @@ TEST(WatchMapImplTest, DeltaOnConfigUpdate) { WatchPtr watch1 = watch_map.addWatch(callbacks1); WatchPtr watch2 = watch_map.addWatch(callbacks2); WatchPtr watch3 = watch_map.addWatch(callbacks3); - watch_map.updateWatchInterest(watch1.get(), {"updated"}); - watch_map.updateWatchInterest(watch2.get(), {"updated", "removed"}); - watch_map.updateWatchInterest(watch3.get(), {"removed"}); + watch1->updateWatchInterest({"updated"}); + watch2->updateWatchInterest({"updated", "removed"}); + watch3->updateWatchInterest({"removed"}); Protobuf::RepeatedPtrField update; envoy::api::v2::ClusterLoadAssignment updated; From 8010950b4bd5165f55d9c8806fbff2d4d0bd55fa Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Mon, 15 Jul 2019 19:09:28 -0400 Subject: [PATCH 22/27] add another word to the dictionary Signed-off-by: Fred Douglas --- test/common/config/watch_map_test.cc | 1 - tools/spelling_dictionary.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index a43c85d998f1f..f718ba87da05b 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -312,7 +312,6 @@ TEST(WatchMapImplTest, UninterestingUpdate) { expectNoDeltaUpdate(callbacks, "version3"); doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version3"); - // TODO TODO DONT NEED THIS // Clean removal of the watch: first update to "interested in nothing", then remove. watch->updateWatchInterest({}); watch.reset(); diff --git a/tools/spelling_dictionary.txt b/tools/spelling_dictionary.txt index 03cc7e808c119..13854b0842432 100644 --- a/tools/spelling_dictionary.txt +++ b/tools/spelling_dictionary.txt @@ -624,6 +624,7 @@ params paren parentid parsers +passthroughs pcall pcap pclose From 8df252b0e5d083f51b456d5b80cc5d622562f9d1 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 31 Jul 2019 13:03:05 -0400 Subject: [PATCH 23/27] back to earlier design Signed-off-by: Fred Douglas --- include/envoy/config/watch_map.h | 76 ----------- source/common/config/BUILD | 17 +-- source/common/config/watch_impl.cc | 51 -------- source/common/config/watch_impl.h | 35 ------ .../{watch_map_impl.cc => watch_map.cc} | 79 +++++++----- source/common/config/watch_map.h | 118 ++++++++++++++++++ source/common/config/watch_map_impl.h | 66 ---------- test/common/config/watch_map_test.cc | 102 ++++++++------- 8 files changed, 227 insertions(+), 317 deletions(-) delete mode 100644 include/envoy/config/watch_map.h delete mode 100644 source/common/config/watch_impl.cc delete mode 100644 source/common/config/watch_impl.h rename source/common/config/{watch_map_impl.cc => watch_map.cc} (60%) create mode 100644 source/common/config/watch_map.h delete mode 100644 source/common/config/watch_map_impl.h diff --git a/include/envoy/config/watch_map.h b/include/envoy/config/watch_map.h deleted file mode 100644 index a8c7984d3de17..0000000000000 --- a/include/envoy/config/watch_map.h +++ /dev/null @@ -1,76 +0,0 @@ -#pragma once - -#include -#include - -#include "envoy/common/pure.h" -#include "envoy/config/subscription.h" - -namespace Envoy { -namespace Config { - -// Watch and WatchMap together manage "watches" of xDS resources. Several callers might ask -// for subscriptions to the same xDS resource "X". The xDS machinery must give each their -// very own Subscription object that receives updates on X, but we can't be sending multiple -// redundant requests to the server. Watch+WatchMap avoid that: each of those Subscriptions -// just holds a Watch on X; behind the scenes, GrpcMux (instructed by WatchMap) manages the -// actual xDS protocol requests for X. -// -// All of this is implicitly within the context of a given type_url (EDS, CDS, etc), and unaware -// of the watches for the other type_urls. - -// pair, set>, but with meaningful field names. -struct AddedRemoved { - AddedRemoved(std::set&& added, std::set&& removed) - : added_(std::move(added)), removed_(std::move(removed)) {} - std::set added_; - std::set removed_; -}; - -// A Watch object tracks the xDS resource names that some object in the wider Envoy codebase is -// interested in. The union of these names becomes the xDS subscription interest. -class Watch : public SubscriptionCallbacks { -public: - virtual ~Watch() = default; - - // Informs the parent WatchMap of an update to this Watch's set of watched resource names. - // The resource names in the returned AddedRemoved should be added to/removed from the actual - // conversation with the xDS server. - virtual AddedRemoved updateWatchInterest(const std::set& update_to_these_names) PURE; -}; -using WatchPtr = std::unique_ptr; - -// WatchMap tracks all of the Watches for a given type_url. When an individual Watch's interest -// changes, its parent WatchMap records the change, and determines what (if any) change to the -// overall xDS subscription interest is needed, based on all other Watches' interests. -class WatchMap { -public: - virtual ~WatchMap() = default; - - // Adds 'callbacks' to the WatchMap as a wildcard watch. You can later call - // Watch::updateWatchInterest() to replace the wildcard matching with specific names. - // Returns ownership of the newly added watch. Destroy to remove from map. - virtual WatchPtr addWatch(SubscriptionCallbacks& callbacks) PURE; - - // Intended to be called only by the Watch's destructor. - // Expects that the watch to be removed has already had all of its resource names removed via - // updateWatchInterest(). - virtual void removeWatch(Watch* watch) PURE; - - // While set to true (which is the default state of a newly added Watch), 'watch' will receive - // all resource updates in each new config update message. - virtual void setWildcardness(Watch* watch, bool is_wildcard) PURE; - - // Given a list of names that are new to an individual watch, returns those names that are in fact - // new to the entire subscription. - virtual std::set findAdditions(const std::vector& newly_added_to_watch, - Watch* watch) PURE; - - // Given a list of names that an individual watch no longer cares about, returns those names that - // in fact the entire subscription no longer cares about. - virtual std::set - findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) PURE; -}; - -} // namespace Config -} // namespace Envoy diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 8e1a579b42bc0..31926b40e55f1 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -414,23 +414,12 @@ envoy_cc_library( ], ) -envoy_cc_library( - name = "watch_lib", - srcs = ["watch_impl.cc"], - hdrs = ["watch_impl.h"], - deps = [ - "//include/envoy/config:watch_map_interface", - "//source/common/protobuf", - ], -) - envoy_cc_library( name = "watch_map_lib", - srcs = ["watch_map_impl.cc"], - hdrs = ["watch_map_impl.h"], + srcs = ["watch_map.cc"], + hdrs = ["watch_map.h"], deps = [ - ":watch_lib", - "//include/envoy/config:watch_map_interface", + "//include/envoy/config:subscription_interface", "//source/common/common:assert_lib", "//source/common/common:minimal_logger_lib", "//source/common/protobuf", diff --git a/source/common/config/watch_impl.cc b/source/common/config/watch_impl.cc deleted file mode 100644 index d12e533fb9aae..0000000000000 --- a/source/common/config/watch_impl.cc +++ /dev/null @@ -1,51 +0,0 @@ -#include "common/config/watch_impl.h" - -namespace Envoy { -namespace Config { - -// The return value logic: -// 1) if update_to_these_names contains X, and no other Watch in the parent WatchMap -// cares about X, then X will be in added_. -// 2) if update_to_these_names does not contain Y, and this Watch was the only one in the -// WatchMap that cared about Y, then Y will be in removed_. -AddedRemoved WatchImpl::updateWatchInterest(const std::set& update_to_these_names) { - parent_map_.setWildcardness(this, update_to_these_names.empty()); - - std::vector newly_added_to_watch; - std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), - resource_names_.begin(), resource_names_.end(), - std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); - - std::vector newly_removed_from_watch; - std::set_difference(resource_names_.begin(), resource_names_.end(), update_to_these_names.begin(), - update_to_these_names.end(), - std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); - - resource_names_ = update_to_these_names; - - return AddedRemoved(parent_map_.findAdditions(newly_added_to_watch, this), - parent_map_.findRemovals(newly_removed_from_watch, this)); -} - -void WatchImpl::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) { - callbacks_.onConfigUpdate(resources, version_info); -} - -void WatchImpl::onConfigUpdate( - const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) { - callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); -} - -void WatchImpl::onConfigUpdateFailed(const EnvoyException* e) { - callbacks_.onConfigUpdateFailed(e); -} - -std::string WatchImpl::resourceName(const ProtobufWkt::Any& resource) { - return callbacks_.resourceName(resource); -} - -} // namespace Config -} // namespace Envoy diff --git a/source/common/config/watch_impl.h b/source/common/config/watch_impl.h deleted file mode 100644 index e018e0cbe4dbd..0000000000000 --- a/source/common/config/watch_impl.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -#include - -#include "envoy/config/watch_map.h" - -namespace Envoy { -namespace Config { - -class WatchImpl : public Watch { -public: - WatchImpl(WatchMap& parent_map, SubscriptionCallbacks& callbacks) - : parent_map_(parent_map), callbacks_(callbacks) {} - ~WatchImpl() override { parent_map_.removeWatch(this); } - - AddedRemoved updateWatchInterest(const std::set& update_to_these_names) override; - -private: - // SubscriptionCallbacks (all passthroughs to callbacks_) - void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) override; - void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) override; - void onConfigUpdateFailed(const EnvoyException* e) override; - std::string resourceName(const ProtobufWkt::Any&) override; - - WatchMap& parent_map_; - SubscriptionCallbacks& callbacks_; - std::set resource_names_; // must be sorted set, for set_difference. -}; - -} // namespace Config -} // namespace Envoy diff --git a/source/common/config/watch_map_impl.cc b/source/common/config/watch_map.cc similarity index 60% rename from source/common/config/watch_map_impl.cc rename to source/common/config/watch_map.cc index 3a45ae51df624..e29455f3d4cdf 100644 --- a/source/common/config/watch_map_impl.cc +++ b/source/common/config/watch_map.cc @@ -1,31 +1,46 @@ -#include "common/config/watch_map_impl.h" - -#include "common/config/watch_impl.h" +#include "common/config/watch_map.h" namespace Envoy { namespace Config { -WatchPtr WatchMapImpl::addWatch(SubscriptionCallbacks& callbacks) { - auto watch = std::make_unique(*this, callbacks); - wildcard_watches_.insert(watch.get()); - watches_.insert(watch.get()); - return watch; +Watch* WatchMap::addWatch(SubscriptionCallbacks& callbacks) { + auto watch = std::make_unique(callbacks); + Watch* watch_ptr = watch.get(); + wildcard_watches_.insert(watch_ptr); + watches_.insert(std::move(watch)); + return watch_ptr; } -void WatchMapImpl::removeWatch(Watch* watch) { +void WatchMap::removeWatch(Watch* watch) { wildcard_watches_.erase(watch); // may or may not be in there, but we want it gone. watches_.erase(watch); } -void WatchMapImpl::setWildcardness(Watch* watch, bool is_wildcard) { - if (is_wildcard) { +AddedRemoved WatchMap::updateWatchInterest(Watch* watch, + const std::set& update_to_these_names) { + if (update_to_these_names.empty()) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); } + + std::vector newly_added_to_watch; + std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), + watch->resource_names_.begin(), watch->resource_names_.end(), + std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); + + std::vector newly_removed_from_watch; + std::set_difference(watch->resource_names_.begin(), watch->resource_names_.end(), + update_to_these_names.begin(), update_to_these_names.end(), + std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); + + watch->resource_names_ = update_to_these_names; + + return AddedRemoved(findAdditions(newly_added_to_watch, watch), + findRemovals(newly_removed_from_watch, watch)); } -absl::flat_hash_set WatchMapImpl::watchesInterestedIn(const std::string& resource_name) { +absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& resource_name) { // Note that std::set_union needs sorted sets. Better to do it ourselves with insert(). absl::flat_hash_set ret = wildcard_watches_; auto watches_interested = watch_interest_.find(resource_name); @@ -37,12 +52,13 @@ absl::flat_hash_set WatchMapImpl::watchesInterestedIn(const std::string& return ret; } -void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) { +void WatchMap::onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) { if (watches_.empty()) { - ENVOY_LOG(warn, "WatchMapImpl::onConfigUpdate: there are no watches!"); + ENVOY_LOG(warn, "WatchMap::onConfigUpdate: there are no watches!"); return; } + SubscriptionCallbacks& name_getter = (*watches_.begin())->callbacks_; // Build a map from watches, to the set of updated resources that each watch cares about. Each // entry in the map is then a nice little bundle that can be fed directly into the individual @@ -50,7 +66,7 @@ void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrField> per_watch_updates; for (const auto& r : resources) { const absl::flat_hash_set& interested_in_r = - watchesInterestedIn((*watches_.begin())->resourceName(r)); + watchesInterestedIn(name_getter.resourceName(r)); for (const auto& interested_watch : interested_in_r) { per_watch_updates[interested_watch].Add()->CopyFrom(r); } @@ -63,14 +79,14 @@ void WatchMapImpl::onConfigUpdate(const Protobuf::RepeatedPtrFieldonConfigUpdate({}, version_info); + watch->callbacks_.onConfigUpdate({}, version_info); } else { - watch->onConfigUpdate(this_watch_updates->second, version_info); + watch->callbacks_.onConfigUpdate(this_watch_updates->second, version_info); } } } -void WatchMapImpl::onConfigUpdate( +void WatchMap::onConfigUpdate( const Protobuf::RepeatedPtrField& added_resources, const Protobuf::RepeatedPtrField& removed_resources, const std::string& system_version_info) { @@ -94,32 +110,32 @@ void WatchMapImpl::onConfigUpdate( // We just bundled up the updates into nice per-watch packages. Now, deliver them. for (const auto& added : per_watch_added) { - Watch* cur_watch = added.first; + const Watch* cur_watch = added.first; auto removed = per_watch_removed.find(cur_watch); if (removed == per_watch_removed.end()) { // additions only, no removals - cur_watch->onConfigUpdate(added.second, {}, system_version_info); + cur_watch->callbacks_.onConfigUpdate(added.second, {}, system_version_info); } else { // both additions and removals - cur_watch->onConfigUpdate(added.second, removed->second, system_version_info); + cur_watch->callbacks_.onConfigUpdate(added.second, removed->second, system_version_info); // Drop the removals now, so the final removals-only pass won't use them. per_watch_removed.erase(removed); } } // Any removals-only updates will not have been picked up in the per_watch_added loop. for (auto& removed : per_watch_removed) { - removed.first->onConfigUpdate({}, removed.second, system_version_info); + removed.first->callbacks_.onConfigUpdate({}, removed.second, system_version_info); } } -void WatchMapImpl::onConfigUpdateFailed(const EnvoyException* e) { +void WatchMap::onConfigUpdateFailed(const EnvoyException* e) { for (auto& watch : watches_) { - watch->onConfigUpdateFailed(e); + watch->callbacks_.onConfigUpdateFailed(e); } } -std::set -WatchMapImpl::findAdditions(const std::vector& newly_added_to_watch, Watch* watch) { +std::set WatchMap::findAdditions(const std::vector& newly_added_to_watch, + Watch* watch) { std::set newly_added_to_subscription; for (const auto& name : newly_added_to_watch) { auto entry = watch_interest_.find(name); @@ -134,14 +150,13 @@ WatchMapImpl::findAdditions(const std::vector& newly_added_to_watch } std::set -WatchMapImpl::findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) { +WatchMap::findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) { std::set newly_removed_from_subscription; for (const auto& name : newly_removed_from_watch) { auto entry = watch_interest_.find(name); - if (entry == watch_interest_.end()) { - ENVOY_LOG(warn, "WatchMapImpl: tried to remove a watch from untracked resource {}", name); - continue; - } + RELEASE_ASSERT( + entry != watch_interest_.end(), + fmt::format("WatchMap: tried to remove a watch from untracked resource {}", name)); entry->second.erase(watch); if (entry->second.empty()) { diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h new file mode 100644 index 0000000000000..e7f4d20a08ec9 --- /dev/null +++ b/source/common/config/watch_map.h @@ -0,0 +1,118 @@ +#pragma once + +#include +#include +#include + +#include "envoy/config/subscription.h" + +#include "common/common/assert.h" +#include "common/common/logger.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" + +namespace Envoy { +namespace Config { + +struct AddedRemoved { + AddedRemoved(std::set&& added, std::set&& removed) + : added_(std::move(added)), removed_(std::move(removed)) {} + std::set added_; + std::set removed_; +}; + +struct Watch { + Watch(SubscriptionCallbacks& callbacks) : callbacks_(callbacks) {} + SubscriptionCallbacks& callbacks_; + std::set resource_names_; // must be sorted set, for set_difference. +}; + +// NOTE: Users are responsible for eventually calling removeWatch() on the Watch* returned +// by addWatch(). We don't expect there to be new users of this class beyond +// NewGrpcMuxImpl and DeltaSubscriptionImpl (TODO(fredlas) to be renamed). +// +// Manages "watches" of xDS resources. Several xDS callers might ask for a subscription to the same +// resource name "X". The xDS machinery must return to each their very own subscription to X. +// The xDS machinery's "watch" concept accomplishes that, while avoiding parallel redundant xDS +// requests for X. Each of those subscriptions is viewed as a "watch" on X, while behind the scenes +// there is just a single real subscription to that resource name. +// +// This class maintains the watches<-->subscription mapping: it +// 1) delivers updates to all interested watches, and +// 2) tracks which resource names should be {added to,removed from} the subscription when the +// {first,last} watch on a resource name is {added,removed}. +// +// #1 is accomplished by WatchMap's implementation of the SubscriptionCallbacks interface. +// This interface allows the xDS client to just throw each xDS update message it receives directly +// into WatchMap::onConfigUpdate, rather than having to track the various watches' callbacks. +// +// The information for #2 is returned by updateWatchInterest(); the caller should use it to +// update the subscription accordingly. +// +// A WatchMap is assumed to be dedicated to a single type_url type of resource (EDS, CDS, etc). +class WatchMap : public SubscriptionCallbacks, public Logger::Loggable { +public: + WatchMap() = default; + + // Adds 'callbacks' to the WatchMap, with every possible resource being watched. + // (Use updateWatchInterest() to narrow it down to some specific names). + // Returns the newly added watch, to be used with updateWatchInterest and removeWatch. + Watch* addWatch(SubscriptionCallbacks& callbacks); + + // Updates the set of resource names that the given watch should watch. + // Returns any resource name additions/removals that are unique across all watches. That is: + // 1) if 'resources' contains X and no other watch cares about X, X will be in added_. + // 2) if 'resources' does not contain Y, and this watch was the only one that cared about Y, + // Y will be in removed_. + AddedRemoved updateWatchInterest(Watch* watch, + const std::set& update_to_these_names); + + // Expects that the watch to be removed has already had all of its resource names removed via + // updateWatchInterest(). + void removeWatch(Watch* watch); + + // SubscriptionCallbacks + virtual void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + virtual void + onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; + + virtual void onConfigUpdateFailed(const EnvoyException* e) override; + + virtual std::string resourceName(const ProtobufWkt::Any&) override { + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + + WatchMap(const WatchMap&) = delete; + WatchMap& operator=(const WatchMap&) = delete; + +private: + // Given a list of names that are new to an individual watch, returns those names that are in fact + // new to the entire subscription. + std::set findAdditions(const std::vector& newly_added_to_watch, + Watch* watch); + + // Given a list of names that an individual watch no longer cares about, returns those names that + // in fact the entire subscription no longer cares about. + std::set findRemovals(const std::vector& newly_removed_from_watch, + Watch* watch); + + // Returns the union of watch_interest_[resource_name] and wildcard_watches_. + absl::flat_hash_set watchesInterestedIn(const std::string& resource_name); + + absl::flat_hash_set> watches_; + + // Watches whose interest set is currently empty, which is interpreted as "everything". + absl::flat_hash_set wildcard_watches_; + + // Maps a resource name to the set of watches interested in that resource. Has two purposes: + // 1) Acts as a reference count; no watches care anymore ==> the resource can be removed. + // 2) Enables efficient lookup of all interested watches when a resource has been updated. + absl::flat_hash_map> watch_interest_; +}; + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/watch_map_impl.h b/source/common/config/watch_map_impl.h deleted file mode 100644 index 31c27ea6a20aa..0000000000000 --- a/source/common/config/watch_map_impl.h +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "envoy/config/watch_map.h" - -#include "common/common/assert.h" -#include "common/common/logger.h" - -#include "absl/container/flat_hash_map.h" -#include "absl/container/flat_hash_set.h" - -namespace Envoy { -namespace Config { - -// Because WatchMap implements the SubscriptionCallbacks interface, the xDS client can just -// throw xDS updates directly into WatchMap::onConfigUpdate, which then routes the right -// updates into the right Watches. -class WatchMapImpl : public WatchMap, - public SubscriptionCallbacks, - public Logger::Loggable { -public: - WatchMapImpl() = default; - - WatchPtr addWatch(SubscriptionCallbacks& callbacks) override; - void removeWatch(Watch* watch) override; - void setWildcardness(Watch* watch, bool is_wildcard) override; - - std::set findAdditions(const std::vector& newly_added_to_watch, - Watch* watch) override; - std::set findRemovals(const std::vector& newly_removed_from_watch, - Watch* watch) override; - - WatchMapImpl(const WatchMapImpl&) = delete; - WatchMapImpl& operator=(const WatchMapImpl&) = delete; - -private: - // SubscriptionCallbacks - void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, - const std::string& version_info) override; - void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) override; - - void onConfigUpdateFailed(const EnvoyException* e) override; - - std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } - - // Returns the union of watch_interest_[resource_name] and wildcard_watches_. - absl::flat_hash_set watchesInterestedIn(const std::string& resource_name); - - absl::flat_hash_set watches_; - - // Watches whose interest set is currently empty, which is interpreted as "everything". - absl::flat_hash_set wildcard_watches_; - - // Maps a resource name to the set of watches interested in that resource. Has two purposes: - // 1) Acts as a reference count; no watches care anymore ==> the resource can be removed. - // 2) Enables efficient lookup of all interested watches when a resource has been updated. - absl::flat_hash_map> watch_interest_; -}; - -} // namespace Config -} // namespace Envoy diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index f718ba87da05b..b7ab15ac7e131 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -4,7 +4,7 @@ #include "envoy/common/exception.h" #include "envoy/stats/scope.h" -#include "common/config/watch_map_impl.h" +#include "common/config/watch_map.h" #include "test/mocks/config/mocks.h" #include "test/test_common/utility.h" @@ -111,15 +111,15 @@ void doDeltaAndSotwUpdate(SubscriptionCallbacks& watch_map, // Tests the simple case of a single watch. Checks that the watch will not be told of updates to // resources it doesn't care about. Checks that the watch can later decide it does care about them, // and then receive subsequent updates to them. -TEST(WatchMapImplTest, Basic) { +TEST(WatchMapTest, Basic) { NamedMockSubscriptionCallbacks callbacks; - WatchMapImpl watch_map; - WatchPtr watch = watch_map.addWatch(callbacks); + WatchMap watch_map; + Watch* watch = watch_map.addWatch(callbacks); { // The watch is interested in Alice and Bob... std::set update_to({"alice", "bob"}); - AddedRemoved added_removed = watch->updateWatchInterest(update_to); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch, update_to); EXPECT_EQ(update_to, added_removed.added_); EXPECT_TRUE(added_removed.removed_.empty()); @@ -142,7 +142,7 @@ TEST(WatchMapImplTest, Basic) { { // The watch is now interested in Bob, Carol, Dave, Eve... std::set update_to({"bob", "carol", "dave", "eve"}); - AddedRemoved added_removed = watch->updateWatchInterest(update_to); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch, update_to); EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.added_); EXPECT_EQ(std::set({"alice"}), added_removed.removed_); @@ -166,7 +166,6 @@ TEST(WatchMapImplTest, Basic) { expectDeltaAndSotwUpdate(callbacks, expected_resources, {"bob"}, "version2"); doDeltaAndSotwUpdate(watch_map, updated_resources, {"bob"}, "version2"); } - watch.reset(); } // Checks the following: @@ -176,12 +175,12 @@ TEST(WatchMapImplTest, Basic) { // Second watch also loses interest ==> "remove it from subscription" // NOTE: we need the resource name "dummy" to keep either watch from ever having no names watched, // which is treated as interest in all names. -TEST(WatchMapImplTest, Overlap) { +TEST(WatchMapTest, Overlap) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; - WatchMapImpl watch_map; - WatchPtr watch1 = watch_map.addWatch(callbacks1); - WatchPtr watch2 = watch_map.addWatch(callbacks2); + WatchMap watch_map; + Watch* watch1 = watch_map.addWatch(callbacks1); + Watch* watch2 = watch_map.addWatch(callbacks2); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -191,10 +190,10 @@ TEST(WatchMapImplTest, Overlap) { // First watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch1->updateWatchInterest(update_to); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, update_to); EXPECT_EQ(update_to, added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); - watch2->updateWatchInterest({"dummy"}); + watch_map.updateWatchInterest(watch2, {"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -204,7 +203,7 @@ TEST(WatchMapImplTest, Overlap) { // Second watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch2->updateWatchInterest(update_to); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch2, update_to); EXPECT_TRUE(added_removed.added_.empty()); // nothing happens EXPECT_TRUE(added_removed.removed_.empty()); @@ -215,7 +214,7 @@ TEST(WatchMapImplTest, Overlap) { } // First watch loses interest. { - AddedRemoved added_removed = watch1->updateWatchInterest({"dummy"}); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, {"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); // nothing happens EXPECT_TRUE(added_removed.removed_.empty()); @@ -226,7 +225,7 @@ TEST(WatchMapImplTest, Overlap) { } // Second watch loses interest. { - AddedRemoved added_removed = watch2->updateWatchInterest({"dummy"}); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch2, {"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription } @@ -238,12 +237,12 @@ TEST(WatchMapImplTest, Overlap) { // Second watch on that name ==> "add it to subscription" // NOTE: we need the resource name "dummy" to keep either watch from ever having no names watched, // which is treated as interest in all names. -TEST(WatchMapImplTest, AddRemoveAdd) { +TEST(WatchMapTest, AddRemoveAdd) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; - WatchMapImpl watch_map; - WatchPtr watch1 = watch_map.addWatch(callbacks1); - WatchPtr watch2 = watch_map.addWatch(callbacks2); + WatchMap watch_map; + Watch* watch1 = watch_map.addWatch(callbacks1); + Watch* watch2 = watch_map.addWatch(callbacks2); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -253,10 +252,10 @@ TEST(WatchMapImplTest, AddRemoveAdd) { // First watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch1->updateWatchInterest(update_to); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, update_to); EXPECT_EQ(update_to, added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); - watch2->updateWatchInterest({"dummy"}); + watch_map.updateWatchInterest(watch2, {"dummy"}); // First watch receives update. expectDeltaAndSotwUpdate(callbacks1, {alice}, {}, "version1"); @@ -265,7 +264,7 @@ TEST(WatchMapImplTest, AddRemoveAdd) { } // First watch loses interest. { - AddedRemoved added_removed = watch1->updateWatchInterest({"dummy"}); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, {"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription @@ -275,7 +274,7 @@ TEST(WatchMapImplTest, AddRemoveAdd) { // Second watch becomes interested. { std::set update_to({"alice", "dummy"}); - AddedRemoved added_removed = watch2->updateWatchInterest(update_to); + AddedRemoved added_removed = watch_map.updateWatchInterest(watch2, update_to); EXPECT_EQ(std::set({"alice"}), added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); @@ -287,11 +286,11 @@ TEST(WatchMapImplTest, AddRemoveAdd) { } // Tests that nothing breaks if an update arrives that we entirely do not care about. -TEST(WatchMapImplTest, UninterestingUpdate) { +TEST(WatchMapTest, UninterestingUpdate) { NamedMockSubscriptionCallbacks callbacks; - WatchMapImpl watch_map; - WatchPtr watch = watch_map.addWatch(callbacks); - watch->updateWatchInterest({"alice"}); + WatchMap watch_map; + Watch* watch = watch_map.addWatch(callbacks); + watch_map.updateWatchInterest(watch, {"alice"}); Protobuf::RepeatedPtrField alice_update; envoy::api::v2::ClusterLoadAssignment alice; @@ -313,20 +312,23 @@ TEST(WatchMapImplTest, UninterestingUpdate) { doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version3"); // Clean removal of the watch: first update to "interested in nothing", then remove. - watch->updateWatchInterest({}); - watch.reset(); + watch_map.updateWatchInterest(watch, {}); + watch_map.removeWatch(watch); + + // Finally, test that calling onConfigUpdate on a map with no watches doesn't break. + doDeltaAndSotwUpdate(watch_map, bob_update, {}, "version4"); } // Tests that a watch that specifies no particular resource interest is treated as interested in // everything. -TEST(WatchMapImplTest, WatchingEverything) { +TEST(WatchMapTest, WatchingEverything) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; - WatchMapImpl watch_map; - WatchPtr watch1 = watch_map.addWatch(callbacks1); - WatchPtr watch2 = watch_map.addWatch(callbacks2); + WatchMap watch_map; + /*Watch* watch1 = */ watch_map.addWatch(callbacks1); + Watch* watch2 = watch_map.addWatch(callbacks2); // watch1 never specifies any names, and so is treated as interested in everything. - watch2->updateWatchInterest({"alice"}); + watch_map.updateWatchInterest(watch2, {"alice"}); Protobuf::RepeatedPtrField updated_resources; envoy::api::v2::ClusterLoadAssignment alice; @@ -352,17 +354,17 @@ TEST(WatchMapImplTest, WatchingEverything) { // exercise those cases. Also, the removal-only case tests that SotW does call a watch's // onConfigUpdate even if none of the watch's interested resources are among the updated resources. // (Which ensures we deliver empty config updates when a resource is dropped.) -TEST(WatchMapImplTest, DeltaOnConfigUpdate) { +TEST(WatchMapTest, DeltaOnConfigUpdate) { NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; NamedMockSubscriptionCallbacks callbacks3; - WatchMapImpl watch_map; - WatchPtr watch1 = watch_map.addWatch(callbacks1); - WatchPtr watch2 = watch_map.addWatch(callbacks2); - WatchPtr watch3 = watch_map.addWatch(callbacks3); - watch1->updateWatchInterest({"updated"}); - watch2->updateWatchInterest({"updated", "removed"}); - watch3->updateWatchInterest({"removed"}); + WatchMap watch_map; + Watch* watch1 = watch_map.addWatch(callbacks1); + Watch* watch2 = watch_map.addWatch(callbacks2); + Watch* watch3 = watch_map.addWatch(callbacks3); + watch_map.updateWatchInterest(watch1, {"updated"}); + watch_map.updateWatchInterest(watch2, {"updated", "removed"}); + watch_map.updateWatchInterest(watch3, {"removed"}); Protobuf::RepeatedPtrField update; envoy::api::v2::ClusterLoadAssignment updated; @@ -375,6 +377,20 @@ TEST(WatchMapImplTest, DeltaOnConfigUpdate) { doDeltaAndSotwUpdate(watch_map, update, {"removed"}, "version1"); } +TEST(WatchMapTest, OnConfigUpdateFailed) { + WatchMap watch_map; + watch_map.onConfigUpdateFailed(nullptr); // calling on empty map doesn't break + + NamedMockSubscriptionCallbacks callbacks1; + NamedMockSubscriptionCallbacks callbacks2; + watch_map.addWatch(callbacks1); + watch_map.addWatch(callbacks2); + + EXPECT_CALL(callbacks1, onConfigUpdateFailed(nullptr)); + EXPECT_CALL(callbacks2, onConfigUpdateFailed(nullptr)); + watch_map.onConfigUpdateFailed(nullptr); +} + } // namespace } // namespace Config } // namespace Envoy From 4dba42db58892ebd2dec2b7a9d8d70dc35c09e3e Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 7 Aug 2019 14:19:36 -0400 Subject: [PATCH 24/27] clang tidy Signed-off-by: Fred Douglas --- source/common/config/watch_map.h | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index e7f4d20a08ec9..57434f8f86388 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -73,18 +73,15 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable& resources, - const std::string& version_info) override; - virtual void - onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, - const Protobuf::RepeatedPtrField& removed_resources, - const std::string& system_version_info) override; - - virtual void onConfigUpdateFailed(const EnvoyException* e) override; - - virtual std::string resourceName(const ProtobufWkt::Any&) override { - NOT_IMPLEMENTED_GCOVR_EXCL_LINE; - } + void onConfigUpdate(const Protobuf::RepeatedPtrField& resources, + const std::string& version_info) override; + void onConfigUpdate(const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& system_version_info) override; + + void onConfigUpdateFailed(const EnvoyException* e) override; + + std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } WatchMap(const WatchMap&) = delete; WatchMap& operator=(const WatchMap&) = delete; From 8a4ae354c6806c1346e155a55e29833e0bf42d28 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 7 Aug 2019 14:52:56 -0400 Subject: [PATCH 25/27] merge conflict Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 4 ++-- source/common/config/watch_map.h | 2 +- test/common/config/watch_map_test.cc | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index e29455f3d4cdf..c7143bd990262 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -128,9 +128,9 @@ void WatchMap::onConfigUpdate( } } -void WatchMap::onConfigUpdateFailed(const EnvoyException* e) { +void WatchMap::onConfigUpdateFailed(ConfigUpdateFailureReason reason, const EnvoyException* e) { for (auto& watch : watches_) { - watch->callbacks_.onConfigUpdateFailed(e); + watch->callbacks_.onConfigUpdateFailed(reason, e); } } diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index 57434f8f86388..5e75e5e88dd76 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -79,7 +79,7 @@ class WatchMap : public SubscriptionCallbacks, public Logger::Loggable& removed_resources, const std::string& system_version_info) override; - void onConfigUpdateFailed(const EnvoyException* e) override; + void onConfigUpdateFailed(ConfigUpdateFailureReason reason, const EnvoyException* e) override; std::string resourceName(const ProtobufWkt::Any&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index b7ab15ac7e131..543298557fabf 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -379,16 +379,17 @@ TEST(WatchMapTest, DeltaOnConfigUpdate) { TEST(WatchMapTest, OnConfigUpdateFailed) { WatchMap watch_map; - watch_map.onConfigUpdateFailed(nullptr); // calling on empty map doesn't break + // calling on empty map doesn't break + watch_map.onConfigUpdateFailed(ConfigUpdateFailureReason::UpdateRejected, nullptr); NamedMockSubscriptionCallbacks callbacks1; NamedMockSubscriptionCallbacks callbacks2; watch_map.addWatch(callbacks1); watch_map.addWatch(callbacks2); - EXPECT_CALL(callbacks1, onConfigUpdateFailed(nullptr)); - EXPECT_CALL(callbacks2, onConfigUpdateFailed(nullptr)); - watch_map.onConfigUpdateFailed(nullptr); + EXPECT_CALL(callbacks1, onConfigUpdateFailed(ConfigUpdateFailureReason::UpdateRejected, nullptr)); + EXPECT_CALL(callbacks2, onConfigUpdateFailed(ConfigUpdateFailureReason::UpdateRejected, nullptr)); + watch_map.onConfigUpdateFailed(ConfigUpdateFailureReason::UpdateRejected, nullptr); } } // namespace From 064fe756f7c0d83d2184cd435d9d233fc1d7b190 Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 7 Aug 2019 14:58:50 -0400 Subject: [PATCH 26/27] merge conflict Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index c7143bd990262..f6e6943ef3001 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -141,7 +141,7 @@ std::set WatchMap::findAdditions(const std::vector& ne auto entry = watch_interest_.find(name); if (entry == watch_interest_.end()) { newly_added_to_subscription.insert(name); - watch_interest_[name] = {watch}; + watch_interest_[name] = watch; } else { entry->second.insert(watch); } From 3a1d2baa36bf10571de25ce91301a05f01fd7bfd Mon Sep 17 00:00:00 2001 From: Fred Douglas Date: Wed, 7 Aug 2019 16:13:21 -0400 Subject: [PATCH 27/27] snapshot Signed-off-by: Fred Douglas --- source/common/config/watch_map.cc | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index f6e6943ef3001..7182310091013 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -41,7 +41,6 @@ AddedRemoved WatchMap::updateWatchInterest(Watch* watch, } absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& resource_name) { - // Note that std::set_union needs sorted sets. Better to do it ourselves with insert(). absl::flat_hash_set ret = wildcard_watches_; auto watches_interested = watch_interest_.find(resource_name); if (watches_interested != watch_interest_.end()) { @@ -141,7 +140,7 @@ std::set WatchMap::findAdditions(const std::vector& ne auto entry = watch_interest_.find(name); if (entry == watch_interest_.end()) { newly_added_to_subscription.insert(name); - watch_interest_[name] = watch; + watch_interest_[name] = {watch}; } else { entry->second.insert(watch); }