From 719661cdf87bc6a1ce24f316fd387b4beed153a3 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 7 Jun 2021 16:17:18 +0200 Subject: [PATCH 01/49] docs: Add an explicit wildcard mode Signed-off-by: Krzesimir Nowak --- docs/root/api-docs/xds_protocol.rst | 29 ++++++++++++++++++--------- docs/root/version_history/current.rst | 1 + 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/root/api-docs/xds_protocol.rst b/docs/root/api-docs/xds_protocol.rst index 2f8873f7c7b4..74e4c8ee663d 100644 --- a/docs/root/api-docs/xds_protocol.rst +++ b/docs/root/api-docs/xds_protocol.rst @@ -427,13 +427,17 @@ names becomes empty, that means that the client is no longer interested in any r specified type. For :ref:`Listener ` and :ref:`Cluster ` resource -types, there is also a "wildcard" mode, which is triggered when the initial request on the stream -for that resource type contains no resource names. In this case, the server should use -site-specific business logic to determine the full set of resources that the client is interested -in, typically based on the client's :ref:`node ` identification. Note -that once a stream has entered wildcard mode for a given resource type, there is no way to change -the stream out of wildcard mode; resource names specified in any subsequent request on the stream -will be ignored. +types, there is also a "wildcard" mode, which comes in two equivalent flavors. First flavor, +implicit, is triggered when the initial request on the stream for that resource type contains no +resource names. Second flavor, explicit, is triggered when a request (not necessarily an initial one +on the stream) for that resource type contains (among other names) a special name "*". Opting in +into the wildcard subscription means adding the wildcard subscription into the subscription state. +For wildcard requests, the server should use site-specific business logic to determine the full set +of resources that the client is interested in, typically based on the client's :ref:`node ` identification. +The client can opt out from the wildcard mode by unsubscribing from the "*" resource name. Note that +subsequently opting back into the wildcard mode can only be done with a request that adds the "*" +resource name to the subscription state. Additional resource names added to the subscription state +while being opted-in into the wildcard mode should not be ignored by the server. Client Behavior """"""""""""""" @@ -535,6 +539,8 @@ being requested by the client, and if one of those resources springs into existe server must send an update to the client informing it of the new resource. Clients that initially see a resource that does not exist must be prepared for the resource to be created at any time. +.. _xds_protocol_unsubscribing_from_resources: + Unsubscribing From Resources ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -553,7 +559,10 @@ Note that for :ref:`Listener ` and resource types where the stream is in "wildcard" mode (see :ref:`How the client specifies what resources to return ` for details), the set of resources being subscribed to is determined by the server instead of the client, so there is no mechanism -for the client to unsubscribe from resources. +for the client to unsubscribe from resources. The only resources that the client could unsubscribe +from are the resources that the client explicitly expressed the interest in before. Note that +the server may still send the resource to the client if the resource was also a part of the set +of resources determined by the server from the wildcard subscription. Requesting Multiple Resources on a Single Stream ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -779,8 +788,8 @@ resources to avoid resending them over the network by sending them in :ref:`initial_resource_versions `. Because no state is assumed to be preserved from the previous stream, the reconnecting client must provide the server with all resource names it is interested in. Note that for wildcard -requests (CDS/LDS/SRDS), the request must have no resources in both -:ref:`resource_names_subscribe ` and +requests (CDS/LDS/SRDS), the request must have only resources the client was explicitly interested in in +:ref:`resource_names_subscribe ` and no resources in :ref:`resource_names_unsubscribe `. .. figure:: diagrams/incremental-reconnect.svg diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 2d880ad41d48..e4f060e35198 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -8,6 +8,7 @@ Incompatible Behavior Changes * grpc_bridge_filter: the filter no longer collects grpc stats in favor of the existing grpc stats filter. The behavior can be reverted by changing runtime key ``envoy.reloadable_features.grpc_bridge_stats_disabled``. * tracing: update Apache SkyWalking tracer version to be compatible with 8.4.0 data collect protocol. This change will introduce incompatibility with SkyWalking 8.3.0. +* xds: added the explicit wildcard mode, which allows subscribing to specific resource names on top of a wildcard subscription. This means that the resource name ``*`` is reserved. This may mean that in some cases the initial wildcard subscription request on the stream will not be empty, but will have a non-empty list of resources with a special name among them. See :ref:`wildcard mode description ` and :ref:`unsubscribing from resources ` for details. Minor Behavior Changes ---------------------- From 9b84dfd0ec5b51bac39c339a74b6b6a239ceb966 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 7 Jun 2021 16:19:18 +0200 Subject: [PATCH 02/49] config: Implement explicit wildcard mode in delta gRPC Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 83 ++++++++++++++----- .../common/config/delta_subscription_state.h | 57 ++++++++++--- source/common/config/new_grpc_mux_impl.cc | 5 +- source/common/config/watch_map.cc | 3 +- 4 files changed, 112 insertions(+), 36 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index b1b162d3f327..d279bbdfbffd 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -22,21 +22,26 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, [this](const auto& expired) { Protobuf::RepeatedPtrField removed_resources; for (const auto& resource : expired) { - setResourceWaitingForServer(resource); - removed_resources.Add(std::string(resource)); + if (auto maybe_resource = getResourceState(resource); maybe_resource.has_value()) { + maybe_resource->setAsWaitingForServer(); + removed_resources.Add(std::string(resource)); + } } watch_map_.onConfigUpdate({}, removed_resources, ""); }, dispatcher, dispatcher.timeSource()), - type_url_(std::move(type_url)), wildcard_(wildcard), watch_map_(watch_map), + type_url_(std::move(type_url)), + mode_(wildcard ? WildcardMode::Implicit : WildcardMode::Disabled), watch_map_(watch_map), local_info_(local_info), dispatcher_(dispatcher) {} void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { - setResourceWaitingForServer(a); + // This adds a resource state that is waiting for the server for + // more information. + resource_state_[resource_name] = ResourceState(ResourceType::ExplicitlyRequested); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -53,6 +58,31 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.erase(r); names_removed_.insert(r); } + switch (mode_) { + case WildcardMode::Implicit: + if (names_removed_.find("*") != names_removed_.end()) { + // we explicitly cancel the wildcard subscription + mode_ = WildcardMode::Disabled; + } else if (!names_added_.empty()) { + // switch to explicit mode if we requested some extra names + mode_ = WildcardMode::Explicit; + } + break; + + case WildcardMode::Explicit: + if (names_removed_.find("*") != names_removed_.end()) { + // we explicitly cancel the wildcard subscription + mode_ = WildcardMode::Disabled; + } + break; + + case WildcardMode::Disabled: + if (names_added_.find("*") != names_added_.end()) { + // we switch into an explicit wildcard subscription + mode_ = WildcardMode::Explicit; + } + break; + } } // Not having sent any requests yet counts as an "update pending" since you're supposed to resend @@ -124,7 +154,7 @@ void DeltaSubscriptionState::handleGoodResponse( { const auto scoped_update = ttl_.scopedTtlUpdate(); for (const auto& resource : message.resources()) { - addResourceState(resource); + addResourceStateFromServer(resource); } } @@ -140,8 +170,8 @@ void DeltaSubscriptionState::handleGoodResponse( // initial_resource_versions messages, but will remind us to explicitly tell the server "I'm // cancelling my subscription" when we lose interest. for (const auto& resource_name : message.removed_resources()) { - if (resource_names_.find(resource_name) != resource_names_.end()) { - setResourceWaitingForServer(resource_name); + if (auto maybe_resource = getResourceState(resource_name); maybe_resource.has_value()) { + maybe_resource->setAsWaitingForServer(); } } ENVOY_LOG(debug, "Delta config for {} accepted with {} resources added, {} removed", type_url_, @@ -177,16 +207,20 @@ DeltaSubscriptionState::getNextRequestAckless() { if (!resource_state.waitingForServer()) { (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); } - // As mentioned above, fill resource_names_subscribe with everything, including names we - // have yet to receive any resource for unless this is a wildcard subscription, for which - // the first request on a stream must be without any resource names. - if (!wildcard_) { + // Add resource names to resource_names_subscribe only if this is not a wildcard subscription + // request or if we requested this resource explicitly (so we are actually in explicit + // wildcard mode). + if (mode_ == WildcardMode::Disabled || + resource_state.type() == ResourceType::ExplicitlyRequested) { names_added_.insert(resource_name); } } - // Wildcard subscription initial requests must have no resource_names_subscribe. - if (wildcard_) { - names_added_.clear(); + // We are not clearing the names_added_ set. If we are in implicit wildcard subscription mode, + // then the set should already be empty. If we are in explicit wildcard mode then the set will + // contain the names we explicitly requested, but we need to add * to the list to make sure it's + // sent too. + if (mode_ == WildcardMode::Explicit) { + names_added_.insert("*"); } names_removed_.clear(); } @@ -213,7 +247,7 @@ DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { return request; } -void DeltaSubscriptionState::addResourceState( +void DeltaSubscriptionState::addResourceStateFromServer( const envoy::service::discovery::v3::Resource& resource) { if (resource.has_ttl()) { ttl_.add(std::chrono::milliseconds(DurationUtil::durationToMilliseconds(resource.ttl())), @@ -222,18 +256,25 @@ void DeltaSubscriptionState::addResourceState( ttl_.clear(resource.name()); } - resource_state_[resource.name()] = ResourceState(resource); - resource_names_.insert(resource.name()); + if (auto it = resource_state_.find(resource.name()); it != resource_state_.end()) { + auto old_type = it->second.type(); + it->second = ResourceState(resource, old_type); + } else { + resource_state_.insert( + {resource.name(), ResourceState(resource, ResourceType::ReceivedFromServer)}); + } } -void DeltaSubscriptionState::setResourceWaitingForServer(const std::string& resource_name) { - resource_state_[resource_name] = ResourceState(); - resource_names_.insert(resource_name); +OptRef DeltaSubscriptionState::getResourceState(const std::string& resource_name) { + auto itr = resource_state_.find(resource_name); + if (itr == resource_state_.end()) { + return {}; + } + return {itr->second}; } void DeltaSubscriptionState::removeResourceState(const std::string& resource_name) { resource_state_.erase(resource_name); - resource_names_.erase(resource_name); } } // namespace Config diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index cafdbe0022ee..ef91509f3c7f 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -59,18 +59,43 @@ class DeltaSubscriptionState : public Logger::Loggable { void handleGoodResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); void handleBadResponse(const EnvoyException& e, UpdateAck& ack); + // This enumeration describes the resource type, which is only relevant for wildcard + // subscriptions. Depending on its type, the resource will or will not be resent on the initial + // wildcard subscription. + enum class ResourceType { + // Explicitly requested resource type means that we have asked about the resource by updating + // the subscription interest. Such resources are resent on the initial wildcard request. + ExplicitlyRequested, + // Received from server resources are resources that the state knows about only from the server + // response. Such resources are not resent on the initial wildcard request. + ReceivedFromServer, + }; + + // Determines the effective resource type. Explicitly requested type overrides the received from + // server type. + ResourceType effectiveResourceType(ResourceType old_type, ResourceType new_type) { + return (old_type == ResourceType::ReceivedFromServer) ? new_type : old_type; + } + class ResourceState { public: - ResourceState(const envoy::service::discovery::v3::Resource& resource) - : version_(resource.version()) {} + ResourceState(absl::optional version, ResourceType type) + : version_(std::move(version)), type_(type) {} + + ResourceState(const envoy::service::discovery::v3::Resource& resource, ResourceType type) + : ResourceState(resource.version(), type) {} // Builds a ResourceState in the waitingForServer state. - ResourceState() = default; + ResourceState(ResourceType type) : ResourceState(absl::nullopt, type) {} + + ResourceType type() const { return type_; } // If true, we currently have no version of this resource - we are waiting for the server to // provide us with one. bool waitingForServer() const { return version_ == absl::nullopt; } + void setAsWaitingForServer() { version_ = absl::nullopt; } + // Must not be called if waitingForServer() == true. std::string version() const { ASSERT(version_.has_value()); @@ -79,14 +104,24 @@ class DeltaSubscriptionState : public Logger::Loggable { private: absl::optional version_; + ResourceType type_; }; - // Use these helpers to ensure resource_state_ and resource_names_ get updated together. - void addResourceState(const envoy::service::discovery::v3::Resource& resource); - void setResourceWaitingForServer(const std::string& resource_name); - void removeResourceState(const std::string& resource_name); + // Describes the wildcard mode the subscription is in. + enum class WildcardMode { + // This mode is being expressed by sending a wildcard subscription request with an empty + // resource subscription list. + Implicit, + // This mode is being expressed by sending a wildcard subscription request that contains "*" + // special name in the resource subscription list. + Explicit, + // This mode is means no wildcard subscription. + Disabled, + }; - void populateDiscoveryRequest(envoy::service::discovery::v3::DeltaDiscoveryResponse& request); + void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); + OptRef getResourceState(const std::string& resource_name); + void removeResourceState(const std::string& resource_name); // A map from resource name to per-resource version. The keys of this map are exactly the resource // names we are currently interested in. Those in the waitingForServer state currently don't have @@ -99,13 +134,9 @@ class DeltaSubscriptionState : public Logger::Loggable { // disable heartbeats for these resources (currently only VHDS). const bool supports_heartbeats_; TtlManager ttl_; - // The keys of resource_versions_. Only tracked separately because std::map does not provide an - // iterator into just its keys. - absl::flat_hash_set resource_names_; const std::string type_url_; - // Is the subscription is for a wildcard request. - const bool wildcard_; + WildcardMode mode_; UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; Event::Dispatcher& dispatcher_; diff --git a/source/common/config/new_grpc_mux_impl.cc b/source/common/config/new_grpc_mux_impl.cc index 07b55626b83a..bb492ea56af1 100644 --- a/source/common/config/new_grpc_mux_impl.cc +++ b/source/common/config/new_grpc_mux_impl.cc @@ -128,7 +128,10 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, auto entry = subscriptions_.find(type_url); if (entry == subscriptions_.end()) { // We don't yet have a subscription for type_url! Make one! - addSubscription(type_url, options.use_namespace_matching_, resources.empty()); + // No resources or an existence of the special name implies that + // this is a wildcard request subscription. + const bool wildcard = resources.empty() || (resources.find("*") != resources.end()); + addSubscription(type_url, options.use_namespace_matching_, wildcard); return addWatch(type_url, resources, callbacks, resource_decoder, options); } diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 992fdc35393c..b2942d438b32 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -49,7 +49,8 @@ void WatchMap::removeDeferredWatches() { AddedRemoved WatchMap::updateWatchInterest(Watch* watch, const absl::flat_hash_set& update_to_these_names) { - if (update_to_these_names.empty()) { + if (update_to_these_names.empty() || + update_to_these_names.find("*") != update_to_these_names.end()) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); From a5f4ed737dd1bf324e7d4a70e6012a2a23e7eff4 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 7 Jun 2021 16:19:35 +0200 Subject: [PATCH 03/49] test: Add tests for explicit wildcar mode in delta gRPC Signed-off-by: Krzesimir Nowak --- .../config/delta_subscription_state_test.cc | 176 ++++++++++++++++-- 1 file changed, 165 insertions(+), 11 deletions(-) diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index fea85b717740..532e172f0827 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -346,14 +346,73 @@ TEST_F(DeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect) { EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } -// For wildcard subscription, upon a reconnection, the server is supposed to assume a -// blank slate for the Envoy's state (hence the need for initial_resource_versions), and -// the resource_names_subscribe and resource_names_unsubscribe must be empty (as is expected -// of every wildcard first message). This is true even if in between the last request of the -// last stream and the first request of the new stream, Envoy gained or lost interest in a -// resource. The subscription & unsubscription implicitly takes effect by simply requesting a -// wildcard subscription in the newly reconnected stream. -TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect) { +// Check that switching into wildcard subscription after initial +// request switches us into the explicit wildcard mode. +TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + // We call deliverDiscoveryResponse twice in this test. + EXPECT_CALL(*timer_, disableTimer()).Times(2); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + + // switch into wildcard mode + state_.updateSubscriptionInterest({"name4", "*"}, {"name1"}); + state_.markStreamFresh(); // simulate a stream reconnection + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: yes do include: we are explicitly interested (from test's base constructor) + // name3: yes do include: we are explicitly interested (from test's base constructor) + // name4: yes do include: we are explicitly interested + // *: explicit wildcard subscription + EXPECT_THAT(cur_request.resource_names_subscribe(), + UnorderedElementsAre("name2", "name3", "name4", "*")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); + + Protobuf::RepeatedPtrField add4_5 = + populateRepeatedResource({{"name4", "version4A"}, {"name5", "version5A"}}); + deliverDiscoveryResponse(add4_5, {}, "debugversion1"); + + state_.markStreamFresh(); // simulate a stream reconnection + cur_request = state_.getNextRequestAckless(); + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: yes do include: we are explicitly interested (from test's base constructor) + // name3: yes do include: we are explicitly interested (from test's base constructor) + // name4: yes do include: we are explicitly interested + // name5: do not include: we are implicitly interested, so this resource should not appear on the + // initial request + // *: explicit wildcard subscription + EXPECT_THAT(cur_request.resource_names_subscribe(), + UnorderedElementsAre("name2", "name3", "name4", "*")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + +// For wildcard subscription, upon a reconnection, the server is supposed to assume a blank slate +// for the Envoy's state (hence the need for initial_resource_versions), and the +// resource_names_subscribe and resource_names_unsubscribe must be empty if we haven't gained any +// new explicit interest in a resource. In such case, the client should send an empty request. +TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnectImplicit) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + + state_.markStreamFresh(); // simulate a stream reconnection + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. + EXPECT_TRUE(cur_request.resource_names_subscribe().empty()); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + +// For wildcard subscription, upon a reconnection, the server is supposed to assume a blank slate +// for the Envoy's state (hence the need for initial_resource_versions). The +// resource_names_unsubscribe must be empty (as is expected of every wildcard first message). The +// resource_names_subscribe should contain all the resources we are explicitly interested in and a +// special resource denoting a wildcard subscription. +TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnectExplicit) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); EXPECT_CALL(*timer_, disableTimer()); @@ -364,11 +423,106 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); // Regarding the resource_names_subscribe field: // name1: do not include: we lost interest. - // name2: do not include: we are interested, but for wildcard it shouldn't be provided. - // name4: do not include: although we are newly interested, an initial wildcard request - // must be with no resources. + // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. + // name3: yes do include: we are explicitly interested. + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*", "name3")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + +// Check the contents of the requests after cancelling the wildcard +// subscription and then reconnection. The second request should look +// like a non-wildcard request, so mention all the known resources in +// the initial request. +TEST_F(WildcardDeltaSubscriptionStateTest, CancellingImplicitWildcardSubscription) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + + state_.updateSubscriptionInterest({"name3"}, {"name1", "*"}); + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); + EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", "*")); + state_.markStreamFresh(); // simulate a stream reconnection + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: yes do include: we are interested, and it's not wildcard. + // name3: yes do include: we are interested, and it's not wildcard. + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name2", "name3")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + +// Check the contents of the requests after cancelling the wildcard +// subscription and then reconnection. The second request should look +// like a non-wildcard request, so mention all the known resources in +// the initial request. +TEST_F(WildcardDeltaSubscriptionStateTest, CancellingExplicitWildcardSubscription) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + // switch to explicit wildcard subscription + state_.updateSubscriptionInterest({"name3"}, {}); + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); + + // cancel wildcard subscription + state_.updateSubscriptionInterest({"name4"}, {"name1", "*"}); + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name4")); + EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", "*")); + state_.markStreamFresh(); // simulate a stream reconnection + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: yes do include: we are interested, and it's not wildcard. + // name3: yes do include: we are interested, and it's not wildcard. + // name4: yes do include: we are interested, and it's not wildcard. + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), + UnorderedElementsAre("name2", "name3", "name4")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + +// Check that resource changes from being interested in implicitly to explicitly when we update the +// subscription interest. Such resources will show up in the initial wildcard requests +// too. Receiving the update on such resource will not change their interest mode. +TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { + Protobuf::RepeatedPtrField add1_2_a = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*timer_, disableTimer()).Times(2); + deliverDiscoveryResponse(add1_2_a, {}, "debugversion1"); + + // verify that neither name1 nor name2 appears in the initial request (they are of implicit + // interest and initial wildcard request should not contain those). + state_.markStreamFresh(); // simulate a stream reconnection + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_TRUE(cur_request.resource_names_subscribe().empty()); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); + + // express the interest in name1 explicitly and verify that the follow-up request will contain it + // (this also switches the wildcard mode to explicit, but we won't see * in resource names, + // because we already are in wildcard mode). + state_.updateSubscriptionInterest({"name1"}, {}); + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); + + // verify that name1 and * appear in the initial request (name1 is of explicit interest and we are + // in explicit wildcard mode). + state_.markStreamFresh(); // simulate a stream reconnection + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", "*")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); + + // verify that getting an update on name1 will keep name1 in the explicit interest mode + Protobuf::RepeatedPtrField add1_2_b = + populateRepeatedResource({{"name1", "version1B"}, {"name2", "version2B"}}); + deliverDiscoveryResponse(add1_2_b, {}, "debugversion1"); + state_.markStreamFresh(); // simulate a stream reconnection + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", "*")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } // initial_resource_versions should not be present on messages after the first in a stream. From d78ec973f9400a51563b2ed635c4844dac923ed9 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 7 Jun 2021 21:36:23 +0200 Subject: [PATCH 04/49] docs: Reword wildcard mode docs Specify that sending an empty initial request as means for opting in into the wildcard subscription is a special case of sending the wildcard resource name. Signed-off-by: Krzesimir Nowak --- docs/root/api-docs/xds_protocol.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/root/api-docs/xds_protocol.rst b/docs/root/api-docs/xds_protocol.rst index 74e4c8ee663d..caf6799bb469 100644 --- a/docs/root/api-docs/xds_protocol.rst +++ b/docs/root/api-docs/xds_protocol.rst @@ -427,17 +427,14 @@ names becomes empty, that means that the client is no longer interested in any r specified type. For :ref:`Listener ` and :ref:`Cluster ` resource -types, there is also a "wildcard" mode, which comes in two equivalent flavors. First flavor, -implicit, is triggered when the initial request on the stream for that resource type contains no -resource names. Second flavor, explicit, is triggered when a request (not necessarily an initial one -on the stream) for that resource type contains (among other names) a special name "*". Opting in -into the wildcard subscription means adding the wildcard subscription into the subscription state. -For wildcard requests, the server should use site-specific business logic to determine the full set -of resources that the client is interested in, typically based on the client's :ref:`node ` identification. -The client can opt out from the wildcard mode by unsubscribing from the "*" resource name. Note that -subsequently opting back into the wildcard mode can only be done with a request that adds the "*" -resource name to the subscription state. Additional resource names added to the subscription state -while being opted-in into the wildcard mode should not be ignored by the server. +types, there is also a "wildcard" mode, which is triggered by sending a request containing a special +resource name "*" in a list of resource names to subscribe to. As a special case, sending an initial +request with no resource names at all also triggers the wildcard mode. For wildcard requests, +the server should use site-specific business logic to determine the full set of resources that +the client is interested in, typically based on the client's :ref:`node ` identification. +The client can opt out from the wildcard mode by unsubscribing from the "*" resource name. +Additional resource names added to the subscription state while being opted-in into the wildcard +mode should not be ignored by the server. Client Behavior """"""""""""""" From 2e8ba91f09e355bfb8903539ebbd1d46758e29db Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 7 Jun 2021 23:14:31 +0200 Subject: [PATCH 05/49] config: Fix build Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index d279bbdfbffd..32efbb1b9af1 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -41,7 +41,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( for (const auto& a : cur_added) { // This adds a resource state that is waiting for the server for // more information. - resource_state_[resource_name] = ResourceState(ResourceType::ExplicitlyRequested); + resource_state_[a] = ResourceState(ResourceType::ExplicitlyRequested); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -265,7 +265,8 @@ void DeltaSubscriptionState::addResourceStateFromServer( } } -OptRef DeltaSubscriptionState::getResourceState(const std::string& resource_name) { +OptRef +DeltaSubscriptionState::getResourceState(const std::string& resource_name) { auto itr = resource_state_.find(resource_name); if (itr == resource_state_.end()) { return {}; From f4ca629c58231b3d2b086e50e0c6d881ec7b5578 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 8 Jun 2021 09:22:59 +0200 Subject: [PATCH 06/49] config: Fix build Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 32efbb1b9af1..ef754670daed 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -41,7 +41,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( for (const auto& a : cur_added) { // This adds a resource state that is waiting for the server for // more information. - resource_state_[a] = ResourceState(ResourceType::ExplicitlyRequested); + resource_state_.insert_or_assign(a, ResourceType::ExplicitlyRequested); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. From 3fa6a37bc4c76d2c3a3ba8dd02618f0a4df9dc7e Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 2 Jul 2021 18:12:21 +0200 Subject: [PATCH 07/49] Drop the entry in version history Signed-off-by: Krzesimir Nowak --- docs/root/version_history/current.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index f995154483ca..a62f14ef5d7a 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -8,7 +8,6 @@ Incompatible Behavior Changes * grpc_bridge_filter: the filter no longer collects grpc stats in favor of the existing grpc stats filter. The behavior can be reverted by changing runtime key ``envoy.reloadable_features.grpc_bridge_stats_disabled``. * tracing: update Apache SkyWalking tracer version to be compatible with 8.4.0 data collect protocol. This change will introduce incompatibility with SkyWalking 8.3.0. -* xds: added the explicit wildcard mode, which allows subscribing to specific resource names on top of a wildcard subscription. This means that the resource name ``*`` is reserved. This may mean that in some cases the initial wildcard subscription request on the stream will not be empty, but will have a non-empty list of resources with a special name among them. See :ref:`wildcard mode description ` and :ref:`unsubscribing from resources ` for details. Minor Behavior Changes ---------------------- From 391461597f66bd5c1224285e4033c21b33e9abb2 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 6 Jul 2021 17:02:17 +0200 Subject: [PATCH 08/49] Always send an asterisk as a wildcard subscription Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 45 +++++++------------ .../common/config/delta_subscription_state.h | 23 +--------- source/common/config/new_grpc_mux_impl.cc | 18 ++++---- source/common/config/new_grpc_mux_impl.h | 8 ++-- source/common/config/watch_map.cc | 3 +- .../config/delta_subscription_state_test.cc | 15 ++++--- test/common/config/new_grpc_mux_impl_test.cc | 17 ++++--- 7 files changed, 48 insertions(+), 81 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index ef754670daed..716dd650e5cd 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -14,7 +14,7 @@ namespace Config { DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, - Event::Dispatcher& dispatcher, const bool wildcard) + Event::Dispatcher& dispatcher) // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on // empty resources as updates. : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), @@ -31,9 +31,8 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, watch_map_.onConfigUpdate({}, removed_resources, ""); }, dispatcher, dispatcher.timeSource()), - type_url_(std::move(type_url)), - mode_(wildcard ? WildcardMode::Implicit : WildcardMode::Disabled), watch_map_(watch_map), - local_info_(local_info), dispatcher_(dispatcher) {} + type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), + dispatcher_(dispatcher) {} void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, @@ -58,30 +57,16 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.erase(r); names_removed_.insert(r); } - switch (mode_) { - case WildcardMode::Implicit: - if (names_removed_.find("*") != names_removed_.end()) { - // we explicitly cancel the wildcard subscription - mode_ = WildcardMode::Disabled; - } else if (!names_added_.empty()) { - // switch to explicit mode if we requested some extra names - mode_ = WildcardMode::Explicit; - } - break; - - case WildcardMode::Explicit: - if (names_removed_.find("*") != names_removed_.end()) { - // we explicitly cancel the wildcard subscription - mode_ = WildcardMode::Disabled; - } - break; - - case WildcardMode::Disabled: - if (names_added_.find("*") != names_added_.end()) { - // we switch into an explicit wildcard subscription - mode_ = WildcardMode::Explicit; - } - break; + // Handle the special case of an empty initial resources list as making a wildcard subscription. + if (!any_request_sent_yet_in_current_stream_ && cur_added.empty() && cur_removed.empty()) { + names_removed_.erase("*"); + names_added_.insert("*"); + } + if (names_added_.find("*") != names_added_.end()) { + has_wildcard_subscription_ = true; + } + if (names_removed_.find("*") != names_removed_.end()) { + has_wildcard_subscription_ = false; } } @@ -210,7 +195,7 @@ DeltaSubscriptionState::getNextRequestAckless() { // Add resource names to resource_names_subscribe only if this is not a wildcard subscription // request or if we requested this resource explicitly (so we are actually in explicit // wildcard mode). - if (mode_ == WildcardMode::Disabled || + if (!has_wildcard_subscription_ || resource_state.type() == ResourceType::ExplicitlyRequested) { names_added_.insert(resource_name); } @@ -219,7 +204,7 @@ DeltaSubscriptionState::getNextRequestAckless() { // then the set should already be empty. If we are in explicit wildcard mode then the set will // contain the names we explicitly requested, but we need to add * to the list to make sure it's // sent too. - if (mode_ == WildcardMode::Explicit) { + if (has_wildcard_subscription_) { names_added_.insert("*"); } names_removed_.clear(); diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index ef91509f3c7f..ed73c54e3d82 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -26,8 +26,7 @@ namespace Config { class DeltaSubscriptionState : public Logger::Loggable { public: DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, - const bool wildcard); + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); // Update which resources we're interested in subscribing to. void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, @@ -71,12 +70,6 @@ class DeltaSubscriptionState : public Logger::Loggable { ReceivedFromServer, }; - // Determines the effective resource type. Explicitly requested type overrides the received from - // server type. - ResourceType effectiveResourceType(ResourceType old_type, ResourceType new_type) { - return (old_type == ResourceType::ReceivedFromServer) ? new_type : old_type; - } - class ResourceState { public: ResourceState(absl::optional version, ResourceType type) @@ -107,18 +100,6 @@ class DeltaSubscriptionState : public Logger::Loggable { ResourceType type_; }; - // Describes the wildcard mode the subscription is in. - enum class WildcardMode { - // This mode is being expressed by sending a wildcard subscription request with an empty - // resource subscription list. - Implicit, - // This mode is being expressed by sending a wildcard subscription request that contains "*" - // special name in the resource subscription list. - Explicit, - // This mode is means no wildcard subscription. - Disabled, - }; - void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); OptRef getResourceState(const std::string& resource_name); void removeResourceState(const std::string& resource_name); @@ -136,7 +117,7 @@ class DeltaSubscriptionState : public Logger::Loggable { TtlManager ttl_; const std::string type_url_; - WildcardMode mode_; + bool has_wildcard_subscription_ = false; UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; Event::Dispatcher& dispatcher_; diff --git a/source/common/config/new_grpc_mux_impl.cc b/source/common/config/new_grpc_mux_impl.cc index 2a28d7949ca8..e4ef58403b4b 100644 --- a/source/common/config/new_grpc_mux_impl.cc +++ b/source/common/config/new_grpc_mux_impl.cc @@ -135,11 +135,10 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, auto entry = subscriptions_.find(type_url); if (entry == subscriptions_.end()) { // We don't yet have a subscription for type_url! Make one! - // No resources or an existence of the special name implies that - // this is a wildcard request subscription. - const bool wildcard = resources.empty() || (resources.find("*") != resources.end()); - addSubscription(type_url, options.use_namespace_matching_, wildcard); - return addWatch(type_url, resources, callbacks, resource_decoder, options); + addSubscription(type_url, options.use_namespace_matching_); + const absl::flat_hash_set wildcard_resource{"*"}; + const auto& effective_resources = (resources.empty() ? wildcard_resource : resources); + return addWatch(type_url, effective_resources, callbacks, resource_decoder, options); } Watch* watch = entry->second->watch_map_.addWatch(callbacks, resource_decoder); @@ -210,11 +209,10 @@ void NewGrpcMuxImpl::removeWatch(const std::string& type_url, Watch* watch) { entry->second->watch_map_.removeWatch(watch); } -void NewGrpcMuxImpl::addSubscription(const std::string& type_url, const bool use_namespace_matching, - const bool wildcard) { - subscriptions_.emplace(type_url, std::make_unique(type_url, local_info_, - use_namespace_matching, - dispatcher_, wildcard)); +void NewGrpcMuxImpl::addSubscription(const std::string& type_url, + const bool use_namespace_matching) { + subscriptions_.emplace(type_url, std::make_unique( + type_url, local_info_, use_namespace_matching, dispatcher_)); subscription_ordering_.emplace_back(type_url); } diff --git a/source/common/config/new_grpc_mux_impl.h b/source/common/config/new_grpc_mux_impl.h index 4c2246fed813..44dd14a73c2d 100644 --- a/source/common/config/new_grpc_mux_impl.h +++ b/source/common/config/new_grpc_mux_impl.h @@ -73,10 +73,9 @@ class NewGrpcMuxImpl struct SubscriptionStuff { SubscriptionStuff(const std::string& type_url, const LocalInfo::LocalInfo& local_info, - const bool use_namespace_matching, Event::Dispatcher& dispatcher, - const bool wildcard) + const bool use_namespace_matching, Event::Dispatcher& dispatcher) : watch_map_(use_namespace_matching), - sub_state_(type_url, watch_map_, local_info, dispatcher, wildcard) {} + sub_state_(type_url, watch_map_, local_info, dispatcher) {} WatchMap watch_map_; DeltaSubscriptionState sub_state_; @@ -130,8 +129,7 @@ class NewGrpcMuxImpl const SubscriptionOptions& options); // Adds a subscription for the type_url to the subscriptions map and order list. - void addSubscription(const std::string& type_url, bool use_namespace_matching, - const bool wildcard); + void addSubscription(const std::string& type_url, bool use_namespace_matching); void trySendDiscoveryRequests(); diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index b2942d438b32..016523ddcd63 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -49,8 +49,7 @@ void WatchMap::removeDeferredWatches() { AddedRemoved WatchMap::updateWatchInterest(Watch* watch, const absl::flat_hash_set& update_to_these_names) { - if (update_to_these_names.empty() || - update_to_these_names.find("*") != update_to_these_names.end()) { + if (update_to_these_names.contains("*")) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 532e172f0827..a8f6b7027abf 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -30,10 +30,10 @@ const char TypeUrl[] = "type.googleapis.com/envoy.api.v2.Cluster"; class DeltaSubscriptionStateTestBase : public testing::Test { protected: DeltaSubscriptionStateTestBase( - const std::string& type_url, const bool wildcard, + const std::string& type_url, const absl::flat_hash_set initial_resources = {"name1", "name2", "name3"}) : timer_(new Event::MockTimer(&dispatcher_)), - state_(type_url, callbacks_, local_info_, dispatcher_, wildcard) { + state_(type_url, callbacks_, local_info_, dispatcher_) { state_.updateSubscriptionInterest(initial_resources, {}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); @@ -98,13 +98,13 @@ populateRepeatedResource(std::vector> items) class DeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, false) {} + DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl) {} }; // Delta subscription state of a wildcard subscription request. class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, true, {}) {} + WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, {"*"}) {} }; // Basic gaining/losing interest in resources should lead to subscription updates. @@ -401,9 +401,10 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); // Regarding the resource_names_subscribe field: + // *: include, it's a wildcard subscription // name1: do not include: we lost interest. // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. - EXPECT_TRUE(cur_request.resource_names_subscribe().empty()); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -497,7 +498,7 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { // interest and initial wildcard request should not contain those). state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); - EXPECT_TRUE(cur_request.resource_names_subscribe().empty()); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); // express the interest in name1 explicitly and verify that the follow-up request will contain it @@ -673,7 +674,7 @@ TEST_F(DeltaSubscriptionStateTest, ResourceTTL) { class VhdsDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: VhdsDeltaSubscriptionStateTest() - : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost", false) {} + : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost") {} }; TEST_F(VhdsDeltaSubscriptionStateTest, ResourceTTL) { diff --git a/test/common/config/new_grpc_mux_impl_test.cc b/test/common/config/new_grpc_mux_impl_test.cc index ce917b39a0fc..43420c82e9fb 100644 --- a/test/common/config/new_grpc_mux_impl_test.cc +++ b/test/common/config/new_grpc_mux_impl_test.cc @@ -113,10 +113,13 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { setup(); InSequence s; auto foo_sub = grpc_mux_->addWatch("foo", {"x", "y"}, callbacks_, resource_decoder_, {}); + // Empty list of subscribed names means a wildcard subscription, so + // we will expect an asterisk in sent message. auto bar_sub = grpc_mux_->addWatch("bar", {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage("foo", {"x", "y"}, {}); - expectSendMessage("bar", {}, {}); + // This is a wildcard subscription, thus expect the wildcard symbol. + expectSendMessage("bar", {"*"}, {}); grpc_mux_->start(); // Unknown type, shouldn't do anything. local_info_.context_provider_.update_cb_handler_.runCallbacks("baz"); @@ -126,6 +129,7 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { // Update to bar type should resend Node. expectSendMessage("bar", {}, {}); local_info_.context_provider_.update_cb_handler_.runCallbacks("bar"); + expectSendMessage("bar", {}, {"*"}); expectSendMessage("foo", {}, {"x", "y"}); } @@ -195,7 +199,7 @@ TEST_F(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { auto foo_sub = grpc_mux_->addWatch(type_url, {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // Send a wildcard request on new connection. - expectSendMessage(type_url, {}, {}); + expectSendMessage(type_url, {"*"}, {}); grpc_mux_->start(); // An helper function to create a response with a single load_assignment resource @@ -254,12 +258,13 @@ TEST_F(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { EXPECT_CALL(*grpc_stream_retry_timer, enableTimer(_, _)) .WillOnce(Invoke(grpc_stream_retry_timer_cb)); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); - // initial_resource_versions should contain client side all resource:version info, and no - // added resources because this is a wildcard request. - expectSendMessage(type_url, {}, {}, "", Grpc::Status::WellKnownGrpcStatus::Ok, "", + // initial_resource_versions should contain client side all resource:version info, and an asterisk + // because this is a wildcard request. + expectSendMessage(type_url, {"*"}, {}, "", Grpc::Status::WellKnownGrpcStatus::Ok, "", {{"x", "1000"}, {"y", "2000"}}); grpc_mux_->grpcStreamForTest().onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Canceled, ""); - // Destruction of wildcard will not issue unsubscribe requests for the resources. + // Destruction of wildcard will issue an unsubscribe request for the resources. + expectSendMessage(type_url, {}, {"*"}); } // Test that we simply ignore a message for an unknown type_url, with no ill effects. From 1d53769a077de08247587f4bb6a6d5dd80a59cf3 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 6 Jul 2021 17:34:10 +0200 Subject: [PATCH 09/49] Better comments, drop redundant tests Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 7 +- .../config/delta_subscription_state_test.cc | 82 ++++--------------- test/common/config/new_grpc_mux_impl_test.cc | 2 + 3 files changed, 19 insertions(+), 72 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 716dd650e5cd..6c4a26b0eaf9 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -193,17 +193,12 @@ DeltaSubscriptionState::getNextRequestAckless() { (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); } // Add resource names to resource_names_subscribe only if this is not a wildcard subscription - // request or if we requested this resource explicitly (so we are actually in explicit - // wildcard mode). + // request or if we requested this resource explicitly. if (!has_wildcard_subscription_ || resource_state.type() == ResourceType::ExplicitlyRequested) { names_added_.insert(resource_name); } } - // We are not clearing the names_added_ set. If we are in implicit wildcard subscription mode, - // then the set should already be empty. If we are in explicit wildcard mode then the set will - // contain the names we explicitly requested, but we need to add * to the list to make sure it's - // sent too. if (has_wildcard_subscription_) { names_added_.insert("*"); } diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index a8f6b7027abf..5597e5718592 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -346,8 +346,8 @@ TEST_F(DeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect) { EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } -// Check that switching into wildcard subscription after initial -// request switches us into the explicit wildcard mode. +// Check that subscribing to the wildcard resource affects the initial request (so resources +// received from server from the wildcard subscription are not resent). TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); @@ -355,7 +355,7 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { EXPECT_CALL(*timer_, disableTimer()).Times(2); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - // switch into wildcard mode + // Add a wildcard subscription. state_.updateSubscriptionInterest({"name4", "*"}, {"name1"}); state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); @@ -369,6 +369,9 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { UnorderedElementsAre("name2", "name3", "name4", "*")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); + // Here we will receive the resource name5, which is not a resource we are explicitly interested + // in, thus it came in response to wildcard subscription. As such, it should not appear later in + // the initial request after a reset. Protobuf::RepeatedPtrField add4_5 = populateRepeatedResource({{"name4", "version4A"}, {"name5", "version5A"}}); deliverDiscoveryResponse(add4_5, {}, "debugversion1"); @@ -390,8 +393,9 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { // For wildcard subscription, upon a reconnection, the server is supposed to assume a blank slate // for the Envoy's state (hence the need for initial_resource_versions), and the -// resource_names_subscribe and resource_names_unsubscribe must be empty if we haven't gained any -// new explicit interest in a resource. In such case, the client should send an empty request. +// resource_names_subscribe should contain only an asterisk and resource_names_unsubscribe must be +// empty if we haven't gained any new explicit interest in a resource. In such case, the client +// should send a request with only a wildcard subscription. TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnectImplicit) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); @@ -408,38 +412,17 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } -// For wildcard subscription, upon a reconnection, the server is supposed to assume a blank slate -// for the Envoy's state (hence the need for initial_resource_versions). The -// resource_names_unsubscribe must be empty (as is expected of every wildcard first message). The -// resource_names_subscribe should contain all the resources we are explicitly interested in and a -// special resource denoting a wildcard subscription. -TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnectExplicit) { - Protobuf::RepeatedPtrField add1_2 = - populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); - EXPECT_CALL(*timer_, disableTimer()); - deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - - state_.updateSubscriptionInterest({"name3"}, {"name1"}); - state_.markStreamFresh(); // simulate a stream reconnection - envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); - // Regarding the resource_names_subscribe field: - // name1: do not include: we lost interest. - // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. - // name3: yes do include: we are explicitly interested. - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*", "name3")); - EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); -} - // Check the contents of the requests after cancelling the wildcard // subscription and then reconnection. The second request should look // like a non-wildcard request, so mention all the known resources in // the initial request. -TEST_F(WildcardDeltaSubscriptionStateTest, CancellingImplicitWildcardSubscription) { +TEST_F(WildcardDeltaSubscriptionStateTest, CancellingWildcardSubscription) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); EXPECT_CALL(*timer_, disableTimer()); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + // Cancel the wildcard subscription. state_.updateSubscriptionInterest({"name3"}, {"name1", "*"}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); @@ -454,37 +437,6 @@ TEST_F(WildcardDeltaSubscriptionStateTest, CancellingImplicitWildcardSubscriptio EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } -// Check the contents of the requests after cancelling the wildcard -// subscription and then reconnection. The second request should look -// like a non-wildcard request, so mention all the known resources in -// the initial request. -TEST_F(WildcardDeltaSubscriptionStateTest, CancellingExplicitWildcardSubscription) { - Protobuf::RepeatedPtrField add1_2 = - populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); - EXPECT_CALL(*timer_, disableTimer()); - deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - // switch to explicit wildcard subscription - state_.updateSubscriptionInterest({"name3"}, {}); - envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); - - // cancel wildcard subscription - state_.updateSubscriptionInterest({"name4"}, {"name1", "*"}); - cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name4")); - EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", "*")); - state_.markStreamFresh(); // simulate a stream reconnection - // Regarding the resource_names_subscribe field: - // name1: do not include: we lost interest. - // name2: yes do include: we are interested, and it's not wildcard. - // name3: yes do include: we are interested, and it's not wildcard. - // name4: yes do include: we are interested, and it's not wildcard. - cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), - UnorderedElementsAre("name2", "name3", "name4")); - EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); -} - // Check that resource changes from being interested in implicitly to explicitly when we update the // subscription interest. Such resources will show up in the initial wildcard requests // too. Receiving the update on such resource will not change their interest mode. @@ -494,29 +446,27 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { EXPECT_CALL(*timer_, disableTimer()).Times(2); deliverDiscoveryResponse(add1_2_a, {}, "debugversion1"); - // verify that neither name1 nor name2 appears in the initial request (they are of implicit + // Verify that neither name1 nor name2 appears in the initial request (they are of implicit // interest and initial wildcard request should not contain those). state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // express the interest in name1 explicitly and verify that the follow-up request will contain it - // (this also switches the wildcard mode to explicit, but we won't see * in resource names, - // because we already are in wildcard mode). + // Express the interest in name1 explicitly and verify that the follow-up request will contain it. state_.updateSubscriptionInterest({"name1"}, {}); cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // verify that name1 and * appear in the initial request (name1 is of explicit interest and we are - // in explicit wildcard mode). + // Verify that name1 and * appear in the initial request (name1 is of explicit interest and we + // have a wildcard subscription). state_.markStreamFresh(); // simulate a stream reconnection cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", "*")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // verify that getting an update on name1 will keep name1 in the explicit interest mode + // Verify that getting an update on name1 will keep name1 in the explicit interest mode Protobuf::RepeatedPtrField add1_2_b = populateRepeatedResource({{"name1", "version1B"}, {"name2", "version2B"}}); deliverDiscoveryResponse(add1_2_b, {}, "debugversion1"); diff --git a/test/common/config/new_grpc_mux_impl_test.cc b/test/common/config/new_grpc_mux_impl_test.cc index 43420c82e9fb..0eca286fddd8 100644 --- a/test/common/config/new_grpc_mux_impl_test.cc +++ b/test/common/config/new_grpc_mux_impl_test.cc @@ -129,6 +129,8 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { // Update to bar type should resend Node. expectSendMessage("bar", {}, {}); local_info_.context_provider_.update_cb_handler_.runCallbacks("bar"); + // Wildcard subscription will be cancelled, just like any other + // subscription. expectSendMessage("bar", {}, {"*"}); expectSendMessage("foo", {}, {"x", "y"}); } From fa839498c0c2a39ed204ef9ea8dff62a51560451 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 6 Jul 2021 17:35:18 +0200 Subject: [PATCH 10/49] Add constants for wildcards This is to avoid putting "*" all over the place, but instead have a readable name. Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 10 +++++----- source/common/config/new_grpc_mux_impl.cc | 3 +-- source/common/config/watch_map.cc | 5 ++++- source/common/config/watch_map.h | 3 +++ .../config/delta_subscription_state_test.cc | 20 +++++++++---------- test/common/config/new_grpc_mux_impl_test.cc | 10 +++++----- 6 files changed, 28 insertions(+), 23 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 6c4a26b0eaf9..e55634836342 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -59,13 +59,13 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } // Handle the special case of an empty initial resources list as making a wildcard subscription. if (!any_request_sent_yet_in_current_stream_ && cur_added.empty() && cur_removed.empty()) { - names_removed_.erase("*"); - names_added_.insert("*"); + names_removed_.erase(Wildcard); + names_added_.insert(Wildcard); } - if (names_added_.find("*") != names_added_.end()) { + if (names_added_.find(Wildcard) != names_added_.end()) { has_wildcard_subscription_ = true; } - if (names_removed_.find("*") != names_removed_.end()) { + if (names_removed_.find(Wildcard) != names_removed_.end()) { has_wildcard_subscription_ = false; } } @@ -200,7 +200,7 @@ DeltaSubscriptionState::getNextRequestAckless() { } } if (has_wildcard_subscription_) { - names_added_.insert("*"); + names_added_.insert(Wildcard); } names_removed_.clear(); } diff --git a/source/common/config/new_grpc_mux_impl.cc b/source/common/config/new_grpc_mux_impl.cc index e4ef58403b4b..05ca3eec58af 100644 --- a/source/common/config/new_grpc_mux_impl.cc +++ b/source/common/config/new_grpc_mux_impl.cc @@ -136,8 +136,7 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, if (entry == subscriptions_.end()) { // We don't yet have a subscription for type_url! Make one! addSubscription(type_url, options.use_namespace_matching_); - const absl::flat_hash_set wildcard_resource{"*"}; - const auto& effective_resources = (resources.empty() ? wildcard_resource : resources); + const auto& effective_resources = (resources.empty() ? WildcardSet : resources); return addWatch(type_url, effective_resources, callbacks, resource_decoder, options); } diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 016523ddcd63..312156d2fca8 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -10,6 +10,9 @@ namespace Envoy { namespace Config { +const std::string Wildcard = "*"; +const absl::flat_hash_set WildcardSet = {Wildcard}; + namespace { // Returns the namespace part (if there's any) in the resource name. std::string namespaceFromName(const std::string& resource_name) { @@ -49,7 +52,7 @@ void WatchMap::removeDeferredWatches() { AddedRemoved WatchMap::updateWatchInterest(Watch* watch, const absl::flat_hash_set& update_to_these_names) { - if (update_to_these_names.contains("*")) { + if (update_to_these_names.contains(Wildcard)) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index d1139e23e0af..eaf5fdc81320 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -16,6 +16,9 @@ namespace Envoy { namespace Config { +extern const std::string Wildcard; +extern const absl::flat_hash_set WildcardSet; + struct AddedRemoved { AddedRemoved(absl::flat_hash_set&& added, absl::flat_hash_set&& removed) : added_(std::move(added)), removed_(std::move(removed)) {} diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 5597e5718592..7f907616fd69 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -104,7 +104,7 @@ class DeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { // Delta subscription state of a wildcard subscription request. class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, {"*"}) {} + WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, {Wildcard}) {} }; // Basic gaining/losing interest in resources should lead to subscription updates. @@ -356,7 +356,7 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { deliverDiscoveryResponse(add1_2, {}, "debugversion1"); // Add a wildcard subscription. - state_.updateSubscriptionInterest({"name4", "*"}, {"name1"}); + state_.updateSubscriptionInterest({"name4", Wildcard}, {"name1"}); state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); // Regarding the resource_names_subscribe field: @@ -366,7 +366,7 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { // name4: yes do include: we are explicitly interested // *: explicit wildcard subscription EXPECT_THAT(cur_request.resource_names_subscribe(), - UnorderedElementsAre("name2", "name3", "name4", "*")); + UnorderedElementsAre("name2", "name3", "name4", Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); // Here we will receive the resource name5, which is not a resource we are explicitly interested @@ -387,7 +387,7 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { // initial request // *: explicit wildcard subscription EXPECT_THAT(cur_request.resource_names_subscribe(), - UnorderedElementsAre("name2", "name3", "name4", "*")); + UnorderedElementsAre("name2", "name3", "name4", Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -408,7 +408,7 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect // *: include, it's a wildcard subscription // name1: do not include: we lost interest. // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre(Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -423,10 +423,10 @@ TEST_F(WildcardDeltaSubscriptionStateTest, CancellingWildcardSubscription) { deliverDiscoveryResponse(add1_2, {}, "debugversion1"); // Cancel the wildcard subscription. - state_.updateSubscriptionInterest({"name3"}, {"name1", "*"}); + state_.updateSubscriptionInterest({"name3"}, {"name1", Wildcard}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); - EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", "*")); + EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", Wildcard)); state_.markStreamFresh(); // simulate a stream reconnection // Regarding the resource_names_subscribe field: // name1: do not include: we lost interest. @@ -450,7 +450,7 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { // interest and initial wildcard request should not contain those). state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre(Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); // Express the interest in name1 explicitly and verify that the follow-up request will contain it. @@ -463,7 +463,7 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { // have a wildcard subscription). state_.markStreamFresh(); // simulate a stream reconnection cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", "*")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); // Verify that getting an update on name1 will keep name1 in the explicit interest mode @@ -472,7 +472,7 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { deliverDiscoveryResponse(add1_2_b, {}, "debugversion1"); state_.markStreamFresh(); // simulate a stream reconnection cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", "*")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } diff --git a/test/common/config/new_grpc_mux_impl_test.cc b/test/common/config/new_grpc_mux_impl_test.cc index 0eca286fddd8..abcbb2d4cd66 100644 --- a/test/common/config/new_grpc_mux_impl_test.cc +++ b/test/common/config/new_grpc_mux_impl_test.cc @@ -119,7 +119,7 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage("foo", {"x", "y"}, {}); // This is a wildcard subscription, thus expect the wildcard symbol. - expectSendMessage("bar", {"*"}, {}); + expectSendMessage("bar", {Wildcard}, {}); grpc_mux_->start(); // Unknown type, shouldn't do anything. local_info_.context_provider_.update_cb_handler_.runCallbacks("baz"); @@ -131,7 +131,7 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { local_info_.context_provider_.update_cb_handler_.runCallbacks("bar"); // Wildcard subscription will be cancelled, just like any other // subscription. - expectSendMessage("bar", {}, {"*"}); + expectSendMessage("bar", {}, {Wildcard}); expectSendMessage("foo", {}, {"x", "y"}); } @@ -201,7 +201,7 @@ TEST_F(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { auto foo_sub = grpc_mux_->addWatch(type_url, {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // Send a wildcard request on new connection. - expectSendMessage(type_url, {"*"}, {}); + expectSendMessage(type_url, {Wildcard}, {}); grpc_mux_->start(); // An helper function to create a response with a single load_assignment resource @@ -262,11 +262,11 @@ TEST_F(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // initial_resource_versions should contain client side all resource:version info, and an asterisk // because this is a wildcard request. - expectSendMessage(type_url, {"*"}, {}, "", Grpc::Status::WellKnownGrpcStatus::Ok, "", + expectSendMessage(type_url, {Wildcard}, {}, "", Grpc::Status::WellKnownGrpcStatus::Ok, "", {{"x", "1000"}, {"y", "2000"}}); grpc_mux_->grpcStreamForTest().onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Canceled, ""); // Destruction of wildcard will issue an unsubscribe request for the resources. - expectSendMessage(type_url, {}, {"*"}); + expectSendMessage(type_url, {}, {Wildcard}); } // Test that we simply ignore a message for an unknown type_url, with no ill effects. From 6748f53e05d0be806829e2910ec750c7c6c48235 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 6 Jul 2021 20:03:16 +0200 Subject: [PATCH 11/49] Test fixes Signed-off-by: Krzesimir Nowak --- test/integration/BUILD | 4 + test/integration/ads_integration.cc | 7 +- test/integration/ads_integration_test.cc | 150 ++++++++++++++-------- test/integration/base_integration_test.cc | 1 + test/integration/cds_integration_test.cc | 4 +- test/integration/vhds_integration_test.cc | 9 +- test/server/config_validation/BUILD | 1 + test/server/config_validation/xds_fuzz.cc | 5 +- 8 files changed, 120 insertions(+), 61 deletions(-) diff --git a/test/integration/BUILD b/test/integration/BUILD index 5d4083e23c05..add01080270f 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -57,6 +57,7 @@ envoy_cc_test( ":ads_integration_lib", ":http_integration_lib", "//source/common/config:protobuf_link_hacks", + "//source/common/config:watch_map_lib", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/test_common:network_utility_lib", @@ -117,6 +118,7 @@ envoy_cc_test( deps = [ ":http_integration_lib", "//source/common/config:protobuf_link_hacks", + "//source/common/config:watch_map_lib", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/mocks/runtime:runtime_mocks", @@ -210,6 +212,7 @@ envoy_cc_test( deps = [ ":http_integration_lib", "//source/common/config:protobuf_link_hacks", + "//source/common/config:watch_map_lib", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/mocks/runtime:runtime_mocks", @@ -734,6 +737,7 @@ envoy_cc_test_library( ":utility_lib", "//source/common/config:api_version_lib", "//source/common/config:version_converter_lib", + "//source/common/config:watch_map_lib", "//source/extensions/transport_sockets/tls:context_config_lib", "//source/extensions/transport_sockets/tls:context_lib", "//source/extensions/transport_sockets/tls:ssl_socket_lib", diff --git a/test/integration/ads_integration.cc b/test/integration/ads_integration.cc index dfa3b81539b9..518d26d1f3f2 100644 --- a/test/integration/ads_integration.cc +++ b/test/integration/ads_integration.cc @@ -9,6 +9,7 @@ #include "envoy/extensions/transport_sockets/tls/v3/cert.pb.h" #include "source/common/config/protobuf_link_hacks.h" +#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -143,7 +144,8 @@ void AdsIntegrationTest::initializeAds(const bool rate_limiting) { void AdsIntegrationTest::testBasicFlow() { // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -155,7 +157,8 @@ void AdsIntegrationTest::testBasicFlow() { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc index f8045c2ad55a..61878d2e7e10 100644 --- a/test/integration/ads_integration_test.cc +++ b/test/integration/ads_integration_test.cc @@ -8,6 +8,7 @@ #include "source/common/config/protobuf_link_hacks.h" #include "source/common/config/version_converter.h" +#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" #include "source/common/version/version.h" @@ -43,7 +44,8 @@ TEST_P(AdsIntegrationTest, BasicClusterInitialWarming) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V3); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", false); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -65,7 +67,8 @@ TEST_P(AdsIntegrationTest, ClusterInitializationUpdateTheOnlyWarmingCluster) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V3); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", false); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -112,7 +115,8 @@ TEST_P(AdsIntegrationTest, TestPrimaryClusterWarmClusterInitialization) { // Active cluster has the same name with warming cluster but has no blocking health check. auto active_cluster = ConfigHelper::buildStaticCluster("fake_cluster", port, loopback); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse(cds_type_url, {warming_cluster}, {warming_cluster}, {}, "1", false); @@ -139,7 +143,8 @@ TEST_P(AdsIntegrationTest, ClusterInitializationUpdateOneOfThe2Warming) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V3); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {ConfigHelper::buildStaticCluster("primary_cluster", 8000, "127.0.0.1"), @@ -202,7 +207,8 @@ TEST_P(AdsIntegrationTest, ClusterSharingSecretWarming) { auto cluster_1 = cluster_template; cluster_1.set_name("cluster_1"); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {cluster_0, cluster_1}, {cluster_0, cluster_1}, {}, "1", false); @@ -238,12 +244,14 @@ TEST_P(AdsIntegrationTest, Failure) { // Send initial configuration, failing each xDS once (via a type mismatch), validate we can // process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); EXPECT_TRUE(compareDiscoveryRequest( Config::TypeUrl::get().Cluster, "", {}, {}, {}, false, @@ -310,7 +318,8 @@ TEST_P(AdsIntegrationTest, Failure) { // Regression test for https://github.com/envoyproxy/envoy/issues/9682. TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -329,11 +338,11 @@ TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - // In SotW cluster_0 will be in the resource_names, but in delta-xDS - // resource_names_subscribe and resource_names_unsubscribe must be empty for - // a wildcard request (cluster_0 will appear in initial_resource_versions). - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, {}, {}, true)); + // In SotW cluster_0 will be in the resource_names, but in delta-xDS resource_names_subscribe will + // have an asterisk and resource_names_unsubscribe must be empty for a wildcard request (cluster_0 + // will appear in initial_resource_versions). + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, + {Config::Wildcard}, {}, true)); } // Verifies that upon stream reconnection: @@ -342,7 +351,8 @@ TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { // Regression test for https://github.com/envoyproxy/envoy/issues/16063. TEST_P(AdsIntegrationTest, ResourceNamesOnStreamReset) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -361,11 +371,11 @@ TEST_P(AdsIntegrationTest, ResourceNamesOnStreamReset) { RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - // In SotW cluster_0 will be in the resource_names, but in delta-xDS - // resource_names_subscribe and resource_names_unsubscribe must be empty for - // a wildcard request (cluster_0 will appear in initial_resource_versions). - EXPECT_TRUE( - compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, {}, {}, true)); + // In SotW cluster_0 will be in the resource_names, but in delta-xDS resource_names_subscribe will + // have an asterisk and resource_names_unsubscribe must be empty for a wildcard request (cluster_0 + // will appear in initial_resource_versions). + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, + {Config::Wildcard}, {}, true)); } // Validate that the request with duplicate listeners is rejected. @@ -373,7 +383,8 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -385,7 +396,8 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); // Send duplicate listeners and validate that the update is rejected. sendDiscoveryResponse( @@ -402,7 +414,8 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { TEST_P(AdsIntegrationTest, DEPRECATED_FEATURE_TEST(RejectV2TransportConfigByDefault)) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); auto cluster = buildCluster("cluster_0"); auto* api_config_source = cluster.mutable_eds_cluster_config()->mutable_eds_config()->mutable_api_config_source(); @@ -457,7 +470,8 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithNoRdsChanges) { // an active cluster is replaced by a newer cluster undergoing warming. TEST_P(AdsIntegrationTest, CdsEdsReplacementWarming) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -469,7 +483,8 @@ TEST_P(AdsIntegrationTest, CdsEdsReplacementWarming) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -514,7 +529,8 @@ TEST_P(AdsIntegrationTest, DuplicateInitialClusters) { // Send initial configuration, failing each xDS once (via a type mismatch), validate we can // process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildCluster("duplicate_cluster"), buildCluster("duplicate_cluster")}, @@ -529,7 +545,8 @@ TEST_P(AdsIntegrationTest, RedisClusterRemoval) { initialize(); // Send initial configuration with a redis cluster and a redis proxy listener. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildRedisCluster("redis_cluster")}, {buildRedisCluster("redis_cluster")}, {}, "1"); @@ -541,7 +558,8 @@ TEST_P(AdsIntegrationTest, RedisClusterRemoval) { {buildClusterLoadAssignment("redis_cluster")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildRedisListener("listener_0", "redis_cluster")}, {buildRedisListener("listener_0", "redis_cluster")}, {}, "1"); @@ -569,7 +587,8 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingClusters) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -581,7 +600,8 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingClusters) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -614,7 +634,8 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -626,7 +647,8 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -696,7 +718,8 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -708,7 +731,8 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -778,7 +802,8 @@ TEST_P(AdsIntegrationTest, RemoveWarmingListener) { initialize(); // Send initial configuration to start workers, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -790,7 +815,8 @@ TEST_P(AdsIntegrationTest, RemoveWarmingListener) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -840,7 +866,8 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -852,7 +879,8 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -976,7 +1004,8 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { // --------------------- // Initial request for any cluster, respond with cluster_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -992,7 +1021,8 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); // Initial request for any listener, respond with listener_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -1190,7 +1220,8 @@ TEST_P(AdsIntegrationTest, ListenerDrainBeforeServerStart) { initialize(); // Initial request for cluster, response for cluster_0. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1205,7 +1236,8 @@ TEST_P(AdsIntegrationTest, ListenerDrainBeforeServerStart) { EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); // Initial request for any listener, respond with listener_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -1262,7 +1294,8 @@ TEST_P(AdsIntegrationTest, SetNodeAlways) { initialize(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1274,7 +1307,8 @@ TEST_P(AdsIntegrationTest, SetNodeAlways) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, true)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); }; // Check if EDS cluster defined in file is loaded before ADS request and used as xDS server @@ -1362,7 +1396,8 @@ TEST_P(AdsClusterFromFileIntegrationTest, BasicTestWidsAdsEndpointLoadedFromFile Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("ads_eds_cluster")}, {buildClusterLoadAssignment("ads_eds_cluster")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {})); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", {"ads_eds_cluster"}, {}, {})); @@ -1402,7 +1437,8 @@ class AdsIntegrationTestWithRtds : public AdsIntegrationTest { Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {})); EXPECT_TRUE( compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, {}, {})); } @@ -1455,7 +1491,8 @@ class AdsIntegrationTestWithRtdsAndSecondaryClusters : public AdsIntegrationTest EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, false)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1475,7 +1512,8 @@ TEST_P(AdsIntegrationTest, ContextParameterUpdate) { initialize(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1487,7 +1525,8 @@ TEST_P(AdsIntegrationTest, ContextParameterUpdate) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, + {Config::Wildcard}, {}, false)); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {}, false)); @@ -1692,7 +1731,8 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(BasicClusterInitialWarming)) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V2); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", true); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -1719,7 +1759,8 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(CdsPausedDuringWarming)) { envoy::config::core::v3::ApiVersion::V2); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", true); EXPECT_TRUE(compareDiscoveryRequest(eds_type_url, "", {"cluster_0"}, {"cluster_0"}, {})); @@ -1729,7 +1770,8 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(CdsPausedDuringWarming)) { {buildClusterLoadAssignment("cluster_0")}, {}, "1", true); EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(lds_type_url, "", {}, {}, {})); + EXPECT_TRUE( + compareDiscoveryRequest(lds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {})); sendDiscoveryResponse( lds_type_url, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1", true); @@ -1844,7 +1886,8 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(TypeUrlAnnotationRegression)) { const auto cds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V2); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); auto cluster = buildCluster("cluster_0"); auto* bias = cluster.mutable_least_request_lb_config()->mutable_active_request_bias(); bias->set_default_value(1.1); @@ -1874,7 +1917,8 @@ TEST_P(AdsV2ResourceRejectTest, DEPRECATED_FEATURE_TEST(RejectV2ConfigByDefault) const auto cds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V2); - EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); + EXPECT_TRUE( + compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", true); test_server_->waitForCounterGe("cluster_manager.cds.update_rejected", 1); diff --git a/test/integration/base_integration_test.cc b/test/integration/base_integration_test.cc index 9421ea9f1901..a4b4b4eeae75 100644 --- a/test/integration/base_integration_test.cc +++ b/test/integration/base_integration_test.cc @@ -19,6 +19,7 @@ #include "source/common/common/fmt.h" #include "source/common/common/thread.h" #include "source/common/config/api_version.h" +#include "source/common/config/watch_map.h" #include "source/common/event/libevent.h" #include "source/common/network/utility.h" #include "source/extensions/transport_sockets/tls/context_config_impl.h" diff --git a/test/integration/cds_integration_test.cc b/test/integration/cds_integration_test.cc index 4b62e426f021..37a2a0ea1247 100644 --- a/test/integration/cds_integration_test.cc +++ b/test/integration/cds_integration_test.cc @@ -4,6 +4,7 @@ #include "envoy/stats/scope.h" #include "source/common/config/protobuf_link_hacks.h" +#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -84,7 +85,8 @@ class CdsIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, public Ht acceptXdsConnection(); // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for cluster_1. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "55"); diff --git a/test/integration/vhds_integration_test.cc b/test/integration/vhds_integration_test.cc index 3af21e7945fe..62d139bdbd1f 100644 --- a/test/integration/vhds_integration_test.cc +++ b/test/integration/vhds_integration_test.cc @@ -4,6 +4,7 @@ #include "envoy/stats/scope.h" #include "source/common/config/protobuf_link_hacks.h" +#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -232,8 +233,8 @@ TEST_P(VhdsInitializationTest, InitializeVhdsAfterRdsHasBeenInitialized) { RELEASE_ASSERT(result, result.message()); vhds_stream_->startGrpcStream(); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_)); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {Config::Wildcard}, + {}, vhds_stream_)); sendDeltaDiscoveryResponse( Config::TypeUrl::get().VirtualHost, {TestUtility::parseYaml( @@ -322,8 +323,8 @@ class VhdsIntegrationTest : public HttpIntegrationTest, RELEASE_ASSERT(result, result.message()); vhds_stream_->startGrpcStream(); - EXPECT_TRUE( - compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_)); + EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {Config::Wildcard}, + {}, vhds_stream_)); sendDeltaDiscoveryResponse( Config::TypeUrl::get().VirtualHost, {buildVirtualHost()}, {}, "1", vhds_stream_); EXPECT_TRUE( diff --git a/test/server/config_validation/BUILD b/test/server/config_validation/BUILD index 2292906cddb9..ac4848fc7316 100644 --- a/test/server/config_validation/BUILD +++ b/test/server/config_validation/BUILD @@ -133,6 +133,7 @@ envoy_cc_test_library( deps = [ ":xds_fuzz_proto_cc_proto", ":xds_verifier_lib", + "//source/common/config:watch_map_lib", "//test/fuzz:utility_lib", "//test/integration:http_integration_lib", "@envoy_api//envoy/admin/v3:pkg_cc_proto", diff --git a/test/server/config_validation/xds_fuzz.cc b/test/server/config_validation/xds_fuzz.cc index 3b4d23f4b157..e3419c29d13d 100644 --- a/test/server/config_validation/xds_fuzz.cc +++ b/test/server/config_validation/xds_fuzz.cc @@ -6,6 +6,8 @@ #include "envoy/config/listener/v3/listener.pb.h" #include "envoy/config/route/v3/route.pb.h" +#include "source/common/config/watch_map.h" + namespace Envoy { // Helper functions to build API responses. @@ -234,7 +236,8 @@ void XdsFuzzTest::replay() { initialize(); // Set up cluster. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, + {Config::Wildcard}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "0"); From 872e737d67a833ceaab5237ae174ad19a56ed3cf Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 8 Jul 2021 15:19:21 +0200 Subject: [PATCH 12/49] Revert "Test fixes" This reverts commit 6748f53e05d0be806829e2910ec750c7c6c48235. Signed-off-by: Krzesimir Nowak --- test/integration/BUILD | 4 - test/integration/ads_integration.cc | 7 +- test/integration/ads_integration_test.cc | 150 ++++++++-------------- test/integration/base_integration_test.cc | 1 - test/integration/cds_integration_test.cc | 4 +- test/integration/vhds_integration_test.cc | 9 +- test/server/config_validation/BUILD | 1 - test/server/config_validation/xds_fuzz.cc | 4 +- 8 files changed, 61 insertions(+), 119 deletions(-) diff --git a/test/integration/BUILD b/test/integration/BUILD index 70cc0021a2cc..7e6d0990dbf6 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -58,7 +58,6 @@ envoy_cc_test( ":ads_integration_lib", ":http_integration_lib", "//source/common/config:protobuf_link_hacks", - "//source/common/config:watch_map_lib", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/test_common:network_utility_lib", @@ -119,7 +118,6 @@ envoy_cc_test( deps = [ ":http_integration_lib", "//source/common/config:protobuf_link_hacks", - "//source/common/config:watch_map_lib", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/mocks/runtime:runtime_mocks", @@ -213,7 +211,6 @@ envoy_cc_test( deps = [ ":http_integration_lib", "//source/common/config:protobuf_link_hacks", - "//source/common/config:watch_map_lib", "//source/common/protobuf:utility_lib", "//test/common/grpc:grpc_client_integration_lib", "//test/mocks/runtime:runtime_mocks", @@ -738,7 +735,6 @@ envoy_cc_test_library( ":utility_lib", "//source/common/config:api_version_lib", "//source/common/config:version_converter_lib", - "//source/common/config:watch_map_lib", "//source/extensions/transport_sockets/tls:context_config_lib", "//source/extensions/transport_sockets/tls:context_lib", "//source/extensions/transport_sockets/tls:ssl_socket_lib", diff --git a/test/integration/ads_integration.cc b/test/integration/ads_integration.cc index 4bf7a23ddb34..f356c7cd6cc2 100644 --- a/test/integration/ads_integration.cc +++ b/test/integration/ads_integration.cc @@ -10,7 +10,6 @@ #include "source/common/common/matchers.h" #include "source/common/config/protobuf_link_hacks.h" -#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -145,8 +144,7 @@ void AdsIntegrationTest::initializeAds(const bool rate_limiting) { void AdsIntegrationTest::testBasicFlow() { // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -158,8 +156,7 @@ void AdsIntegrationTest::testBasicFlow() { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc index 61878d2e7e10..f8045c2ad55a 100644 --- a/test/integration/ads_integration_test.cc +++ b/test/integration/ads_integration_test.cc @@ -8,7 +8,6 @@ #include "source/common/config/protobuf_link_hacks.h" #include "source/common/config/version_converter.h" -#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" #include "source/common/version/version.h" @@ -44,8 +43,7 @@ TEST_P(AdsIntegrationTest, BasicClusterInitialWarming) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V3); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", false); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -67,8 +65,7 @@ TEST_P(AdsIntegrationTest, ClusterInitializationUpdateTheOnlyWarmingCluster) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V3); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", false); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -115,8 +112,7 @@ TEST_P(AdsIntegrationTest, TestPrimaryClusterWarmClusterInitialization) { // Active cluster has the same name with warming cluster but has no blocking health check. auto active_cluster = ConfigHelper::buildStaticCluster("fake_cluster", port, loopback); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse(cds_type_url, {warming_cluster}, {warming_cluster}, {}, "1", false); @@ -143,8 +139,7 @@ TEST_P(AdsIntegrationTest, ClusterInitializationUpdateOneOfThe2Warming) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V3); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {ConfigHelper::buildStaticCluster("primary_cluster", 8000, "127.0.0.1"), @@ -207,8 +202,7 @@ TEST_P(AdsIntegrationTest, ClusterSharingSecretWarming) { auto cluster_1 = cluster_template; cluster_1.set_name("cluster_1"); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {cluster_0, cluster_1}, {cluster_0, cluster_1}, {}, "1", false); @@ -244,14 +238,12 @@ TEST_P(AdsIntegrationTest, Failure) { // Send initial configuration, failing each xDS once (via a type mismatch), validate we can // process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildClusterLoadAssignment("cluster_0")}, {buildClusterLoadAssignment("cluster_0")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); EXPECT_TRUE(compareDiscoveryRequest( Config::TypeUrl::get().Cluster, "", {}, {}, {}, false, @@ -318,8 +310,7 @@ TEST_P(AdsIntegrationTest, Failure) { // Regression test for https://github.com/envoyproxy/envoy/issues/9682. TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -338,11 +329,11 @@ TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - // In SotW cluster_0 will be in the resource_names, but in delta-xDS resource_names_subscribe will - // have an asterisk and resource_names_unsubscribe must be empty for a wildcard request (cluster_0 - // will appear in initial_resource_versions). - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, - {Config::Wildcard}, {}, true)); + // In SotW cluster_0 will be in the resource_names, but in delta-xDS + // resource_names_subscribe and resource_names_unsubscribe must be empty for + // a wildcard request (cluster_0 will appear in initial_resource_versions). + EXPECT_TRUE( + compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, {}, {}, true)); } // Verifies that upon stream reconnection: @@ -351,8 +342,7 @@ TEST_P(AdsIntegrationTest, ResendNodeOnStreamReset) { // Regression test for https://github.com/envoyproxy/envoy/issues/16063. TEST_P(AdsIntegrationTest, ResourceNamesOnStreamReset) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -371,11 +361,11 @@ TEST_P(AdsIntegrationTest, ResourceNamesOnStreamReset) { RELEASE_ASSERT(result, result.message()); xds_stream_->startGrpcStream(); - // In SotW cluster_0 will be in the resource_names, but in delta-xDS resource_names_subscribe will - // have an asterisk and resource_names_unsubscribe must be empty for a wildcard request (cluster_0 - // will appear in initial_resource_versions). - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, - {Config::Wildcard}, {}, true)); + // In SotW cluster_0 will be in the resource_names, but in delta-xDS + // resource_names_subscribe and resource_names_unsubscribe must be empty for + // a wildcard request (cluster_0 will appear in initial_resource_versions). + EXPECT_TRUE( + compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {"cluster_0"}, {}, {}, true)); } // Validate that the request with duplicate listeners is rejected. @@ -383,8 +373,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -396,8 +385,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); // Send duplicate listeners and validate that the update is rejected. sendDiscoveryResponse( @@ -414,8 +402,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingListeners) { TEST_P(AdsIntegrationTest, DEPRECATED_FEATURE_TEST(RejectV2TransportConfigByDefault)) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); auto cluster = buildCluster("cluster_0"); auto* api_config_source = cluster.mutable_eds_cluster_config()->mutable_eds_config()->mutable_api_config_source(); @@ -470,8 +457,7 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsWithNoRdsChanges) { // an active cluster is replaced by a newer cluster undergoing warming. TEST_P(AdsIntegrationTest, CdsEdsReplacementWarming) { initialize(); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -483,8 +469,7 @@ TEST_P(AdsIntegrationTest, CdsEdsReplacementWarming) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -529,8 +514,7 @@ TEST_P(AdsIntegrationTest, DuplicateInitialClusters) { // Send initial configuration, failing each xDS once (via a type mismatch), validate we can // process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildCluster("duplicate_cluster"), buildCluster("duplicate_cluster")}, @@ -545,8 +529,7 @@ TEST_P(AdsIntegrationTest, RedisClusterRemoval) { initialize(); // Send initial configuration with a redis cluster and a redis proxy listener. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildRedisCluster("redis_cluster")}, {buildRedisCluster("redis_cluster")}, {}, "1"); @@ -558,8 +541,7 @@ TEST_P(AdsIntegrationTest, RedisClusterRemoval) { {buildClusterLoadAssignment("redis_cluster")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildRedisListener("listener_0", "redis_cluster")}, {buildRedisListener("listener_0", "redis_cluster")}, {}, "1"); @@ -587,8 +569,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingClusters) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -600,8 +581,7 @@ TEST_P(AdsIntegrationTest, DuplicateWarmingClusters) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -634,8 +614,7 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -647,8 +626,7 @@ TEST_P(AdsIntegrationTest, CdsPausedDuringWarming) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -718,8 +696,7 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -731,8 +708,7 @@ TEST_P(AdsIntegrationTest, RemoveWarmingCluster) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -802,8 +778,7 @@ TEST_P(AdsIntegrationTest, RemoveWarmingListener) { initialize(); // Send initial configuration to start workers, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -815,8 +790,7 @@ TEST_P(AdsIntegrationTest, RemoveWarmingListener) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -866,8 +840,7 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { initialize(); // Send initial configuration, validate we can process a request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -879,8 +852,7 @@ TEST_P(AdsIntegrationTest, ClusterWarmingOnNamedResponse) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -1004,8 +976,7 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { // --------------------- // Initial request for any cluster, respond with cluster_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1021,8 +992,7 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); // Initial request for any listener, respond with listener_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -1220,8 +1190,7 @@ TEST_P(AdsIntegrationTest, ListenerDrainBeforeServerStart) { initialize(); // Initial request for cluster, response for cluster_0. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1236,8 +1205,7 @@ TEST_P(AdsIntegrationTest, ListenerDrainBeforeServerStart) { EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); // Initial request for any listener, respond with listener_0 version 1 - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {})); sendDiscoveryResponse( Config::TypeUrl::get().Listener, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1"); @@ -1294,8 +1262,7 @@ TEST_P(AdsIntegrationTest, SetNodeAlways) { initialize(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1307,8 +1274,7 @@ TEST_P(AdsIntegrationTest, SetNodeAlways) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, true)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, true)); }; // Check if EDS cluster defined in file is loaded before ADS request and used as xDS server @@ -1396,8 +1362,7 @@ TEST_P(AdsClusterFromFileIntegrationTest, BasicTestWidsAdsEndpointLoadedFromFile Config::TypeUrl::get().ClusterLoadAssignment, {buildClusterLoadAssignment("ads_eds_cluster")}, {buildClusterLoadAssignment("ads_eds_cluster")}, {}, "1"); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {})); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", {"ads_eds_cluster"}, {}, {})); @@ -1437,8 +1402,7 @@ class AdsIntegrationTestWithRtds : public AdsIntegrationTest { Config::TypeUrl::get().Runtime, {some_rtds_layer}, {some_rtds_layer}, {}, "1"); test_server_->waitForCounterGe("runtime.load_success", 1); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {})); EXPECT_TRUE( compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, {}, {})); } @@ -1491,8 +1455,7 @@ class AdsIntegrationTestWithRtdsAndSecondaryClusters : public AdsIntegrationTest EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Runtime, "1", {"ads_rtds_layer"}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, false)); sendDiscoveryResponse( Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1512,8 +1475,7 @@ TEST_P(AdsIntegrationTest, ContextParameterUpdate) { initialize(); // Check that the node is sent in each request. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1"); @@ -1525,8 +1487,7 @@ TEST_P(AdsIntegrationTest, ContextParameterUpdate) { {buildClusterLoadAssignment("cluster_0")}, {}, "1"); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {}, false)); - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {Config::Wildcard}, - {Config::Wildcard}, {}, false)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, {}, {}, false)); EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", {"cluster_0"}, {}, {}, false)); @@ -1731,8 +1692,7 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(BasicClusterInitialWarming)) { const auto eds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V2); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", true); test_server_->waitForGaugeEq("cluster_manager.warming_clusters", 1); @@ -1759,8 +1719,7 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(CdsPausedDuringWarming)) { envoy::config::core::v3::ApiVersion::V2); // Send initial configuration, validate we can process a request. - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", true); EXPECT_TRUE(compareDiscoveryRequest(eds_type_url, "", {"cluster_0"}, {"cluster_0"}, {})); @@ -1770,8 +1729,7 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(CdsPausedDuringWarming)) { {buildClusterLoadAssignment("cluster_0")}, {}, "1", true); EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "1", {}, {}, {})); - EXPECT_TRUE( - compareDiscoveryRequest(lds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {})); + EXPECT_TRUE(compareDiscoveryRequest(lds_type_url, "", {}, {}, {})); sendDiscoveryResponse( lds_type_url, {buildListener("listener_0", "route_config_0")}, {buildListener("listener_0", "route_config_0")}, {}, "1", true); @@ -1886,8 +1844,7 @@ TEST_P(AdsClusterV2Test, DEPRECATED_FEATURE_TEST(TypeUrlAnnotationRegression)) { const auto cds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V2); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); auto cluster = buildCluster("cluster_0"); auto* bias = cluster.mutable_least_request_lb_config()->mutable_active_request_bias(); bias->set_default_value(1.1); @@ -1917,8 +1874,7 @@ TEST_P(AdsV2ResourceRejectTest, DEPRECATED_FEATURE_TEST(RejectV2ConfigByDefault) const auto cds_type_url = Config::getTypeUrl( envoy::config::core::v3::ApiVersion::V2); - EXPECT_TRUE( - compareDiscoveryRequest(cds_type_url, "", {Config::Wildcard}, {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(cds_type_url, "", {}, {}, {}, true)); sendDiscoveryResponse( cds_type_url, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "1", true); test_server_->waitForCounterGe("cluster_manager.cds.update_rejected", 1); diff --git a/test/integration/base_integration_test.cc b/test/integration/base_integration_test.cc index 87a666f459fd..26c12716cfcb 100644 --- a/test/integration/base_integration_test.cc +++ b/test/integration/base_integration_test.cc @@ -19,7 +19,6 @@ #include "source/common/common/fmt.h" #include "source/common/common/thread.h" #include "source/common/config/api_version.h" -#include "source/common/config/watch_map.h" #include "source/common/event/libevent.h" #include "source/common/network/utility.h" #include "source/extensions/transport_sockets/tls/context_config_impl.h" diff --git a/test/integration/cds_integration_test.cc b/test/integration/cds_integration_test.cc index 37a2a0ea1247..4b62e426f021 100644 --- a/test/integration/cds_integration_test.cc +++ b/test/integration/cds_integration_test.cc @@ -4,7 +4,6 @@ #include "envoy/stats/scope.h" #include "source/common/config/protobuf_link_hacks.h" -#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -85,8 +84,7 @@ class CdsIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, public Ht acceptXdsConnection(); // Do the initial compareDiscoveryRequest / sendDiscoveryResponse for cluster_1. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {cluster1_}, {cluster1_}, {}, "55"); diff --git a/test/integration/vhds_integration_test.cc b/test/integration/vhds_integration_test.cc index 62d139bdbd1f..3af21e7945fe 100644 --- a/test/integration/vhds_integration_test.cc +++ b/test/integration/vhds_integration_test.cc @@ -4,7 +4,6 @@ #include "envoy/stats/scope.h" #include "source/common/config/protobuf_link_hacks.h" -#include "source/common/config/watch_map.h" #include "source/common/protobuf/protobuf.h" #include "source/common/protobuf/utility.h" @@ -233,8 +232,8 @@ TEST_P(VhdsInitializationTest, InitializeVhdsAfterRdsHasBeenInitialized) { RELEASE_ASSERT(result, result.message()); vhds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {Config::Wildcard}, - {}, vhds_stream_)); + EXPECT_TRUE( + compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_)); sendDeltaDiscoveryResponse( Config::TypeUrl::get().VirtualHost, {TestUtility::parseYaml( @@ -323,8 +322,8 @@ class VhdsIntegrationTest : public HttpIntegrationTest, RELEASE_ASSERT(result, result.message()); vhds_stream_->startGrpcStream(); - EXPECT_TRUE(compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {Config::Wildcard}, - {}, vhds_stream_)); + EXPECT_TRUE( + compareDeltaDiscoveryRequest(Config::TypeUrl::get().VirtualHost, {}, {}, vhds_stream_)); sendDeltaDiscoveryResponse( Config::TypeUrl::get().VirtualHost, {buildVirtualHost()}, {}, "1", vhds_stream_); EXPECT_TRUE( diff --git a/test/server/config_validation/BUILD b/test/server/config_validation/BUILD index 5602706a29bc..51ebda4ae37c 100644 --- a/test/server/config_validation/BUILD +++ b/test/server/config_validation/BUILD @@ -134,7 +134,6 @@ envoy_cc_test_library( ":xds_fuzz_proto_cc_proto", ":xds_verifier_lib", "//source/common/common:matchers_lib", - "//source/common/config:watch_map_lib", "//test/fuzz:utility_lib", "//test/integration:http_integration_lib", "@envoy_api//envoy/admin/v3:pkg_cc_proto", diff --git a/test/server/config_validation/xds_fuzz.cc b/test/server/config_validation/xds_fuzz.cc index 66795c22ee1a..653acd8db012 100644 --- a/test/server/config_validation/xds_fuzz.cc +++ b/test/server/config_validation/xds_fuzz.cc @@ -7,7 +7,6 @@ #include "envoy/config/route/v3/route.pb.h" #include "source/common/common/matchers.h" -#include "source/common/config/watch_map.h" namespace Envoy { @@ -237,8 +236,7 @@ void XdsFuzzTest::replay() { initialize(); // Set up cluster. - EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {Config::Wildcard}, - {Config::Wildcard}, {}, true)); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {buildCluster("cluster_0")}, {buildCluster("cluster_0")}, {}, "0"); From 502de45bbb471c3d1352af7ba955e6e7b57e0231 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 8 Jul 2021 15:22:08 +0200 Subject: [PATCH 13/49] Revert "Better comments, drop redundant tests" This reverts commit 1d53769a077de08247587f4bb6a6d5dd80a59cf3. Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 7 +- .../config/delta_subscription_state_test.cc | 82 +++++++++++++++---- test/common/config/new_grpc_mux_impl_test.cc | 2 - 3 files changed, 72 insertions(+), 19 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index e55634836342..f8e5b275bdf7 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -193,12 +193,17 @@ DeltaSubscriptionState::getNextRequestAckless() { (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); } // Add resource names to resource_names_subscribe only if this is not a wildcard subscription - // request or if we requested this resource explicitly. + // request or if we requested this resource explicitly (so we are actually in explicit + // wildcard mode). if (!has_wildcard_subscription_ || resource_state.type() == ResourceType::ExplicitlyRequested) { names_added_.insert(resource_name); } } + // We are not clearing the names_added_ set. If we are in implicit wildcard subscription mode, + // then the set should already be empty. If we are in explicit wildcard mode then the set will + // contain the names we explicitly requested, but we need to add * to the list to make sure it's + // sent too. if (has_wildcard_subscription_) { names_added_.insert(Wildcard); } diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 7f907616fd69..446af1379f5d 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -346,8 +346,8 @@ TEST_F(DeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect) { EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } -// Check that subscribing to the wildcard resource affects the initial request (so resources -// received from server from the wildcard subscription are not resent). +// Check that switching into wildcard subscription after initial +// request switches us into the explicit wildcard mode. TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); @@ -355,7 +355,7 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { EXPECT_CALL(*timer_, disableTimer()).Times(2); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - // Add a wildcard subscription. + // switch into wildcard mode state_.updateSubscriptionInterest({"name4", Wildcard}, {"name1"}); state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); @@ -369,9 +369,6 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { UnorderedElementsAre("name2", "name3", "name4", Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // Here we will receive the resource name5, which is not a resource we are explicitly interested - // in, thus it came in response to wildcard subscription. As such, it should not appear later in - // the initial request after a reset. Protobuf::RepeatedPtrField add4_5 = populateRepeatedResource({{"name4", "version4A"}, {"name5", "version5A"}}); deliverDiscoveryResponse(add4_5, {}, "debugversion1"); @@ -393,9 +390,8 @@ TEST_F(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { // For wildcard subscription, upon a reconnection, the server is supposed to assume a blank slate // for the Envoy's state (hence the need for initial_resource_versions), and the -// resource_names_subscribe should contain only an asterisk and resource_names_unsubscribe must be -// empty if we haven't gained any new explicit interest in a resource. In such case, the client -// should send a request with only a wildcard subscription. +// resource_names_subscribe and resource_names_unsubscribe must be empty if we haven't gained any +// new explicit interest in a resource. In such case, the client should send an empty request. TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnectImplicit) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); @@ -412,17 +408,38 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } +// For wildcard subscription, upon a reconnection, the server is supposed to assume a blank slate +// for the Envoy's state (hence the need for initial_resource_versions). The +// resource_names_unsubscribe must be empty (as is expected of every wildcard first message). The +// resource_names_subscribe should contain all the resources we are explicitly interested in and a +// special resource denoting a wildcard subscription. +TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnectExplicit) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + + state_.updateSubscriptionInterest({"name3"}, {"name1"}); + state_.markStreamFresh(); // simulate a stream reconnection + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. + // name3: yes do include: we are explicitly interested. + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*", "name3")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + // Check the contents of the requests after cancelling the wildcard // subscription and then reconnection. The second request should look // like a non-wildcard request, so mention all the known resources in // the initial request. -TEST_F(WildcardDeltaSubscriptionStateTest, CancellingWildcardSubscription) { +TEST_F(WildcardDeltaSubscriptionStateTest, CancellingImplicitWildcardSubscription) { Protobuf::RepeatedPtrField add1_2 = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); EXPECT_CALL(*timer_, disableTimer()); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - // Cancel the wildcard subscription. state_.updateSubscriptionInterest({"name3"}, {"name1", Wildcard}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); @@ -437,6 +454,37 @@ TEST_F(WildcardDeltaSubscriptionStateTest, CancellingWildcardSubscription) { EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } +// Check the contents of the requests after cancelling the wildcard +// subscription and then reconnection. The second request should look +// like a non-wildcard request, so mention all the known resources in +// the initial request. +TEST_F(WildcardDeltaSubscriptionStateTest, CancellingExplicitWildcardSubscription) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + // switch to explicit wildcard subscription + state_.updateSubscriptionInterest({"name3"}, {}); + envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); + + // cancel wildcard subscription + state_.updateSubscriptionInterest({"name4"}, {"name1", "*"}); + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name4")); + EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", "*")); + state_.markStreamFresh(); // simulate a stream reconnection + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: yes do include: we are interested, and it's not wildcard. + // name3: yes do include: we are interested, and it's not wildcard. + // name4: yes do include: we are interested, and it's not wildcard. + cur_request = state_.getNextRequestAckless(); + EXPECT_THAT(cur_request.resource_names_subscribe(), + UnorderedElementsAre("name2", "name3", "name4")); + EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); +} + // Check that resource changes from being interested in implicitly to explicitly when we update the // subscription interest. Such resources will show up in the initial wildcard requests // too. Receiving the update on such resource will not change their interest mode. @@ -446,27 +494,29 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { EXPECT_CALL(*timer_, disableTimer()).Times(2); deliverDiscoveryResponse(add1_2_a, {}, "debugversion1"); - // Verify that neither name1 nor name2 appears in the initial request (they are of implicit + // verify that neither name1 nor name2 appears in the initial request (they are of implicit // interest and initial wildcard request should not contain those). state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre(Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // Express the interest in name1 explicitly and verify that the follow-up request will contain it. + // express the interest in name1 explicitly and verify that the follow-up request will contain it + // (this also switches the wildcard mode to explicit, but we won't see * in resource names, + // because we already are in wildcard mode). state_.updateSubscriptionInterest({"name1"}, {}); cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // Verify that name1 and * appear in the initial request (name1 is of explicit interest and we - // have a wildcard subscription). + // verify that name1 and * appear in the initial request (name1 is of explicit interest and we are + // in explicit wildcard mode). state_.markStreamFresh(); // simulate a stream reconnection cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name1", Wildcard)); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); - // Verify that getting an update on name1 will keep name1 in the explicit interest mode + // verify that getting an update on name1 will keep name1 in the explicit interest mode Protobuf::RepeatedPtrField add1_2_b = populateRepeatedResource({{"name1", "version1B"}, {"name2", "version2B"}}); deliverDiscoveryResponse(add1_2_b, {}, "debugversion1"); diff --git a/test/common/config/new_grpc_mux_impl_test.cc b/test/common/config/new_grpc_mux_impl_test.cc index abcbb2d4cd66..e69717c56686 100644 --- a/test/common/config/new_grpc_mux_impl_test.cc +++ b/test/common/config/new_grpc_mux_impl_test.cc @@ -129,8 +129,6 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { // Update to bar type should resend Node. expectSendMessage("bar", {}, {}); local_info_.context_provider_.update_cb_handler_.runCallbacks("bar"); - // Wildcard subscription will be cancelled, just like any other - // subscription. expectSendMessage("bar", {}, {Wildcard}); expectSendMessage("foo", {}, {"x", "y"}); } From 4ba0fc49a92d71d82b49a1f6c4fcd3f46833a081 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 8 Jul 2021 15:27:56 +0200 Subject: [PATCH 14/49] Revert "Always send an asterisk as a wildcard subscription" This reverts commit 391461597f66bd5c1224285e4033c21b33e9abb2. Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 45 ++++++++++++------- .../common/config/delta_subscription_state.h | 23 +++++++++- source/common/config/new_grpc_mux_impl.cc | 17 ++++--- source/common/config/new_grpc_mux_impl.h | 8 ++-- source/common/config/watch_map.cc | 2 +- .../config/delta_subscription_state_test.cc | 15 +++---- test/common/config/new_grpc_mux_impl_test.cc | 17 +++---- 7 files changed, 80 insertions(+), 47 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index f8e5b275bdf7..1214e7f5b6bc 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -14,7 +14,7 @@ namespace Config { DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, - Event::Dispatcher& dispatcher) + Event::Dispatcher& dispatcher, const bool wildcard) // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on // empty resources as updates. : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), @@ -31,8 +31,9 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, watch_map_.onConfigUpdate({}, removed_resources, ""); }, dispatcher, dispatcher.timeSource()), - type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), - dispatcher_(dispatcher) {} + type_url_(std::move(type_url)), + mode_(wildcard ? WildcardMode::Implicit : WildcardMode::Disabled), watch_map_(watch_map), + local_info_(local_info), dispatcher_(dispatcher) {} void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, @@ -57,16 +58,30 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.erase(r); names_removed_.insert(r); } - // Handle the special case of an empty initial resources list as making a wildcard subscription. - if (!any_request_sent_yet_in_current_stream_ && cur_added.empty() && cur_removed.empty()) { - names_removed_.erase(Wildcard); - names_added_.insert(Wildcard); - } - if (names_added_.find(Wildcard) != names_added_.end()) { - has_wildcard_subscription_ = true; - } - if (names_removed_.find(Wildcard) != names_removed_.end()) { - has_wildcard_subscription_ = false; + switch (mode_) { + case WildcardMode::Implicit: + if (names_removed_.find(Wildcard) != names_removed_.end()) { + // we explicitly cancel the wildcard subscription + mode_ = WildcardMode::Disabled; + } else if (!names_added_.empty()) { + // switch to explicit mode if we requested some extra names + mode_ = WildcardMode::Explicit; + } + break; + + case WildcardMode::Explicit: + if (names_removed_.find(Wildcard) != names_removed_.end()) { + // we explicitly cancel the wildcard subscription + mode_ = WildcardMode::Disabled; + } + break; + + case WildcardMode::Disabled: + if (names_added_.find(Wildcard) != names_added_.end()) { + // we switch into an explicit wildcard subscription + mode_ = WildcardMode::Explicit; + } + break; } } @@ -195,7 +210,7 @@ DeltaSubscriptionState::getNextRequestAckless() { // Add resource names to resource_names_subscribe only if this is not a wildcard subscription // request or if we requested this resource explicitly (so we are actually in explicit // wildcard mode). - if (!has_wildcard_subscription_ || + if (mode_ == WildcardMode::Disabled || resource_state.type() == ResourceType::ExplicitlyRequested) { names_added_.insert(resource_name); } @@ -204,7 +219,7 @@ DeltaSubscriptionState::getNextRequestAckless() { // then the set should already be empty. If we are in explicit wildcard mode then the set will // contain the names we explicitly requested, but we need to add * to the list to make sure it's // sent too. - if (has_wildcard_subscription_) { + if (mode_ == WildcardMode::Explicit) { names_added_.insert(Wildcard); } names_removed_.clear(); diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index ed73c54e3d82..ef91509f3c7f 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -26,7 +26,8 @@ namespace Config { class DeltaSubscriptionState : public Logger::Loggable { public: DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, + const bool wildcard); // Update which resources we're interested in subscribing to. void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, @@ -70,6 +71,12 @@ class DeltaSubscriptionState : public Logger::Loggable { ReceivedFromServer, }; + // Determines the effective resource type. Explicitly requested type overrides the received from + // server type. + ResourceType effectiveResourceType(ResourceType old_type, ResourceType new_type) { + return (old_type == ResourceType::ReceivedFromServer) ? new_type : old_type; + } + class ResourceState { public: ResourceState(absl::optional version, ResourceType type) @@ -100,6 +107,18 @@ class DeltaSubscriptionState : public Logger::Loggable { ResourceType type_; }; + // Describes the wildcard mode the subscription is in. + enum class WildcardMode { + // This mode is being expressed by sending a wildcard subscription request with an empty + // resource subscription list. + Implicit, + // This mode is being expressed by sending a wildcard subscription request that contains "*" + // special name in the resource subscription list. + Explicit, + // This mode is means no wildcard subscription. + Disabled, + }; + void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); OptRef getResourceState(const std::string& resource_name); void removeResourceState(const std::string& resource_name); @@ -117,7 +136,7 @@ class DeltaSubscriptionState : public Logger::Loggable { TtlManager ttl_; const std::string type_url_; - bool has_wildcard_subscription_ = false; + WildcardMode mode_; UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; Event::Dispatcher& dispatcher_; diff --git a/source/common/config/new_grpc_mux_impl.cc b/source/common/config/new_grpc_mux_impl.cc index 05ca3eec58af..78f9fe829ddf 100644 --- a/source/common/config/new_grpc_mux_impl.cc +++ b/source/common/config/new_grpc_mux_impl.cc @@ -135,9 +135,11 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, auto entry = subscriptions_.find(type_url); if (entry == subscriptions_.end()) { // We don't yet have a subscription for type_url! Make one! - addSubscription(type_url, options.use_namespace_matching_); - const auto& effective_resources = (resources.empty() ? WildcardSet : resources); - return addWatch(type_url, effective_resources, callbacks, resource_decoder, options); + // No resources or an existence of the special name implies that + // this is a wildcard request subscription. + const bool wildcard = resources.empty() || (resources.find(Wildcard) != resources.end()); + addSubscription(type_url, options.use_namespace_matching_, wildcard); + return addWatch(type_url, resources, callbacks, resource_decoder, options); } Watch* watch = entry->second->watch_map_.addWatch(callbacks, resource_decoder); @@ -208,10 +210,11 @@ void NewGrpcMuxImpl::removeWatch(const std::string& type_url, Watch* watch) { entry->second->watch_map_.removeWatch(watch); } -void NewGrpcMuxImpl::addSubscription(const std::string& type_url, - const bool use_namespace_matching) { - subscriptions_.emplace(type_url, std::make_unique( - type_url, local_info_, use_namespace_matching, dispatcher_)); +void NewGrpcMuxImpl::addSubscription(const std::string& type_url, const bool use_namespace_matching, + const bool wildcard) { + subscriptions_.emplace(type_url, std::make_unique(type_url, local_info_, + use_namespace_matching, + dispatcher_, wildcard)); subscription_ordering_.emplace_back(type_url); } diff --git a/source/common/config/new_grpc_mux_impl.h b/source/common/config/new_grpc_mux_impl.h index 44dd14a73c2d..4c2246fed813 100644 --- a/source/common/config/new_grpc_mux_impl.h +++ b/source/common/config/new_grpc_mux_impl.h @@ -73,9 +73,10 @@ class NewGrpcMuxImpl struct SubscriptionStuff { SubscriptionStuff(const std::string& type_url, const LocalInfo::LocalInfo& local_info, - const bool use_namespace_matching, Event::Dispatcher& dispatcher) + const bool use_namespace_matching, Event::Dispatcher& dispatcher, + const bool wildcard) : watch_map_(use_namespace_matching), - sub_state_(type_url, watch_map_, local_info, dispatcher) {} + sub_state_(type_url, watch_map_, local_info, dispatcher, wildcard) {} WatchMap watch_map_; DeltaSubscriptionState sub_state_; @@ -129,7 +130,8 @@ class NewGrpcMuxImpl const SubscriptionOptions& options); // Adds a subscription for the type_url to the subscriptions map and order list. - void addSubscription(const std::string& type_url, bool use_namespace_matching); + void addSubscription(const std::string& type_url, bool use_namespace_matching, + const bool wildcard); void trySendDiscoveryRequests(); diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index 312156d2fca8..b0a8cfa96e35 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -52,7 +52,7 @@ void WatchMap::removeDeferredWatches() { AddedRemoved WatchMap::updateWatchInterest(Watch* watch, const absl::flat_hash_set& update_to_these_names) { - if (update_to_these_names.contains(Wildcard)) { + if (update_to_these_names.empty() || update_to_these_names.contains(Wildcard)) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 446af1379f5d..27db8965ca41 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -30,10 +30,10 @@ const char TypeUrl[] = "type.googleapis.com/envoy.api.v2.Cluster"; class DeltaSubscriptionStateTestBase : public testing::Test { protected: DeltaSubscriptionStateTestBase( - const std::string& type_url, + const std::string& type_url, const bool wildcard, const absl::flat_hash_set initial_resources = {"name1", "name2", "name3"}) : timer_(new Event::MockTimer(&dispatcher_)), - state_(type_url, callbacks_, local_info_, dispatcher_) { + state_(type_url, callbacks_, local_info_, dispatcher_, wildcard) { state_.updateSubscriptionInterest(initial_resources, {}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); @@ -98,13 +98,13 @@ populateRepeatedResource(std::vector> items) class DeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl) {} + DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, false) {} }; // Delta subscription state of a wildcard subscription request. class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, {Wildcard}) {} + WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, true, {}) {} }; // Basic gaining/losing interest in resources should lead to subscription updates. @@ -401,10 +401,9 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); // Regarding the resource_names_subscribe field: - // *: include, it's a wildcard subscription // name1: do not include: we lost interest. // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre(Wildcard)); + EXPECT_TRUE(cur_request.resource_names_subscribe().empty()); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -498,7 +497,7 @@ TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { // interest and initial wildcard request should not contain those). state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre(Wildcard)); + EXPECT_TRUE(cur_request.resource_names_subscribe().empty()); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); // express the interest in name1 explicitly and verify that the follow-up request will contain it @@ -674,7 +673,7 @@ TEST_F(DeltaSubscriptionStateTest, ResourceTTL) { class VhdsDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: VhdsDeltaSubscriptionStateTest() - : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost") {} + : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost", false) {} }; TEST_F(VhdsDeltaSubscriptionStateTest, ResourceTTL) { diff --git a/test/common/config/new_grpc_mux_impl_test.cc b/test/common/config/new_grpc_mux_impl_test.cc index e69717c56686..ce917b39a0fc 100644 --- a/test/common/config/new_grpc_mux_impl_test.cc +++ b/test/common/config/new_grpc_mux_impl_test.cc @@ -113,13 +113,10 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { setup(); InSequence s; auto foo_sub = grpc_mux_->addWatch("foo", {"x", "y"}, callbacks_, resource_decoder_, {}); - // Empty list of subscribed names means a wildcard subscription, so - // we will expect an asterisk in sent message. auto bar_sub = grpc_mux_->addWatch("bar", {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); expectSendMessage("foo", {"x", "y"}, {}); - // This is a wildcard subscription, thus expect the wildcard symbol. - expectSendMessage("bar", {Wildcard}, {}); + expectSendMessage("bar", {}, {}); grpc_mux_->start(); // Unknown type, shouldn't do anything. local_info_.context_provider_.update_cb_handler_.runCallbacks("baz"); @@ -129,7 +126,6 @@ TEST_F(NewGrpcMuxImplTest, DynamicContextParameters) { // Update to bar type should resend Node. expectSendMessage("bar", {}, {}); local_info_.context_provider_.update_cb_handler_.runCallbacks("bar"); - expectSendMessage("bar", {}, {Wildcard}); expectSendMessage("foo", {}, {"x", "y"}); } @@ -199,7 +195,7 @@ TEST_F(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { auto foo_sub = grpc_mux_->addWatch(type_url, {}, callbacks_, resource_decoder_, {}); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); // Send a wildcard request on new connection. - expectSendMessage(type_url, {Wildcard}, {}); + expectSendMessage(type_url, {}, {}); grpc_mux_->start(); // An helper function to create a response with a single load_assignment resource @@ -258,13 +254,12 @@ TEST_F(NewGrpcMuxImplTest, ReconnectionResetsWildcardSubscription) { EXPECT_CALL(*grpc_stream_retry_timer, enableTimer(_, _)) .WillOnce(Invoke(grpc_stream_retry_timer_cb)); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); - // initial_resource_versions should contain client side all resource:version info, and an asterisk - // because this is a wildcard request. - expectSendMessage(type_url, {Wildcard}, {}, "", Grpc::Status::WellKnownGrpcStatus::Ok, "", + // initial_resource_versions should contain client side all resource:version info, and no + // added resources because this is a wildcard request. + expectSendMessage(type_url, {}, {}, "", Grpc::Status::WellKnownGrpcStatus::Ok, "", {{"x", "1000"}, {"y", "2000"}}); grpc_mux_->grpcStreamForTest().onRemoteClose(Grpc::Status::WellKnownGrpcStatus::Canceled, ""); - // Destruction of wildcard will issue an unsubscribe request for the resources. - expectSendMessage(type_url, {}, {Wildcard}); + // Destruction of wildcard will not issue unsubscribe requests for the resources. } // Test that we simply ignore a message for an unknown type_url, with no ill effects. From 4e346005aa26aeb81d941c1c177167366aa5f10d Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 8 Jul 2021 19:11:49 +0200 Subject: [PATCH 15/49] New attempt at implementing new wildcard subscriptions Split the resource cache into two categories - resources requested and resources from wildcard subscription. The former will be used for filling the initial request, the latter can be easily erased when unsubscribing from wildcard request. Also add a boolean for storing the legacy wildcard state. When support for it is dropped, it should be a matter of dropping the boolean member and the code that uses the it. Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 133 +++++++++--------- .../common/config/delta_subscription_state.h | 55 ++------ source/common/config/new_grpc_mux_impl.cc | 14 +- source/common/config/new_grpc_mux_impl.h | 8 +- .../config/delta_subscription_state_test.cc | 42 +++--- 5 files changed, 109 insertions(+), 143 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 1214e7f5b6bc..47d895bb552e 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -14,7 +14,7 @@ namespace Config { DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, - Event::Dispatcher& dispatcher, const bool wildcard) + Event::Dispatcher& dispatcher) // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on // empty resources as updates. : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), @@ -22,26 +22,30 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, [this](const auto& expired) { Protobuf::RepeatedPtrField removed_resources; for (const auto& resource : expired) { - if (auto maybe_resource = getResourceState(resource); maybe_resource.has_value()) { + if (auto maybe_resource = getRequestedResourceState(resource); + maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); removed_resources.Add(std::string(resource)); + } else if (auto erased_count = wildcard_resource_state_.erase(resource); + erased_count > 0) { + removed_resources.Add(std::string(resource)); } } watch_map_.onConfigUpdate({}, removed_resources, ""); }, dispatcher, dispatcher.timeSource()), - type_url_(std::move(type_url)), - mode_(wildcard ? WildcardMode::Implicit : WildcardMode::Disabled), watch_map_(watch_map), - local_info_(local_info), dispatcher_(dispatcher) {} + type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), + dispatcher_(dispatcher) {} void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { - // This adds a resource state that is waiting for the server for - // more information. - resource_state_.insert_or_assign(a, ResourceType::ExplicitlyRequested); + // This adds a resource state that is waiting for the server for more information. This also may + // be a wildcard resource, which is fine too. + requested_resource_state_.insert_or_assign(a, ResourceState()); + wildcard_resource_state_.erase(a); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -49,7 +53,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.insert(a); } for (const auto& r : cur_removed) { - removeResourceState(r); + requested_resource_state_.erase(r); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -58,30 +62,24 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.erase(r); names_removed_.insert(r); } - switch (mode_) { - case WildcardMode::Implicit: - if (names_removed_.find(Wildcard) != names_removed_.end()) { - // we explicitly cancel the wildcard subscription - mode_ = WildcardMode::Disabled; - } else if (!names_added_.empty()) { - // switch to explicit mode if we requested some extra names - mode_ = WildcardMode::Explicit; - } - break; - - case WildcardMode::Explicit: - if (names_removed_.find(Wildcard) != names_removed_.end()) { - // we explicitly cancel the wildcard subscription - mode_ = WildcardMode::Disabled; - } - break; - - case WildcardMode::Disabled: - if (names_added_.find(Wildcard) != names_added_.end()) { - // we switch into an explicit wildcard subscription - mode_ = WildcardMode::Explicit; + // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from + // cache. + if (cur_removed.contains(Wildcard)) { + wildcard_resource_state_.clear(); + } + // Check if this is a legacy wildcard subscription request. If we repeatedly call this function + // with empty cur_added and cur_removed, we keep the legacy wildcard subscription. + if (is_legacy_wildcard_) { + is_legacy_wildcard_ = cur_added.empty() && cur_removed.empty(); + } else { + // This is a legacy wildcard subscription if we have not expressed interest in any resources so + // far. Nor we tried to remove any resources. + is_legacy_wildcard_ = !any_request_sent_yet_in_current_stream_ && + requested_resource_state_.empty() && names_removed_.empty(); + if (is_legacy_wildcard_) { + requested_resource_state_.insert_or_assign(Wildcard, ResourceState()); + names_added_.insert(Wildcard); } - break; } } @@ -111,13 +109,20 @@ bool DeltaSubscriptionState::isHeartbeatResponse( !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { return false; } - const auto itr = resource_state_.find(resource.name()); - if (itr == resource_state_.end()) { + if (resource.has_resource()) { return false; } + const auto itr = requested_resource_state_.find(resource.name()); + if (itr != requested_resource_state_.end()) { + return !itr->second.waitingForServer() && resource.version() == itr->second.version(); + } - return !resource.has_resource() && !itr->second.waitingForServer() && - resource.version() == itr->second.version(); + const auto itr2 = wildcard_resource_state_.find(resource.name()); + if (itr2 != wildcard_resource_state_.end()) { + return resource.version() == itr2->second; + } + + return false; } void DeltaSubscriptionState::handleGoodResponse( @@ -168,10 +173,14 @@ void DeltaSubscriptionState::handleGoodResponse( // // So, leave the version map entry present but blank. It will be left out of // initial_resource_versions messages, but will remind us to explicitly tell the server "I'm - // cancelling my subscription" when we lose interest. + // cancelling my subscription" when we lose interest. In case of resources received as a part of + // the wildcard subscription, we just drop them. for (const auto& resource_name : message.removed_resources()) { - if (auto maybe_resource = getResourceState(resource_name); maybe_resource.has_value()) { + if (auto maybe_resource = getRequestedResourceState(resource_name); + maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); + } else { + wildcard_resource_state_.erase(resource_name); } } ENVOY_LOG(debug, "Delta config for {} accepted with {} resources added, {} removed", type_url_, @@ -200,27 +209,26 @@ DeltaSubscriptionState::getNextRequestAckless() { // initial_resource_versions "must be populated for first request in a stream". // Also, since this might be a new server, we must explicitly state *all* of our subscription // interest. - for (auto const& [resource_name, resource_state] : resource_state_) { + for (auto const& [resource_name, resource_state] : requested_resource_state_) { // Populate initial_resource_versions with the resource versions we currently have. // Resources we are interested in, but are still waiting to get any version of from the // server, do not belong in initial_resource_versions. (But do belong in new subscriptions!) if (!resource_state.waitingForServer()) { (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); } - // Add resource names to resource_names_subscribe only if this is not a wildcard subscription - // request or if we requested this resource explicitly (so we are actually in explicit - // wildcard mode). - if (mode_ == WildcardMode::Disabled || - resource_state.type() == ResourceType::ExplicitlyRequested) { - names_added_.insert(resource_name); - } + // We are going over a list of resources that we are interested in, so add them to + // resource_names_subscribe. + names_added_.insert(resource_name); } - // We are not clearing the names_added_ set. If we are in implicit wildcard subscription mode, - // then the set should already be empty. If we are in explicit wildcard mode then the set will - // contain the names we explicitly requested, but we need to add * to the list to make sure it's - // sent too. - if (mode_ == WildcardMode::Explicit) { - names_added_.insert(Wildcard); + for (auto const& [resource_name, resource_version] : wildcard_resource_state_) { + // Populate initial_resource_versions with the resource versions we currently have. + (*request.mutable_initial_resource_versions())[resource_name] = resource_version; + // We are not adding these resources to resource_names_subscribe. + } + // If this is a legacy wildcard request, then make sure that the resource_names_subscribe is + // empty. + if (is_legacy_wildcard_) { + names_added_.clear(); } names_removed_.clear(); } @@ -256,27 +264,24 @@ void DeltaSubscriptionState::addResourceStateFromServer( ttl_.clear(resource.name()); } - if (auto it = resource_state_.find(resource.name()); it != resource_state_.end()) { - auto old_type = it->second.type(); - it->second = ResourceState(resource, old_type); + if (auto it = requested_resource_state_.find(resource.name()); + it != requested_resource_state_.end()) { + // It is a resource that we requested. + it->second = ResourceState(resource); } else { - resource_state_.insert( - {resource.name(), ResourceState(resource, ResourceType::ReceivedFromServer)}); + // It is a resource that is a part of our wildcard request. + wildcard_resource_state_.insert({resource.name(), resource.version()}); } } OptRef -DeltaSubscriptionState::getResourceState(const std::string& resource_name) { - auto itr = resource_state_.find(resource_name); - if (itr == resource_state_.end()) { +DeltaSubscriptionState::getRequestedResourceState(const std::string& resource_name) { + auto itr = requested_resource_state_.find(resource_name); + if (itr == requested_resource_state_.end()) { return {}; } return {itr->second}; } -void DeltaSubscriptionState::removeResourceState(const std::string& resource_name) { - resource_state_.erase(resource_name); -} - } // namespace Config } // namespace Envoy diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index ef91509f3c7f..a59b483651a9 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -26,8 +26,7 @@ namespace Config { class DeltaSubscriptionState : public Logger::Loggable { public: DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, - const bool wildcard); + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); // Update which resources we're interested in subscribing to. void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, @@ -59,36 +58,15 @@ class DeltaSubscriptionState : public Logger::Loggable { void handleGoodResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); void handleBadResponse(const EnvoyException& e, UpdateAck& ack); - // This enumeration describes the resource type, which is only relevant for wildcard - // subscriptions. Depending on its type, the resource will or will not be resent on the initial - // wildcard subscription. - enum class ResourceType { - // Explicitly requested resource type means that we have asked about the resource by updating - // the subscription interest. Such resources are resent on the initial wildcard request. - ExplicitlyRequested, - // Received from server resources are resources that the state knows about only from the server - // response. Such resources are not resent on the initial wildcard request. - ReceivedFromServer, - }; - - // Determines the effective resource type. Explicitly requested type overrides the received from - // server type. - ResourceType effectiveResourceType(ResourceType old_type, ResourceType new_type) { - return (old_type == ResourceType::ReceivedFromServer) ? new_type : old_type; - } - class ResourceState { public: - ResourceState(absl::optional version, ResourceType type) - : version_(std::move(version)), type_(type) {} + ResourceState(absl::optional version) : version_(std::move(version)) {} - ResourceState(const envoy::service::discovery::v3::Resource& resource, ResourceType type) - : ResourceState(resource.version(), type) {} + ResourceState(const envoy::service::discovery::v3::Resource& resource) + : ResourceState(resource.version()) {} // Builds a ResourceState in the waitingForServer state. - ResourceState(ResourceType type) : ResourceState(absl::nullopt, type) {} - - ResourceType type() const { return type_; } + ResourceState() : ResourceState(absl::nullopt) {} // If true, we currently have no version of this resource - we are waiting for the server to // provide us with one. @@ -104,30 +82,19 @@ class DeltaSubscriptionState : public Logger::Loggable { private: absl::optional version_; - ResourceType type_; - }; - - // Describes the wildcard mode the subscription is in. - enum class WildcardMode { - // This mode is being expressed by sending a wildcard subscription request with an empty - // resource subscription list. - Implicit, - // This mode is being expressed by sending a wildcard subscription request that contains "*" - // special name in the resource subscription list. - Explicit, - // This mode is means no wildcard subscription. - Disabled, }; void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); - OptRef getResourceState(const std::string& resource_name); - void removeResourceState(const std::string& resource_name); + OptRef getRequestedResourceState(const std::string& resource_name); // A map from resource name to per-resource version. The keys of this map are exactly the resource // names we are currently interested in. Those in the waitingForServer state currently don't have // any version for that resource: we need to inform the server if we lose interest in them, but we // also need to *not* include them in the initial_resource_versions map upon a reconnect. - absl::node_hash_map resource_state_; + absl::node_hash_map requested_resource_state_; + // A map from resource name to per-resource version. The keys of this map are resource names we + // have received as a part of the wildcard subscription. + absl::node_hash_map wildcard_resource_state_; // Not all xDS resources supports heartbeats due to there being specific information encoded in // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just @@ -136,7 +103,6 @@ class DeltaSubscriptionState : public Logger::Loggable { TtlManager ttl_; const std::string type_url_; - WildcardMode mode_; UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; Event::Dispatcher& dispatcher_; @@ -144,6 +110,7 @@ class DeltaSubscriptionState : public Logger::Loggable { bool any_request_sent_yet_in_current_stream_{}; bool must_send_discovery_request_{}; + bool is_legacy_wildcard_{}; // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. diff --git a/source/common/config/new_grpc_mux_impl.cc b/source/common/config/new_grpc_mux_impl.cc index 78f9fe829ddf..6da0cf95271a 100644 --- a/source/common/config/new_grpc_mux_impl.cc +++ b/source/common/config/new_grpc_mux_impl.cc @@ -135,10 +135,7 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, auto entry = subscriptions_.find(type_url); if (entry == subscriptions_.end()) { // We don't yet have a subscription for type_url! Make one! - // No resources or an existence of the special name implies that - // this is a wildcard request subscription. - const bool wildcard = resources.empty() || (resources.find(Wildcard) != resources.end()); - addSubscription(type_url, options.use_namespace_matching_, wildcard); + addSubscription(type_url, options.use_namespace_matching_); return addWatch(type_url, resources, callbacks, resource_decoder, options); } @@ -210,11 +207,10 @@ void NewGrpcMuxImpl::removeWatch(const std::string& type_url, Watch* watch) { entry->second->watch_map_.removeWatch(watch); } -void NewGrpcMuxImpl::addSubscription(const std::string& type_url, const bool use_namespace_matching, - const bool wildcard) { - subscriptions_.emplace(type_url, std::make_unique(type_url, local_info_, - use_namespace_matching, - dispatcher_, wildcard)); +void NewGrpcMuxImpl::addSubscription(const std::string& type_url, + const bool use_namespace_matching) { + subscriptions_.emplace(type_url, std::make_unique( + type_url, local_info_, use_namespace_matching, dispatcher_)); subscription_ordering_.emplace_back(type_url); } diff --git a/source/common/config/new_grpc_mux_impl.h b/source/common/config/new_grpc_mux_impl.h index 4c2246fed813..44dd14a73c2d 100644 --- a/source/common/config/new_grpc_mux_impl.h +++ b/source/common/config/new_grpc_mux_impl.h @@ -73,10 +73,9 @@ class NewGrpcMuxImpl struct SubscriptionStuff { SubscriptionStuff(const std::string& type_url, const LocalInfo::LocalInfo& local_info, - const bool use_namespace_matching, Event::Dispatcher& dispatcher, - const bool wildcard) + const bool use_namespace_matching, Event::Dispatcher& dispatcher) : watch_map_(use_namespace_matching), - sub_state_(type_url, watch_map_, local_info, dispatcher, wildcard) {} + sub_state_(type_url, watch_map_, local_info, dispatcher) {} WatchMap watch_map_; DeltaSubscriptionState sub_state_; @@ -130,8 +129,7 @@ class NewGrpcMuxImpl const SubscriptionOptions& options); // Adds a subscription for the type_url to the subscriptions map and order list. - void addSubscription(const std::string& type_url, bool use_namespace_matching, - const bool wildcard); + void addSubscription(const std::string& type_url, bool use_namespace_matching); void trySendDiscoveryRequests(); diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 27db8965ca41..0638e3b389d5 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -30,10 +30,10 @@ const char TypeUrl[] = "type.googleapis.com/envoy.api.v2.Cluster"; class DeltaSubscriptionStateTestBase : public testing::Test { protected: DeltaSubscriptionStateTestBase( - const std::string& type_url, const bool wildcard, + const std::string& type_url, const absl::flat_hash_set initial_resources = {"name1", "name2", "name3"}) : timer_(new Event::MockTimer(&dispatcher_)), - state_(type_url, callbacks_, local_info_, dispatcher_, wildcard) { + state_(type_url, callbacks_, local_info_, dispatcher_) { state_.updateSubscriptionInterest(initial_resources, {}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); @@ -98,13 +98,13 @@ populateRepeatedResource(std::vector> items) class DeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, false) {} + DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl) {} }; // Delta subscription state of a wildcard subscription request. class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, true, {}) {} + WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, {}) {} }; // Basic gaining/losing interest in resources should lead to subscription updates. @@ -418,14 +418,14 @@ TEST_F(WildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect EXPECT_CALL(*timer_, disableTimer()); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - state_.updateSubscriptionInterest({"name3"}, {"name1"}); + state_.updateSubscriptionInterest({"name3"}, {}); state_.markStreamFresh(); // simulate a stream reconnection envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); // Regarding the resource_names_subscribe field: - // name1: do not include: we lost interest. + // name1: do not include: see below // name2: do not include: we are implicitly interested, but for wildcard it shouldn't be provided. // name3: yes do include: we are explicitly interested. - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("*", "name3")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre(Wildcard, "name3")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -439,17 +439,18 @@ TEST_F(WildcardDeltaSubscriptionStateTest, CancellingImplicitWildcardSubscriptio EXPECT_CALL(*timer_, disableTimer()); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - state_.updateSubscriptionInterest({"name3"}, {"name1", Wildcard}); + state_.updateSubscriptionInterest({"name3"}, {Wildcard}); envoy::service::discovery::v3::DeltaDiscoveryRequest cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); - EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", Wildcard)); + EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre(Wildcard)); state_.markStreamFresh(); // simulate a stream reconnection // Regarding the resource_names_subscribe field: - // name1: do not include: we lost interest. - // name2: yes do include: we are interested, and it's not wildcard. - // name3: yes do include: we are interested, and it's not wildcard. + // name1: do not include, see below + // name2: do not include: it came from wildcard subscription we lost interest in, so we are not + // interested in name2 too + // name3: yes do include: we are interested cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name2", "name3")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -468,19 +469,18 @@ TEST_F(WildcardDeltaSubscriptionStateTest, CancellingExplicitWildcardSubscriptio EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3")); // cancel wildcard subscription - state_.updateSubscriptionInterest({"name4"}, {"name1", "*"}); + state_.updateSubscriptionInterest({"name4"}, {Wildcard}); cur_request = state_.getNextRequestAckless(); EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name4")); - EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre("name1", "*")); + EXPECT_THAT(cur_request.resource_names_unsubscribe(), UnorderedElementsAre(Wildcard)); state_.markStreamFresh(); // simulate a stream reconnection // Regarding the resource_names_subscribe field: - // name1: do not include: we lost interest. - // name2: yes do include: we are interested, and it's not wildcard. - // name3: yes do include: we are interested, and it's not wildcard. + // name1: do not include: see name2 + // name2: do not include: it came as a part of wildcard subscription we cancelled, so we are not + // interested in this resource name3: yes do include: we are interested, and it's not wildcard. // name4: yes do include: we are interested, and it's not wildcard. cur_request = state_.getNextRequestAckless(); - EXPECT_THAT(cur_request.resource_names_subscribe(), - UnorderedElementsAre("name2", "name3", "name4")); + EXPECT_THAT(cur_request.resource_names_subscribe(), UnorderedElementsAre("name3", "name4")); EXPECT_TRUE(cur_request.resource_names_unsubscribe().empty()); } @@ -673,7 +673,7 @@ TEST_F(DeltaSubscriptionStateTest, ResourceTTL) { class VhdsDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: VhdsDeltaSubscriptionStateTest() - : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost", false) {} + : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost") {} }; TEST_F(VhdsDeltaSubscriptionStateTest, ResourceTTL) { From 18e9f70f517d4cd8e020db920fa603058e8299a3 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 14 Jul 2021 16:42:34 +0200 Subject: [PATCH 16/49] Formatting fixes Signed-off-by: Krzesimir Nowak --- test/common/config/delta_subscription_state_test.cc | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 3f95ae537a15..1272d6574dfd 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -39,11 +39,11 @@ class DeltaSubscriptionStateTestBase : public testing::TestWithParam( - type_url, callbacks_, dispatcher_); + state_ = std::make_unique(type_url, callbacks_, + dispatcher_); } else { - state_ = std::make_unique( - type_url, callbacks_, local_info_, dispatcher_); + state_ = std::make_unique(type_url, callbacks_, + local_info_, dispatcher_); } updateSubscriptionInterest(initial_resources, {}); auto cur_request = getNextRequestAckless(); @@ -160,8 +160,7 @@ INSTANTIATE_TEST_SUITE_P(DeltaSubscriptionStateTest, DeltaSubscriptionStateTest, // Delta subscription state of a wildcard subscription request. class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { public: - WildcardDeltaSubscriptionStateTest() - : DeltaSubscriptionStateTestBase(TypeUrl, GetParam(), {}) {} + WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, GetParam(), {}) {} }; INSTANTIATE_TEST_SUITE_P(WildcardDeltaSubscriptionStateTest, WildcardDeltaSubscriptionStateTest, From d6e273fd9a4fb47b5f244356582e1e13041640d2 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 14 Jul 2021 22:00:40 +0200 Subject: [PATCH 17/49] Update wildcard handling in xds delta subscription state This also moves the Wildcard constant to utility lib, so the xds delta subscription state can use it too. Signed-off-by: Krzesimir Nowak --- source/common/config/BUILD | 1 + .../common/config/delta_subscription_state.cc | 39 ++++-- .../common/config/delta_subscription_state.h | 15 ++- source/common/config/utility.cc | 2 + source/common/config/utility.h | 2 + source/common/config/watch_map.cc | 4 +- source/common/config/watch_map.h | 3 - .../xds_mux/delta_subscription_state.cc | 119 ++++++++++++++---- .../config/xds_mux/delta_subscription_state.h | 20 +-- .../config/delta_subscription_state_test.cc | 2 +- 10 files changed, 147 insertions(+), 60 deletions(-) diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 1890b921b93d..6ec9b90efe94 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -439,6 +439,7 @@ envoy_cc_library( hdrs = ["watch_map.h"], deps = [ ":decoded_resource_lib", + ":utility_lib", ":xds_resource_lib", "//envoy/config:subscription_interface", "//source/common/common:assert_lib", diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 47d895bb552e..8585271e59f0 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -44,7 +44,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( for (const auto& a : cur_added) { // This adds a resource state that is waiting for the server for more information. This also may // be a wildcard resource, which is fine too. - requested_resource_state_.insert_or_assign(a, ResourceState()); + requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); wildcard_resource_state_.erase(a); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its @@ -77,7 +77,10 @@ void DeltaSubscriptionState::updateSubscriptionInterest( is_legacy_wildcard_ = !any_request_sent_yet_in_current_stream_ && requested_resource_state_.empty() && names_removed_.empty(); if (is_legacy_wildcard_) { - requested_resource_state_.insert_or_assign(Wildcard, ResourceState()); + // Inserting wildcard to requested resource as waiting for server, which means that wildcard + // resource has no version and should never get one actually. As such, it won't be listed in + // initial_resource_versions field. + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); names_added_.insert(Wildcard); } } @@ -112,14 +115,15 @@ bool DeltaSubscriptionState::isHeartbeatResponse( if (resource.has_resource()) { return false; } - const auto itr = requested_resource_state_.find(resource.name()); - if (itr != requested_resource_state_.end()) { - return !itr->second.waitingForServer() && resource.version() == itr->second.version(); + + if (const auto maybe_resource = getRequestedResourceState(resource.name()); + maybe_resource.has_value()) { + return !maybe_resource->isWaitingForServer() && resource.version() == maybe_resource->version(); } - const auto itr2 = wildcard_resource_state_.find(resource.name()); - if (itr2 != wildcard_resource_state_.end()) { - return resource.version() == itr2->second; + if (const auto itr = wildcard_resource_state_.find(resource.name()); + itr != wildcard_resource_state_.end()) { + return resource.version() == itr->second; } return false; @@ -213,7 +217,7 @@ DeltaSubscriptionState::getNextRequestAckless() { // Populate initial_resource_versions with the resource versions we currently have. // Resources we are interested in, but are still waiting to get any version of from the // server, do not belong in initial_resource_versions. (But do belong in new subscriptions!) - if (!resource_state.waitingForServer()) { + if (!resource_state.isWaitingForServer()) { (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); } // We are going over a list of resources that we are interested in, so add them to @@ -264,10 +268,10 @@ void DeltaSubscriptionState::addResourceStateFromServer( ttl_.clear(resource.name()); } - if (auto it = requested_resource_state_.find(resource.name()); - it != requested_resource_state_.end()) { + if (auto maybe_resource = getRequestedResourceState(resource.name()); + maybe_resource.has_value()) { // It is a resource that we requested. - it->second = ResourceState(resource); + maybe_resource->setVersion(resource.version()); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); @@ -275,7 +279,16 @@ void DeltaSubscriptionState::addResourceStateFromServer( } OptRef -DeltaSubscriptionState::getRequestedResourceState(const std::string& resource_name) { +DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) { + auto itr = requested_resource_state_.find(resource_name); + if (itr == requested_resource_state_.end()) { + return {}; + } + return {itr->second}; +} + +OptRef +DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) const { auto itr = requested_resource_state_.find(resource_name); if (itr == requested_resource_state_.end()) { return {}; diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index a59b483651a9..1429c8b1120e 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -60,19 +60,17 @@ class DeltaSubscriptionState : public Logger::Loggable { class ResourceState { public: - ResourceState(absl::optional version) : version_(std::move(version)) {} - - ResourceState(const envoy::service::discovery::v3::Resource& resource) - : ResourceState(resource.version()) {} - // Builds a ResourceState in the waitingForServer state. - ResourceState() : ResourceState(absl::nullopt) {} + ResourceState() = default; + // Self-documenting alias of default constructor. + static ResourceState waitingForServer() { return ResourceState(); } // If true, we currently have no version of this resource - we are waiting for the server to // provide us with one. - bool waitingForServer() const { return version_ == absl::nullopt; } + bool isWaitingForServer() const { return version_ == absl::nullopt; } void setAsWaitingForServer() { version_ = absl::nullopt; } + void setVersion(absl::string_view version) { version_ = std::string(version); } // Must not be called if waitingForServer() == true. std::string version() const { @@ -85,7 +83,8 @@ class DeltaSubscriptionState : public Logger::Loggable { }; void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); - OptRef getRequestedResourceState(const std::string& resource_name); + OptRef getRequestedResourceState(absl::string_view resource_name); + OptRef getRequestedResourceState(absl::string_view resource_name) const; // A map from resource name to per-resource version. The keys of this map are exactly the resource // names we are currently interested in. Those in the waitingForServer state currently don't have diff --git a/source/common/config/utility.cc b/source/common/config/utility.cc index 89b752be1940..523eed6924d5 100644 --- a/source/common/config/utility.cc +++ b/source/common/config/utility.cc @@ -25,6 +25,8 @@ namespace Envoy { namespace Config { +const std::string Wildcard = "*"; + std::string Utility::truncateGrpcStatusMessage(absl::string_view error_message) { // GRPC sends error message via trailers, which by default has a 8KB size limit(see // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests). Truncates the diff --git a/source/common/config/utility.h b/source/common/config/utility.h index e4227ff06cae..93ef16def189 100644 --- a/source/common/config/utility.h +++ b/source/common/config/utility.h @@ -35,6 +35,8 @@ namespace Envoy { namespace Config { +extern const std::string Wildcard; + /** * Constant Api Type Values, used by envoy::config::core::v3::ApiConfigSource. */ diff --git a/source/common/config/watch_map.cc b/source/common/config/watch_map.cc index b0a8cfa96e35..f54d860f06af 100644 --- a/source/common/config/watch_map.cc +++ b/source/common/config/watch_map.cc @@ -5,14 +5,12 @@ #include "source/common/common/cleanup.h" #include "source/common/common/utility.h" #include "source/common/config/decoded_resource_impl.h" +#include "source/common/config/utility.h" #include "source/common/config/xds_resource.h" namespace Envoy { namespace Config { -const std::string Wildcard = "*"; -const absl::flat_hash_set WildcardSet = {Wildcard}; - namespace { // Returns the namespace part (if there's any) in the resource name. std::string namespaceFromName(const std::string& resource_name) { diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index eaf5fdc81320..d1139e23e0af 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -16,9 +16,6 @@ namespace Envoy { namespace Config { -extern const std::string Wildcard; -extern const absl::flat_hash_set WildcardSet; - struct AddedRemoved { AddedRemoved(absl::flat_hash_set&& added, absl::flat_hash_set&& removed) : added_(std::move(added)), removed_(std::move(removed)) {} diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index dd3b8e686cb7..bb79b830cfdb 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -13,11 +13,11 @@ namespace XdsMux { DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - Event::Dispatcher& dispatcher, const bool wildcard) + Event::Dispatcher& dispatcher) : BaseSubscriptionState(std::move(type_url), watch_map, dispatcher), // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on // empty resources as updates. - supports_heartbeats_(type_url_ != "envoy.config.route.v3.VirtualHost"), wildcard_(wildcard) {} + supports_heartbeats_(type_url_ != "envoy.config.route.v3.VirtualHost") {} DeltaSubscriptionState::~DeltaSubscriptionState() = default; @@ -25,7 +25,10 @@ void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { - resource_state_[a] = ResourceState::waitingForServer(); + // This adds a resource state that is waiting for the server for more information. This also may + // be a wildcard resource, which is fine too. + requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); + wildcard_resource_state_.erase(a); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -33,7 +36,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.insert(a); } for (const auto& r : cur_removed) { - resource_state_.erase(r); + requested_resource_state_.erase(r); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -42,6 +45,28 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.erase(r); names_removed_.insert(r); } + // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from + // cache. + if (cur_removed.contains(Wildcard)) { + wildcard_resource_state_.clear(); + } + // Check if this is a legacy wildcard subscription request. If we repeatedly call this function + // with empty cur_added and cur_removed, we keep the legacy wildcard subscription. + if (is_legacy_wildcard_) { + is_legacy_wildcard_ = cur_added.empty() && cur_removed.empty(); + } else { + // This is a legacy wildcard subscription if we have not expressed interest in any resources so + // far. Nor we tried to remove any resources. + is_legacy_wildcard_ = !any_request_sent_yet_in_current_stream_ && + requested_resource_state_.empty() && names_removed_.empty(); + if (is_legacy_wildcard_) { + // Inserting wildcard to requested resource as waiting for server, which means that wildcard + // resource has no version and should never get one actually. As such, it won't be listed in + // initial_resource_versions field. + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); + names_added_.insert(Wildcard); + } + } } // Not having sent any requests yet counts as an "update pending" since you're supposed to resend @@ -57,13 +82,21 @@ bool DeltaSubscriptionState::isHeartbeatResource( !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { return false; } - const auto itr = resource_state_.find(resource.name()); - if (itr == resource_state_.end()) { + if (resource.has_resource()) { return false; } - return !resource.has_resource() && !itr->second.isWaitingForServer() && - resource.version() == itr->second.version(); + if (const auto maybe_resource = getRequestedResourceState(resource.name()); + maybe_resource.has_value()) { + return !maybe_resource->isWaitingForServer() && resource.version() == maybe_resource->version(); + } + + if (const auto itr = wildcard_resource_state_.find(resource.name()); + itr != wildcard_resource_state_.end()) { + return resource.version() == itr->second; + } + + return false; } void DeltaSubscriptionState::handleGoodResponse( @@ -102,7 +135,7 @@ void DeltaSubscriptionState::handleGoodResponse( { const auto scoped_update = ttl_.scopedTtlUpdate(); for (const auto& resource : message.resources()) { - addResourceState(resource); + addResourceStateFromServer(resource); } } @@ -116,10 +149,14 @@ void DeltaSubscriptionState::handleGoodResponse( // // So, leave the version map entry present but blank. It will be left out of // initial_resource_versions messages, but will remind us to explicitly tell the server "I'm - // cancelling my subscription" when we lose interest. + // cancelling my subscription" when we lose interest. In case of resources received as a part of + // the wildcard subscription, we just drop them. for (const auto& resource_name : message.removed_resources()) { - if (resource_state_.find(resource_name) != resource_state_.end()) { - resource_state_[resource_name] = ResourceState::waitingForServer(); + if (auto maybe_resource = getRequestedResourceState(resource_name); + maybe_resource.has_value()) { + maybe_resource->setAsWaitingForServer(); + } else { + wildcard_resource_state_.erase(resource_name); } } ENVOY_LOG(debug, "Delta config for {} accepted with {} resources added, {} removed", typeUrl(), @@ -135,22 +172,25 @@ DeltaSubscriptionState::getNextRequestInternal() { // initial_resource_versions "must be populated for first request in a stream". // Also, since this might be a new server, we must explicitly state *all* of our subscription // interest. - for (auto const& [resource_name, resource_state] : resource_state_) { + for (auto const& [resource_name, resource_state] : requested_resource_state_) { // Populate initial_resource_versions with the resource versions we currently have. // Resources we are interested in, but are still waiting to get any version of from the // server, do not belong in initial_resource_versions. (But do belong in new subscriptions!) if (!resource_state.isWaitingForServer()) { (*request->mutable_initial_resource_versions())[resource_name] = resource_state.version(); } - // As mentioned above, fill resource_names_subscribe with everything, including names we - // have yet to receive any resource for unless this is a wildcard subscription, for which - // the first request on a stream must be without any resource names. - if (!wildcard_) { - names_added_.insert(resource_name); - } + // We are going over a list of resources that we are interested in, so add them to + // resource_names_subscribe. + names_added_.insert(resource_name); + } + for (auto const& [resource_name, resource_version] : wildcard_resource_state_) { + // Populate initial_resource_versions with the resource versions we currently have. + (*request->mutable_initial_resource_versions())[resource_name] = resource_version; + // We are not adding these resources to resource_names_subscribe. } - // Wildcard subscription initial requests must have no resource_names_subscribe. - if (wildcard_) { + // If this is a legacy wildcard request, then make sure that the resource_names_subscribe is + // empty. + if (is_legacy_wildcard_) { names_added_.clear(); } names_removed_.clear(); @@ -166,10 +206,17 @@ DeltaSubscriptionState::getNextRequestInternal() { return request; } -void DeltaSubscriptionState::addResourceState( +void DeltaSubscriptionState::addResourceStateFromServer( const envoy::service::discovery::v3::Resource& resource) { setResourceTtl(resource); - resource_state_[resource.name()] = ResourceState(resource.version()); + if (auto maybe_resource = getRequestedResourceState(resource.name()); + maybe_resource.has_value()) { + // It is a resource that we requested. + maybe_resource->setVersion(resource.version()); + } else { + // It is a resource that is a part of our wildcard request. + wildcard_resource_state_.insert({resource.name(), resource.version()}); + } } void DeltaSubscriptionState::setResourceTtl( @@ -185,12 +232,34 @@ void DeltaSubscriptionState::setResourceTtl( void DeltaSubscriptionState::ttlExpiryCallback(const std::vector& expired) { Protobuf::RepeatedPtrField removed_resources; for (const auto& resource : expired) { - resource_state_[resource] = ResourceState::waitingForServer(); - removed_resources.Add(std::string(resource)); + if (auto maybe_resource = getRequestedResourceState(resource); maybe_resource.has_value()) { + maybe_resource->setAsWaitingForServer(); + removed_resources.Add(std::string(resource)); + } else if (auto erased_count = wildcard_resource_state_.erase(resource); erased_count > 0) { + removed_resources.Add(std::string(resource)); + } } callbacks().onConfigUpdate({}, removed_resources, ""); } +OptRef +DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) { + auto itr = requested_resource_state_.find(resource_name); + if (itr == requested_resource_state_.end()) { + return {}; + } + return {itr->second}; +} + +OptRef +DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) const { + auto itr = requested_resource_state_.find(resource_name); + if (itr == requested_resource_state_.end()) { + return {}; + } + return {itr->second}; +} + } // namespace XdsMux } // namespace Config } // namespace Envoy diff --git a/source/common/config/xds_mux/delta_subscription_state.h b/source/common/config/xds_mux/delta_subscription_state.h index 801bd5edc0c1..34d329535721 100644 --- a/source/common/config/xds_mux/delta_subscription_state.h +++ b/source/common/config/xds_mux/delta_subscription_state.h @@ -20,7 +20,7 @@ class DeltaSubscriptionState envoy::service::discovery::v3::DeltaDiscoveryRequest> { public: DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - Event::Dispatcher& dispatcher, const bool wildcard); + Event::Dispatcher& dispatcher); ~DeltaSubscriptionState() override; @@ -46,11 +46,10 @@ class DeltaSubscriptionState bool isHeartbeatResource(const envoy::service::discovery::v3::Resource& resource) const; void handleGoodResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) override; - void addResourceState(const envoy::service::discovery::v3::Resource& resource); + void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); class ResourceState { public: - explicit ResourceState(absl::string_view version) : version_(version) {} // Builds a ResourceVersion in the waitingForServer state. ResourceState() = default; // Self-documenting alias of default constructor. @@ -60,6 +59,9 @@ class DeltaSubscriptionState // provide us with one. bool isWaitingForServer() const { return version_ == absl::nullopt; } + void setAsWaitingForServer() { version_ = absl::nullopt; } + void setVersion(absl::string_view version) { version_ = std::string(version); } + // Must not be called if waitingForServer() == true. std::string version() const { ASSERT(version_.has_value()); @@ -70,21 +72,25 @@ class DeltaSubscriptionState absl::optional version_; }; + OptRef getRequestedResourceState(absl::string_view resource_name); + OptRef getRequestedResourceState(absl::string_view resource_name) const; + // Not all xDS resources support heartbeats due to there being specific information encoded in // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just // disable heartbeats for these resources (currently only VHDS). const bool supports_heartbeats_; - // Is the subscription is for a wildcard request. - const bool wildcard_; - // A map from resource name to per-resource version. The keys of this map are exactly the resource // names we are currently interested in. Those in the waitingForServer state currently don't have // any version for that resource: we need to inform the server if we lose interest in them, but we // also need to *not* include them in the initial_resource_versions map upon a reconnect. - absl::node_hash_map resource_state_; + absl::node_hash_map requested_resource_state_; + // A map from resource name to per-resource version. The keys of this map are resource names we + // have received as a part of the wildcard subscription. + absl::node_hash_map wildcard_resource_state_; bool any_request_sent_yet_in_current_stream_{}; + bool is_legacy_wildcard_{}; // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 1272d6574dfd..4f8623db88f4 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -540,7 +540,7 @@ TEST_P(WildcardDeltaSubscriptionStateTest, CancellingExplicitWildcardSubscriptio // Check that resource changes from being interested in implicitly to explicitly when we update the // subscription interest. Such resources will show up in the initial wildcard requests // too. Receiving the update on such resource will not change their interest mode. -TEST_F(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { +TEST_P(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { Protobuf::RepeatedPtrField add1_2_a = populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); EXPECT_CALL(*ttl_timer_, disableTimer()).Times(2); From db884fe1732a0a1b76f1c6caf064b474e1265b5d Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 16 Jul 2021 14:20:02 +0200 Subject: [PATCH 18/49] Change Wildcard into constexpr string view Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 2 +- source/common/config/utility.cc | 2 -- source/common/config/utility.h | 2 +- source/common/config/xds_mux/delta_subscription_state.cc | 2 +- test/common/config/delta_subscription_state_test.cc | 7 ++++--- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 8585271e59f0..7c65427e23f9 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -81,7 +81,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // resource has no version and should never get one actually. As such, it won't be listed in // initial_resource_versions field. requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - names_added_.insert(Wildcard); + names_added_.emplace(Wildcard); } } } diff --git a/source/common/config/utility.cc b/source/common/config/utility.cc index 523eed6924d5..89b752be1940 100644 --- a/source/common/config/utility.cc +++ b/source/common/config/utility.cc @@ -25,8 +25,6 @@ namespace Envoy { namespace Config { -const std::string Wildcard = "*"; - std::string Utility::truncateGrpcStatusMessage(absl::string_view error_message) { // GRPC sends error message via trailers, which by default has a 8KB size limit(see // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests). Truncates the diff --git a/source/common/config/utility.h b/source/common/config/utility.h index 93ef16def189..1bc27099029b 100644 --- a/source/common/config/utility.h +++ b/source/common/config/utility.h @@ -35,7 +35,7 @@ namespace Envoy { namespace Config { -extern const std::string Wildcard; +constexpr absl::string_view Wildcard = "*"; /** * Constant Api Type Values, used by envoy::config::core::v3::ApiConfigSource. diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index bb79b830cfdb..67ac68ff5891 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -64,7 +64,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // resource has no version and should never get one actually. As such, it won't be listed in // initial_resource_versions field. requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - names_added_.insert(Wildcard); + names_added_.emplace(Wildcard); } } } diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 4f8623db88f4..10fdb539bb05 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -29,6 +29,7 @@ namespace { const char TypeUrl[] = "type.googleapis.com/envoy.api.v2.Cluster"; enum class LegacyOrUnified { Legacy, Unified }; +const auto WildcardStr = std::string(Wildcard); class DeltaSubscriptionStateTestBase : public testing::TestWithParam { protected: @@ -409,7 +410,7 @@ TEST_P(DeltaSubscriptionStateTest, SwitchIntoWildcardMode) { deliverDiscoveryResponse(add1_2, {}, "debugversion1"); // switch into wildcard mode - updateSubscriptionInterest({"name4", Wildcard}, {"name1"}); + updateSubscriptionInterest({"name4", WildcardStr}, {"name1"}); markStreamFresh(); // simulate a stream reconnection auto cur_request = getNextRequestAckless(); // Regarding the resource_names_subscribe field: @@ -492,7 +493,7 @@ TEST_P(WildcardDeltaSubscriptionStateTest, CancellingImplicitWildcardSubscriptio EXPECT_CALL(*ttl_timer_, disableTimer()); deliverDiscoveryResponse(add1_2, {}, "debugversion1"); - updateSubscriptionInterest({"name3"}, {Wildcard}); + updateSubscriptionInterest({"name3"}, {WildcardStr}); auto cur_request = getNextRequestAckless(); EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name3")); EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre(Wildcard)); @@ -522,7 +523,7 @@ TEST_P(WildcardDeltaSubscriptionStateTest, CancellingExplicitWildcardSubscriptio EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name3")); // cancel wildcard subscription - updateSubscriptionInterest({"name4"}, {Wildcard}); + updateSubscriptionInterest({"name4"}, {WildcardStr}); cur_request = getNextRequestAckless(); EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4")); EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre(Wildcard)); From cb444312964621f2f8b1e03b2f8d005672be72b3 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 5 Aug 2021 16:17:08 +0200 Subject: [PATCH 19/49] Rework legacy wildcard subscription handling Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 173 ++++++++-- .../common/config/delta_subscription_state.h | 9 +- .../xds_mux/delta_subscription_state.cc | 172 ++++++++-- .../config/xds_mux/delta_subscription_state.h | 9 +- .../config/delta_subscription_state_test.cc | 320 ++++++++++++++++-- 5 files changed, 591 insertions(+), 92 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 7c65427e23f9..2c635281fadf 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -11,6 +11,16 @@ namespace Envoy { namespace Config { +namespace { + +// Usable with maps and sets. Used for ASSERTs, to avoid too much typing. +template +bool container_contains(const Container& container, const Key& key) { + return container.find(key) != container.end(); +} + +} // namespace + DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, @@ -26,7 +36,8 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); removed_resources.Add(std::string(resource)); - } else if (auto erased_count = wildcard_resource_state_.erase(resource); + } else if (auto erased_count = wildcard_resource_state_.erase(resource) + + ambiguous_resource_state_.erase(resource); erased_count > 0) { removed_resources.Add(std::string(resource)); } @@ -42,10 +53,24 @@ void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { - // This adds a resource state that is waiting for the server for more information. This also may - // be a wildcard resource, which is fine too. - requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); - wildcard_resource_state_.erase(a); + if (in_initial_legacy_wildcard_ && a != Wildcard) { + in_initial_legacy_wildcard_ = false; + } + // If the requested resource existed as a wildcard resource, + // transition it to requested. Otherwise mark it as a resource + // waiting for the server to receive the version. + if (auto it = wildcard_resource_state_.find(a); it != wildcard_resource_state_.end()) { + requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); + wildcard_resource_state_.erase(it); + } else if (it = ambiguous_resource_state_.find(a); it != ambiguous_resource_state_.end()) { + requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); + ambiguous_resource_state_.erase(it); + } else { + requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); + } + ASSERT(container_contains(requested_resource_state_, a)); + ASSERT(!container_contains(wildcard_resource_state_, a)); + ASSERT(!container_contains(ambiguous_resource_state_, a)); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -53,7 +78,26 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.insert(a); } for (const auto& r : cur_removed) { - requested_resource_state_.erase(r); + in_initial_legacy_wildcard_ = false; + // The resource we are interested in could also come from a wildcard subscription. Instead of + // removing it outright, mark the resource as not interesting to us any more. The server could + // later send us an update. If we don't have a wildcard subscription, just drop it. + if (auto it = requested_resource_state_.find(Wildcard); it != requested_resource_state_.end()) { + if (it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { + // Wildcard resources always have a version. If our requested resource has no version, it + // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version + // attached to it. + if (!it->second.isWaitingForServer()) { + ambiguous_resource_state_.insert({it->first, it->second.version()}); + } + requested_resource_state_.erase(it); + } + } else { + requested_resource_state_.erase(r); + } + ASSERT(!container_contains(requested_resource_state_, r)); + // This function shouldn't ever be called for resources that came from wildcard subscription. + ASSERT(!container_contains(wildcard_resource_state_, r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -63,34 +107,45 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_removed_.insert(r); } // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from - // cache. + // cache. Also drop the ambiguous resources - we aren't interested in those, but we didn't know if + // those came from wildcard subscription or not, but now it's not important any more. if (cur_removed.contains(Wildcard)) { wildcard_resource_state_.clear(); - } - // Check if this is a legacy wildcard subscription request. If we repeatedly call this function - // with empty cur_added and cur_removed, we keep the legacy wildcard subscription. - if (is_legacy_wildcard_) { - is_legacy_wildcard_ = cur_added.empty() && cur_removed.empty(); - } else { - // This is a legacy wildcard subscription if we have not expressed interest in any resources so - // far. Nor we tried to remove any resources. - is_legacy_wildcard_ = !any_request_sent_yet_in_current_stream_ && - requested_resource_state_.empty() && names_removed_.empty(); - if (is_legacy_wildcard_) { - // Inserting wildcard to requested resource as waiting for server, which means that wildcard - // resource has no version and should never get one actually. As such, it won't be listed in - // initial_resource_versions field. - requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - names_added_.emplace(Wildcard); - } + ambiguous_resource_state_.clear(); } } // Not having sent any requests yet counts as an "update pending" since you're supposed to resend // the entirety of your interest at the start of a stream, even if nothing has changed. bool DeltaSubscriptionState::subscriptionUpdatePending() const { - return !names_added_.empty() || !names_removed_.empty() || - !any_request_sent_yet_in_current_stream_ || must_send_discovery_request_; + if (!names_added_.empty() || !names_removed_.empty()) { + return true; + } + // At this point, we have no new resources to subscribe to or any + // resources to unsubscribe from. + if (!any_request_sent_yet_in_current_stream_) { + // If we haven't sent anything on the current stream, but we are actually interested in some + // resource then we obviously need to let the server know about those. + if (!requested_resource_state_.empty()) { + return true; + } + // So there are no new names and we are interested in nothing. This may either mean that we want + // the legacy wildcard subscription to kick in or we actually unsubscribed from everything. If + // the latter is true, then we should not be sending any requests. In such case the initial + // wildcard mode will be false. Otherwise it means that the legacy wildcard request should be + // sent. + return in_initial_legacy_wildcard_; + } + + // At this point, we have no changes in subscription resources and this isn't a first request in + // the stream, so even if there are no resources we are interested in, we can send the request, + // because even if it's empty, it won't be interpreted as legacy wildcard subscription, which can + // only for the first request in the stream. So sending an empty request at this point should be + // harmless. + // + // If sending empty requests at this point is actually harmful, we would need to add "&& + // !requested_resource_state_.empty()" to the return below. + return must_send_discovery_request_; } UpdateAck DeltaSubscriptionState::handleResponse( @@ -126,6 +181,14 @@ bool DeltaSubscriptionState::isHeartbeatResponse( return resource.version() == itr->second; } + if (const auto itr = ambiguous_resource_state_.find(resource.name()); + itr != wildcard_resource_state_.end()) { + // In theory we should move the ambiguous resource to wildcard, because probably we shouldn't be + // getting hearbeat responses about resources that we are not interested in, but the server + // could have sent this heartbeat before it learned about our lack of interest in the resource. + return resource.version() == itr->second; + } + return false; } @@ -175,15 +238,17 @@ void DeltaSubscriptionState::handleGoodResponse( // case saying nothing is fine, or the server will bring back something new, which we should // receive regardless (which is the logic that not specifying a version will get you). // - // So, leave the version map entry present but blank. It will be left out of - // initial_resource_versions messages, but will remind us to explicitly tell the server "I'm - // cancelling my subscription" when we lose interest. In case of resources received as a part of - // the wildcard subscription, we just drop them. + // So, leave the version map entry present but blank if we are still interested in the resource. + // It will be left out of initial_resource_versions messages, but will remind us to explicitly + // tell the server "I'm cancelling my subscription" when we lose interest. In case of resources + // received as a part of the wildcard subscription or resources we already lost interest in, we + // just drop them. for (const auto& resource_name : message.removed_resources()) { if (auto maybe_resource = getRequestedResourceState(resource_name); maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); - } else { + } else if (auto erased_count = ambiguous_resource_state_.erase(resource_name); + erased_count == 0) { wildcard_resource_state_.erase(resource_name); } } @@ -210,6 +275,36 @@ DeltaSubscriptionState::getNextRequestAckless() { must_send_discovery_request_ = false; if (!any_request_sent_yet_in_current_stream_) { any_request_sent_yet_in_current_stream_ = true; + bool is_legacy_wildcard = in_initial_legacy_wildcard_; + if (is_legacy_wildcard) { + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); + ASSERT(container_contains(requested_resource_state_, Wildcard)); + ASSERT(!container_contains(wildcard_resource_state_, Wildcard)); + ASSERT(!container_contains(ambiguous_resource_state_, Wildcard)); + } else { + // If we are here, this means that we lost our initial wildcard mode, because we subscribed to + // something in the past. We could still be in the situation now that all we are subscribed to + // now is wildcard resource, so in such case try to send a legacy wildcard subscription + // request anyway. For this to happen, two conditions need to apply: + // + // 1. No change in interest. + // 2. The only requested resource is Wildcard resource. + // + // The invariant of the code here is that this code is executed only when + // subscriptionUpdatePending actually returns true, which in our case can only happen if the + // requested resources state_ isn't empty. + ASSERT(!requested_resource_state_.empty()); + + // If our subscription interest didn't change then the first condition for using legacy + // wildcard subscription is met. + is_legacy_wildcard = names_added_.empty() && names_removed_.empty(); + if (is_legacy_wildcard) { + // If we requested only a wildcard resource then the second condition for using legacy + // wildcard condition is met. + is_legacy_wildcard = requested_resource_state_.size() == 1 && + requested_resource_state_.begin()->first == Wildcard; + } + } // initial_resource_versions "must be populated for first request in a stream". // Also, since this might be a new server, we must explicitly state *all* of our subscription // interest. @@ -225,13 +320,14 @@ DeltaSubscriptionState::getNextRequestAckless() { names_added_.insert(resource_name); } for (auto const& [resource_name, resource_version] : wildcard_resource_state_) { - // Populate initial_resource_versions with the resource versions we currently have. (*request.mutable_initial_resource_versions())[resource_name] = resource_version; - // We are not adding these resources to resource_names_subscribe. + } + for (auto const& [resource_name, resource_version] : ambiguous_resource_state_) { + (*request.mutable_initial_resource_versions())[resource_name] = resource_version; } // If this is a legacy wildcard request, then make sure that the resource_names_subscribe is // empty. - if (is_legacy_wildcard_) { + if (is_legacy_wildcard) { names_added_.clear(); } names_removed_.clear(); @@ -272,9 +368,18 @@ void DeltaSubscriptionState::addResourceStateFromServer( maybe_resource.has_value()) { // It is a resource that we requested. maybe_resource->setVersion(resource.version()); + ASSERT(container_contains(requested_resource_state_, resource.name())); + ASSERT(!container_contains(wildcard_resource_state_, resource.name())); + ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); + // The resource could be ambiguous before, but now the ambiguity + // is resolved. + ambiguous_resource_state_.erase(resource.name()); + ASSERT(!container_contains(requested_resource_state_, resource.name())); + ASSERT(container_contains(wildcard_resource_state_, resource.name())); + ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); } } diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 6d5fd0cd240d..50685edc5ec8 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -61,8 +61,12 @@ class DeltaSubscriptionState : public Logger::Loggable { public: // Builds a ResourceState in the waitingForServer state. ResourceState() = default; + // Builds a ResourceState with a specific version + ResourceState(absl::string_view version) : version_(version) {} // Self-documenting alias of default constructor. static ResourceState waitingForServer() { return ResourceState(); } + // Self-documenting alias of constructor with version. + static ResourceState withVersion(absl::string_view version) { return ResourceState(version); } // If true, we currently have no version of this resource - we are waiting for the server to // provide us with one. @@ -93,6 +97,9 @@ class DeltaSubscriptionState : public Logger::Loggable { // A map from resource name to per-resource version. The keys of this map are resource names we // have received as a part of the wildcard subscription. absl::node_hash_map wildcard_resource_state_; + // Used for storing resources that we lost interest in, but could + // also be a part of wildcard subscription. + absl::node_hash_map ambiguous_resource_state_; // Not all xDS resources supports heartbeats due to there being specific information encoded in // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just @@ -106,9 +113,9 @@ class DeltaSubscriptionState : public Logger::Loggable { Event::Dispatcher& dispatcher_; std::chrono::milliseconds init_fetch_timeout_; + bool in_initial_legacy_wildcard_{true}; bool any_request_sent_yet_in_current_stream_{}; bool must_send_discovery_request_{}; - bool is_legacy_wildcard_{}; // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 67ac68ff5891..39c4310f464c 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -11,6 +11,16 @@ namespace Envoy { namespace Config { namespace XdsMux { +namespace { + +// Usable with maps and sets. Used for ASSERTs, to avoid too much typing. +template +bool container_contains(const Container& container, const Key& key) { + return container.find(key) != container.end(); +} + +} // namespace + DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, Event::Dispatcher& dispatcher) @@ -25,10 +35,24 @@ void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { - // This adds a resource state that is waiting for the server for more information. This also may - // be a wildcard resource, which is fine too. - requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); - wildcard_resource_state_.erase(a); + if (in_initial_legacy_wildcard_ && a != Wildcard) { + in_initial_legacy_wildcard_ = false; + } + // If the requested resource existed as a wildcard resource, + // transition it to requested. Otherwise mark it as a resource + // waiting for the server to receive the version. + if (auto it = wildcard_resource_state_.find(a); it != wildcard_resource_state_.end()) { + requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); + wildcard_resource_state_.erase(it); + } else if (it = ambiguous_resource_state_.find(a); it != ambiguous_resource_state_.end()) { + requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); + ambiguous_resource_state_.erase(it); + } else { + requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); + } + ASSERT(container_contains(requested_resource_state_, a)); + ASSERT(!container_contains(wildcard_resource_state_, a)); + ASSERT(!container_contains(ambiguous_resource_state_, a)); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -36,7 +60,26 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.insert(a); } for (const auto& r : cur_removed) { - requested_resource_state_.erase(r); + in_initial_legacy_wildcard_ = false; + // The resource we are interested in could also come from a wildcard subscription. Instead of + // removing it outright, mark the resource as not interesting to us any more. The server could + // later send us an update. If we don't have a wildcard subscription, just drop it. + if (auto it = requested_resource_state_.find(Wildcard); it != requested_resource_state_.end()) { + if (it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { + // Wildcard resources always have a version. If our requested resource has no version, it + // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version + // attached to it. + if (!it->second.isWaitingForServer()) { + ambiguous_resource_state_.insert({it->first, it->second.version()}); + } + requested_resource_state_.erase(it); + } + } else { + requested_resource_state_.erase(r); + } + ASSERT(!container_contains(requested_resource_state_, r)); + // This function shouldn't ever be called for resources that came from wildcard subscription. + ASSERT(!container_contains(wildcard_resource_state_, r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -49,31 +92,41 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // cache. if (cur_removed.contains(Wildcard)) { wildcard_resource_state_.clear(); - } - // Check if this is a legacy wildcard subscription request. If we repeatedly call this function - // with empty cur_added and cur_removed, we keep the legacy wildcard subscription. - if (is_legacy_wildcard_) { - is_legacy_wildcard_ = cur_added.empty() && cur_removed.empty(); - } else { - // This is a legacy wildcard subscription if we have not expressed interest in any resources so - // far. Nor we tried to remove any resources. - is_legacy_wildcard_ = !any_request_sent_yet_in_current_stream_ && - requested_resource_state_.empty() && names_removed_.empty(); - if (is_legacy_wildcard_) { - // Inserting wildcard to requested resource as waiting for server, which means that wildcard - // resource has no version and should never get one actually. As such, it won't be listed in - // initial_resource_versions field. - requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - names_added_.emplace(Wildcard); - } + ambiguous_resource_state_.clear(); } } // Not having sent any requests yet counts as an "update pending" since you're supposed to resend // the entirety of your interest at the start of a stream, even if nothing has changed. bool DeltaSubscriptionState::subscriptionUpdatePending() const { - return !names_added_.empty() || !names_removed_.empty() || - !any_request_sent_yet_in_current_stream_ || dynamicContextChanged(); + if (!names_added_.empty() || !names_removed_.empty()) { + return true; + } + // At this point, we have no new resources to subscribe to or any + // resources to unsubscribe from. + if (!any_request_sent_yet_in_current_stream_) { + // If we haven't sent anything on the current stream, but we are actually interested in some + // resource then we obviously need to let the server know about those. + if (!requested_resource_state_.empty()) { + return true; + } + // So there are no new names and we are interested in nothing. This may either mean that we want + // the legacy wildcard subscription to kick in or we actually unsubscribed from everything. If + // the latter is true, then we should not be sending any requests. In such case the initial + // wildcard mode will be false. Otherwise it means that the legacy wildcard request should be + // sent. + return in_initial_legacy_wildcard_; + } + + // At this point, we have no changes in subscription resources and this isn't a first request in + // the stream, so even if there are no resources we are interested in, we can send the request, + // because even if it's empty, it won't be interpreted as legacy wildcard subscription, which can + // only for the first request in the stream. So sending an empty request at this point should be + // harmless. + // + // If sending empty requests at this point is actually harmful, we would need to add "&& + // !requested_resource_state_.empty()" to the return below. + return dynamicContextChanged(); } bool DeltaSubscriptionState::isHeartbeatResource( @@ -96,6 +149,14 @@ bool DeltaSubscriptionState::isHeartbeatResource( return resource.version() == itr->second; } + if (const auto itr = ambiguous_resource_state_.find(resource.name()); + itr != wildcard_resource_state_.end()) { + // In theory we should move the ambiguous resource to wildcard, because probably we shouldn't be + // getting hearbeat responses about resources that we are not interested in, but the server + // could have sent this heartbeat before it learned about our lack of interest in the resource. + return resource.version() == itr->second; + } + return false; } @@ -147,15 +208,17 @@ void DeltaSubscriptionState::handleGoodResponse( // case saying nothing is fine, or the server will bring back something new, which we should // receive regardless (which is the logic that not specifying a version will get you). // - // So, leave the version map entry present but blank. It will be left out of - // initial_resource_versions messages, but will remind us to explicitly tell the server "I'm - // cancelling my subscription" when we lose interest. In case of resources received as a part of - // the wildcard subscription, we just drop them. + // So, leave the version map entry present but blank if we are still interested in the resource. + // It will be left out of initial_resource_versions messages, but will remind us to explicitly + // tell the server "I'm cancelling my subscription" when we lose interest. In case of resources + // received as a part of the wildcard subscription or resources we already lost interest in, we + // just drop them. for (const auto& resource_name : message.removed_resources()) { if (auto maybe_resource = getRequestedResourceState(resource_name); maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); - } else { + } else if (auto erased_count = ambiguous_resource_state_.erase(resource_name); + erased_count == 0) { wildcard_resource_state_.erase(resource_name); } } @@ -169,6 +232,36 @@ DeltaSubscriptionState::getNextRequestInternal() { request->set_type_url(typeUrl()); if (!any_request_sent_yet_in_current_stream_) { any_request_sent_yet_in_current_stream_ = true; + bool is_legacy_wildcard = in_initial_legacy_wildcard_; + if (is_legacy_wildcard) { + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); + ASSERT(container_contains(requested_resource_state_, Wildcard)); + ASSERT(!container_contains(wildcard_resource_state_, Wildcard)); + ASSERT(!container_contains(ambiguous_resource_state_, Wildcard)); + } else { + // If we are here, this means that we lost our initial wildcard mode, because we subscribed to + // something in the past. We could still be in the situation now that all we are subscribed to + // now is wildcard resource, so in such case try to send a legacy wildcard subscription + // request anyway. For this to happen, two conditions need to apply: + // + // 1. No change in interest. + // 2. The only requested resource is Wildcard resource. + // + // The invariant of the code here is that this code is executed only when + // subscriptionUpdatePending actually returns true, which in our case can only happen if the + // requested resources state_ isn't empty. + ASSERT(!requested_resource_state_.empty()); + + // If our subscription interest didn't change then the first condition for using legacy + // wildcard subscription is met. + is_legacy_wildcard = names_added_.empty() && names_removed_.empty(); + if (is_legacy_wildcard) { + // If we requested only a wildcard resource then the second condition for using legacy + // wildcard condition is met. + is_legacy_wildcard = requested_resource_state_.size() == 1 && + requested_resource_state_.begin()->first == Wildcard; + } + } // initial_resource_versions "must be populated for first request in a stream". // Also, since this might be a new server, we must explicitly state *all* of our subscription // interest. @@ -184,13 +277,14 @@ DeltaSubscriptionState::getNextRequestInternal() { names_added_.insert(resource_name); } for (auto const& [resource_name, resource_version] : wildcard_resource_state_) { - // Populate initial_resource_versions with the resource versions we currently have. (*request->mutable_initial_resource_versions())[resource_name] = resource_version; - // We are not adding these resources to resource_names_subscribe. + } + for (auto const& [resource_name, resource_version] : ambiguous_resource_state_) { + (*request->mutable_initial_resource_versions())[resource_name] = resource_version; } // If this is a legacy wildcard request, then make sure that the resource_names_subscribe is // empty. - if (is_legacy_wildcard_) { + if (is_legacy_wildcard) { names_added_.clear(); } names_removed_.clear(); @@ -209,13 +303,23 @@ DeltaSubscriptionState::getNextRequestInternal() { void DeltaSubscriptionState::addResourceStateFromServer( const envoy::service::discovery::v3::Resource& resource) { setResourceTtl(resource); + if (auto maybe_resource = getRequestedResourceState(resource.name()); maybe_resource.has_value()) { // It is a resource that we requested. maybe_resource->setVersion(resource.version()); + ASSERT(container_contains(requested_resource_state_, resource.name())); + ASSERT(!container_contains(wildcard_resource_state_, resource.name())); + ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); + // The resource could be ambiguous before, but now the ambiguity + // is resolved. + ambiguous_resource_state_.erase(resource.name()); + ASSERT(!container_contains(requested_resource_state_, resource.name())); + ASSERT(container_contains(wildcard_resource_state_, resource.name())); + ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); } } @@ -235,7 +339,9 @@ void DeltaSubscriptionState::ttlExpiryCallback(const std::vector& e if (auto maybe_resource = getRequestedResourceState(resource); maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); removed_resources.Add(std::string(resource)); - } else if (auto erased_count = wildcard_resource_state_.erase(resource); erased_count > 0) { + } else if (auto erased_count = wildcard_resource_state_.erase(resource) + + ambiguous_resource_state_.erase(resource); + erased_count > 0) { removed_resources.Add(std::string(resource)); } } diff --git a/source/common/config/xds_mux/delta_subscription_state.h b/source/common/config/xds_mux/delta_subscription_state.h index 34d329535721..7d889d65c987 100644 --- a/source/common/config/xds_mux/delta_subscription_state.h +++ b/source/common/config/xds_mux/delta_subscription_state.h @@ -52,8 +52,12 @@ class DeltaSubscriptionState public: // Builds a ResourceVersion in the waitingForServer state. ResourceState() = default; + // Builds a ResourceState with a specific version + ResourceState(absl::string_view version) : version_(version) {} // Self-documenting alias of default constructor. static ResourceState waitingForServer() { return ResourceState(); } + // Self-documenting alias of constructor with version. + static ResourceState withVersion(absl::string_view version) { return ResourceState(version); } // If true, we currently have no version of this resource - we are waiting for the server to // provide us with one. @@ -88,9 +92,12 @@ class DeltaSubscriptionState // A map from resource name to per-resource version. The keys of this map are resource names we // have received as a part of the wildcard subscription. absl::node_hash_map wildcard_resource_state_; + // Used for storing resources that we lost interest in, but could + // also be a part of wildcard subscription. + absl::node_hash_map ambiguous_resource_state_; + bool in_initial_legacy_wildcard_{true}; bool any_request_sent_yet_in_current_stream_{}; - bool is_legacy_wildcard_{}; // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 10fdb539bb05..cc16384f7935 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -19,6 +19,7 @@ using testing::IsSubstring; using testing::NiceMock; +using testing::Pair; using testing::Throw; using testing::UnorderedElementsAre; using testing::UnorderedElementsAreArray; @@ -31,11 +32,29 @@ const char TypeUrl[] = "type.googleapis.com/envoy.api.v2.Cluster"; enum class LegacyOrUnified { Legacy, Unified }; const auto WildcardStr = std::string(Wildcard); +Protobuf::RepeatedPtrField +populateRepeatedResource(std::vector> items) { + Protobuf::RepeatedPtrField add_to; + for (const auto& item : items) { + auto* resource = add_to.Add(); + resource->set_name(item.first); + resource->set_version(item.second); + } + return add_to; +} + +Protobuf::RepeatedPtrField populateRepeatedString(std::vector items) { + Protobuf::RepeatedPtrField add_to; + for (const auto& item : items) { + auto* str = add_to.Add(); + *str = item; + } + return add_to; +} + class DeltaSubscriptionStateTestBase : public testing::TestWithParam { protected: - DeltaSubscriptionStateTestBase( - const std::string& type_url, LegacyOrUnified legacy_or_unified, - const absl::flat_hash_set initial_resources = {"name1", "name2", "name3"}) + DeltaSubscriptionStateTestBase(const std::string& type_url, LegacyOrUnified legacy_or_unified) : should_use_unified_(legacy_or_unified == LegacyOrUnified::Unified) { ttl_timer_ = new Event::MockTimer(&dispatcher_); @@ -46,11 +65,6 @@ class DeltaSubscriptionStateTestBase : public testing::TestWithParam(type_url, callbacks_, local_info_, dispatcher_); } - updateSubscriptionInterest(initial_resources, {}); - auto cur_request = getNextRequestAckless(); - EXPECT_THAT(cur_request->resource_names_subscribe(), - // UnorderedElementsAre("name1", "name2", "name3")); - UnorderedElementsAreArray(initial_resources.cbegin(), initial_resources.cend())); } void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, @@ -113,6 +127,16 @@ class DeltaSubscriptionStateTestBase : public testing::TestWithParam> added_resources, + std::vector removed_resources, + const std::string& version_info) { + EXPECT_CALL(*ttl_timer_, disableTimer()); + auto add = populateRepeatedResource(added_resources); + auto remove = populateRepeatedString(removed_resources); + return deliverDiscoveryResponse(add, remove, version_info); + } + void markStreamFresh() { if (should_use_unified_) { absl::get<1>(state_)->markStreamFresh(); @@ -139,29 +163,243 @@ class DeltaSubscriptionStateTestBase : public testing::TestWithParam -populateRepeatedResource(std::vector> items) { - Protobuf::RepeatedPtrField add_to; - for (const auto& item : items) { - auto* resource = add_to.Add(); - resource->set_name(item.first); - resource->set_version(item.second); - } - return add_to; +class DeltaSubscriptionStateTestBlank : public DeltaSubscriptionStateTestBase { +public: + DeltaSubscriptionStateTestBlank() : DeltaSubscriptionStateTestBase(TypeUrl, GetParam()) {} +}; + +INSTANTIATE_TEST_SUITE_P(DeltaSubscriptionStateTestBlank, DeltaSubscriptionStateTestBlank, + testing::ValuesIn({LegacyOrUnified::Legacy, LegacyOrUnified::Unified})); + +// Checks if subscriptionUpdatePending returns correct value depending on scenario. +TEST_P(DeltaSubscriptionStateTestBlank, SubscriptionPendingTest) { + // We should send a request, because nothing has been sent out yet. + EXPECT_TRUE(subscriptionUpdatePending()); + getNextRequestAckless(); + + // We should not be sending any requests if nothing yet changed since last time we sent a + // request. Or if out subscription interest was not modified. + EXPECT_FALSE(subscriptionUpdatePending()); + updateSubscriptionInterest({}, {}); + EXPECT_FALSE(subscriptionUpdatePending()); + + // We should send a request, because our interest changed (we are interested in foo now). + updateSubscriptionInterest({"foo"}, {}); + EXPECT_TRUE(subscriptionUpdatePending()); + getNextRequestAckless(); + + // We should send a request after a new stream is established if we are interested in some + // resource. + EXPECT_FALSE(subscriptionUpdatePending()); + markStreamFresh(); + EXPECT_TRUE(subscriptionUpdatePending()); + getNextRequestAckless(); + + // We should send a request, because our interest changed (we are not interested in foo and in + // wildcard resource any more). + EXPECT_FALSE(subscriptionUpdatePending()); + updateSubscriptionInterest({}, {WildcardStr, "foo"}); + EXPECT_TRUE(subscriptionUpdatePending()); + getNextRequestAckless(); + + // We should not be sending anything after stream reestablishing, because we are not interested in + // anything. + markStreamFresh(); + EXPECT_FALSE(subscriptionUpdatePending()); } -class DeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { +TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionNonWildcardFromRequestedToDropped) { + updateSubscriptionInterest({"foo", "bar"}, {}); + auto req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo", "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_TRUE(req->initial_resource_versions().empty()); + + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"bar", "1"}}, {}, "d1"); + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo", "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("bar", "1"))); + + updateSubscriptionInterest({}, {"foo"}); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_THAT(req->resource_names_unsubscribe(), UnorderedElementsAre("foo")); + deliverSimpleDiscoveryResponse({}, {"foo"}, "d2"); + + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), UnorderedElementsAre(Pair("bar", "1"))); +} + +// Check if we keep foo resource in cache even if we lost interest in it. It could be a part of the +// wildcard subscription. +TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromRequestedToAmbiguous) { + // subscribe to foo and make sure we have it. + updateSubscriptionInterest({WildcardStr, "foo", "bar"}, {}); + auto req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "foo", "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_TRUE(req->initial_resource_versions().empty()); + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"bar", "1"}, {"wild1", "1"}}, {}, "d1"); + + // ensure that foo is a part of resource versions + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "foo", "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("bar", "1"), Pair("wild1", "1"))); + + // unsubscribe from foo just before the stream breaks, make sure we still send the foo initial + // version + updateSubscriptionInterest({}, {"foo"}); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_THAT(req->resource_names_unsubscribe(), UnorderedElementsAre("foo")); + // didn't receive a reply + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("bar", "1"), Pair("wild1", "1"))); +} + +// Check that foo and bar do not appear in initial versions after we lost interest. Foo won't +// appear, because we got a reply from server confirming dropping the resource. Bar won't appear +// because we never got a reply from server with a version of it. +TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromRequestedToDropped) { + // subscribe to foo and bar and make sure we have it. + updateSubscriptionInterest({WildcardStr, "foo", "bar", "baz"}, {}); + auto req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), + UnorderedElementsAre(WildcardStr, "foo", "bar", "baz")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_TRUE(req->initial_resource_versions().empty()); + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"baz", "1"}, {"wild1", "1"}}, {}, "d1"); + + // ensure that foo is a part of resource versions, bar won't be, because we don't have its version + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), + UnorderedElementsAre(WildcardStr, "foo", "bar", "baz")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("baz", "1"), Pair("wild1", "1"))); + + // unsubscribe from foo and bar, and receive an confirmation about dropping foo. Now neither will + // appear in initial versions in the initial request after breaking the stream. + updateSubscriptionInterest({}, {"foo", "bar"}); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_THAT(req->resource_names_unsubscribe(), UnorderedElementsAre("foo", "bar")); + deliverSimpleDiscoveryResponse({}, {"foo"}, "d2"); + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "baz")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("baz", "1"), Pair("wild1", "1"))); +} + +// Check that foo and bar do not appear in initial versions after we lost interest. Foo won't +// appear, because we got a reply from server confirming dropping the resource. Bar won't appear +// because we never got a reply from server with a version of it. +TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromWildcardToRequested) { + updateSubscriptionInterest({}, {}); + auto req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_TRUE(req->initial_resource_versions().empty()); + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"wild1", "1"}}, {}, "d1"); + + updateSubscriptionInterest({"foo"}, {}); + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "foo")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("wild1", "1"))); +} + +// Check that foo and bar do not appear in initial versions after we lost interest. Foo won't +// appear, because we got a reply from server confirming dropping the resource. Bar won't appear +// because we never got a reply from server with a version of it. +TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromAmbiguousToRequested) { + updateSubscriptionInterest({WildcardStr, "foo"}, {}); + auto req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "foo")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_TRUE(req->initial_resource_versions().empty()); + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"wild1", "1"}}, {}, "d1"); + + // make foo ambiguous and request it again + updateSubscriptionInterest({}, {"foo"}); + updateSubscriptionInterest({"foo"}, {}); + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "foo")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("wild1", "1"))); +} + +TEST_P(DeltaSubscriptionStateTestBlank, LegacyWildcardInitialRequests) { + updateSubscriptionInterest({}, {}); + auto req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + deliverSimpleDiscoveryResponse({{"wild1", "1"}}, {}, "d1"); + + updateSubscriptionInterest({"foo"}, {}); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + deliverSimpleDiscoveryResponse({{"foo", "1"}}, {}, "d1"); + updateSubscriptionInterest({}, {"foo"}); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_THAT(req->resource_names_unsubscribe(), UnorderedElementsAre("foo")); + deliverSimpleDiscoveryResponse({}, {"foo"}, "d1"); + + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); +} + +class DeltaSubscriptionStateTestWithResources : public DeltaSubscriptionStateTestBase { +protected: + DeltaSubscriptionStateTestWithResources( + const std::string& type_url, LegacyOrUnified legacy_or_unified, + const absl::flat_hash_set initial_resources = {"name1", "name2", "name3"}) + : DeltaSubscriptionStateTestBase(type_url, legacy_or_unified) { + updateSubscriptionInterest(initial_resources, {}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), + // UnorderedElementsAre("name1", "name2", "name3")); + UnorderedElementsAreArray(initial_resources.cbegin(), initial_resources.cend())); + } +}; + +class DeltaSubscriptionStateTest : public DeltaSubscriptionStateTestWithResources { public: - DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, GetParam()) {} + DeltaSubscriptionStateTest() : DeltaSubscriptionStateTestWithResources(TypeUrl, GetParam()) {} }; INSTANTIATE_TEST_SUITE_P(DeltaSubscriptionStateTest, DeltaSubscriptionStateTest, testing::ValuesIn({LegacyOrUnified::Legacy, LegacyOrUnified::Unified})); // Delta subscription state of a wildcard subscription request. -class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { +class WildcardDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestWithResources { public: - WildcardDeltaSubscriptionStateTest() : DeltaSubscriptionStateTestBase(TypeUrl, GetParam(), {}) {} + WildcardDeltaSubscriptionStateTest() + : DeltaSubscriptionStateTestWithResources(TypeUrl, GetParam(), {}) {} }; INSTANTIATE_TEST_SUITE_P(WildcardDeltaSubscriptionStateTest, WildcardDeltaSubscriptionStateTest, @@ -579,6 +817,42 @@ TEST_P(WildcardDeltaSubscriptionStateTest, ExplicitInterestOverridesImplicit) { EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); } +// Check that resource changes from being interested in implicitly to explicitly when we update the +// subscription interest. Such resources will show up in the initial wildcard requests +// too. Receiving the update on such resource will not change their interest mode. +TEST_P(WildcardDeltaSubscriptionStateTest, ResetToLegacyWildcardBehaviorOnStreamReset) { + // verify that we will send the legacy wildcard subscription request + // after stream reset + updateSubscriptionInterest({"resource"}, {}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("resource")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + updateSubscriptionInterest({}, {"resource"}); + cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("resource")); + markStreamFresh(); // simulate a stream reconnection + cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + + // verify that we will send the legacy wildcard subscription request + // after stream reset and confirming our subscription interest + updateSubscriptionInterest({"resource"}, {}); + cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("resource")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + updateSubscriptionInterest({}, {"resource"}); + cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("resource")); + markStreamFresh(); // simulate a stream reconnection + updateSubscriptionInterest({}, {}); + cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); +} + // initial_resource_versions should not be present on messages after the first in a stream. TEST_P(DeltaSubscriptionStateTest, InitialVersionMapFirstMessageOnly) { // First, verify that the first message of a new stream sends initial versions. @@ -748,10 +1022,10 @@ TEST_P(DeltaSubscriptionStateTest, TypeUrlMismatch) { handleResponse(message); } -class VhdsDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestBase { +class VhdsDeltaSubscriptionStateTest : public DeltaSubscriptionStateTestWithResources { public: VhdsDeltaSubscriptionStateTest() - : DeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost", GetParam()) {} + : DeltaSubscriptionStateTestWithResources("envoy.config.route.v3.VirtualHost", GetParam()) {} }; INSTANTIATE_TEST_SUITE_P(VhdsDeltaSubscriptionStateTest, VhdsDeltaSubscriptionStateTest, From 9a1855da9d54de0774cc518b2bfe7229176c05d9 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 12 Aug 2021 20:22:43 +0200 Subject: [PATCH 20/49] Fix typos Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 2 +- source/common/config/xds_mux/delta_subscription_state.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 2c635281fadf..87b3b018c02a 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -184,7 +184,7 @@ bool DeltaSubscriptionState::isHeartbeatResponse( if (const auto itr = ambiguous_resource_state_.find(resource.name()); itr != wildcard_resource_state_.end()) { // In theory we should move the ambiguous resource to wildcard, because probably we shouldn't be - // getting hearbeat responses about resources that we are not interested in, but the server + // getting heartbeat responses about resources that we are not interested in, but the server // could have sent this heartbeat before it learned about our lack of interest in the resource. return resource.version() == itr->second; } diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 39c4310f464c..83d184071c0f 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -152,7 +152,7 @@ bool DeltaSubscriptionState::isHeartbeatResource( if (const auto itr = ambiguous_resource_state_.find(resource.name()); itr != wildcard_resource_state_.end()) { // In theory we should move the ambiguous resource to wildcard, because probably we shouldn't be - // getting hearbeat responses about resources that we are not interested in, but the server + // getting heartbeat responses about resources that we are not interested in, but the server // could have sent this heartbeat before it learned about our lack of interest in the resource. return resource.version() == itr->second; } From e71317f6ccd7467175ded5c9e47297fc916fb945 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 12 Aug 2021 21:45:12 +0200 Subject: [PATCH 21/49] Rename function to placate clang tidy Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 30 +++++++++---------- .../xds_mux/delta_subscription_state.cc | 30 +++++++++---------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 87b3b018c02a..4dfdb3dac9af 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -15,7 +15,7 @@ namespace { // Usable with maps and sets. Used for ASSERTs, to avoid too much typing. template -bool container_contains(const Container& container, const Key& key) { +bool containerContains(const Container& container, const Key& key) { return container.find(key) != container.end(); } @@ -68,9 +68,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); } - ASSERT(container_contains(requested_resource_state_, a)); - ASSERT(!container_contains(wildcard_resource_state_, a)); - ASSERT(!container_contains(ambiguous_resource_state_, a)); + ASSERT(containerContains(requested_resource_state_, a)); + ASSERT(!containerContains(wildcard_resource_state_, a)); + ASSERT(!containerContains(ambiguous_resource_state_, a)); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -95,9 +95,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.erase(r); } - ASSERT(!container_contains(requested_resource_state_, r)); + ASSERT(!containerContains(requested_resource_state_, r)); // This function shouldn't ever be called for resources that came from wildcard subscription. - ASSERT(!container_contains(wildcard_resource_state_, r)); + ASSERT(!containerContains(wildcard_resource_state_, r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -278,9 +278,9 @@ DeltaSubscriptionState::getNextRequestAckless() { bool is_legacy_wildcard = in_initial_legacy_wildcard_; if (is_legacy_wildcard) { requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(container_contains(requested_resource_state_, Wildcard)); - ASSERT(!container_contains(wildcard_resource_state_, Wildcard)); - ASSERT(!container_contains(ambiguous_resource_state_, Wildcard)); + ASSERT(containerContains(requested_resource_state_, Wildcard)); + ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); + ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); } else { // If we are here, this means that we lost our initial wildcard mode, because we subscribed to // something in the past. We could still be in the situation now that all we are subscribed to @@ -368,18 +368,18 @@ void DeltaSubscriptionState::addResourceStateFromServer( maybe_resource.has_value()) { // It is a resource that we requested. maybe_resource->setVersion(resource.version()); - ASSERT(container_contains(requested_resource_state_, resource.name())); - ASSERT(!container_contains(wildcard_resource_state_, resource.name())); - ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); + ASSERT(containerContains(requested_resource_state_, resource.name())); + ASSERT(!containerContains(wildcard_resource_state_, resource.name())); + ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); // The resource could be ambiguous before, but now the ambiguity // is resolved. ambiguous_resource_state_.erase(resource.name()); - ASSERT(!container_contains(requested_resource_state_, resource.name())); - ASSERT(container_contains(wildcard_resource_state_, resource.name())); - ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); + ASSERT(!containerContains(requested_resource_state_, resource.name())); + ASSERT(containerContains(wildcard_resource_state_, resource.name())); + ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); } } diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 83d184071c0f..516ca6550438 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -15,7 +15,7 @@ namespace { // Usable with maps and sets. Used for ASSERTs, to avoid too much typing. template -bool container_contains(const Container& container, const Key& key) { +bool containerContains(const Container& container, const Key& key) { return container.find(key) != container.end(); } @@ -50,9 +50,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); } - ASSERT(container_contains(requested_resource_state_, a)); - ASSERT(!container_contains(wildcard_resource_state_, a)); - ASSERT(!container_contains(ambiguous_resource_state_, a)); + ASSERT(containerContains(requested_resource_state_, a)); + ASSERT(!containerContains(wildcard_resource_state_, a)); + ASSERT(!containerContains(ambiguous_resource_state_, a)); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -77,9 +77,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.erase(r); } - ASSERT(!container_contains(requested_resource_state_, r)); + ASSERT(!containerContains(requested_resource_state_, r)); // This function shouldn't ever be called for resources that came from wildcard subscription. - ASSERT(!container_contains(wildcard_resource_state_, r)); + ASSERT(!containerContains(wildcard_resource_state_, r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -235,9 +235,9 @@ DeltaSubscriptionState::getNextRequestInternal() { bool is_legacy_wildcard = in_initial_legacy_wildcard_; if (is_legacy_wildcard) { requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(container_contains(requested_resource_state_, Wildcard)); - ASSERT(!container_contains(wildcard_resource_state_, Wildcard)); - ASSERT(!container_contains(ambiguous_resource_state_, Wildcard)); + ASSERT(containerContains(requested_resource_state_, Wildcard)); + ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); + ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); } else { // If we are here, this means that we lost our initial wildcard mode, because we subscribed to // something in the past. We could still be in the situation now that all we are subscribed to @@ -308,18 +308,18 @@ void DeltaSubscriptionState::addResourceStateFromServer( maybe_resource.has_value()) { // It is a resource that we requested. maybe_resource->setVersion(resource.version()); - ASSERT(container_contains(requested_resource_state_, resource.name())); - ASSERT(!container_contains(wildcard_resource_state_, resource.name())); - ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); + ASSERT(containerContains(requested_resource_state_, resource.name())); + ASSERT(!containerContains(wildcard_resource_state_, resource.name())); + ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); // The resource could be ambiguous before, but now the ambiguity // is resolved. ambiguous_resource_state_.erase(resource.name()); - ASSERT(!container_contains(requested_resource_state_, resource.name())); - ASSERT(container_contains(wildcard_resource_state_, resource.name())); - ASSERT(!container_contains(ambiguous_resource_state_, resource.name())); + ASSERT(!containerContains(requested_resource_state_, resource.name())); + ASSERT(containerContains(wildcard_resource_state_, resource.name())); + ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); } } From cb93839a2b1ad277a559b2077b9d3c569c9d7170 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 13 Aug 2021 13:24:41 +0200 Subject: [PATCH 22/49] Try to improve coverage Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 4 +- .../xds_mux/delta_subscription_state.cc | 4 +- .../config/delta_subscription_state_test.cc | 56 +++++++++++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 4dfdb3dac9af..fb93ec2b9c56 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -82,8 +82,8 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // The resource we are interested in could also come from a wildcard subscription. Instead of // removing it outright, mark the resource as not interesting to us any more. The server could // later send us an update. If we don't have a wildcard subscription, just drop it. - if (auto it = requested_resource_state_.find(Wildcard); it != requested_resource_state_.end()) { - if (it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { + if (containerContains(requested_resource_state_, Wildcard)) { + if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { // Wildcard resources always have a version. If our requested resource has no version, it // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version // attached to it. diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 516ca6550438..9a85ce1df779 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -64,8 +64,8 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // The resource we are interested in could also come from a wildcard subscription. Instead of // removing it outright, mark the resource as not interesting to us any more. The server could // later send us an update. If we don't have a wildcard subscription, just drop it. - if (auto it = requested_resource_state_.find(Wildcard); it != requested_resource_state_.end()) { - if (it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { + if (containerContains(requested_resource_state_, Wildcard)) { + if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { // Wildcard resources always have a version. If our requested resource has no version, it // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version // attached to it. diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 3d3ab111a5ff..2e067f149f17 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -373,6 +373,62 @@ TEST_P(DeltaSubscriptionStateTestBlank, LegacyWildcardInitialRequests) { EXPECT_TRUE(req->resource_names_unsubscribe().empty()); } +TEST_P(DeltaSubscriptionStateTestBlank, AmbiguousResourceTTL) { + Event::SimulatedTimeSystem time_system; + time_system.setSystemTime(std::chrono::milliseconds(0)); + + auto create_resource_with_ttl = [](absl::string_view name, absl::string_view version, + absl::optional ttl_s, + bool include_resource) { + Protobuf::RepeatedPtrField added_resources; + auto* resource = added_resources.Add(); + resource->set_name(std::string(name)); + resource->set_version(std::string(version)); + + if (include_resource) { + resource->mutable_resource(); + } + + if (ttl_s) { + ProtobufWkt::Duration ttl; + ttl.set_seconds(ttl_s->count()); + resource->mutable_ttl()->CopyFrom(ttl); + } + + return added_resources; + }; + + updateSubscriptionInterest({WildcardStr, "foo"}, {}); + auto req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre(WildcardStr, "foo")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + { + EXPECT_CALL(*ttl_timer_, enabled()); + EXPECT_CALL(*ttl_timer_, enableTimer(std::chrono::milliseconds(1000), _)); + deliverDiscoveryResponse(create_resource_with_ttl("foo", "1", std::chrono::seconds(1), true), + {}, "debug1", "nonce1"); + } + + // make foo ambiguous + updateSubscriptionInterest({}, {"foo"}); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_THAT(req->resource_names_unsubscribe(), UnorderedElementsAre("foo")); + { + // Refresh the TTL with a heartbeat. The resource should not be passed to the update callbacks. + EXPECT_CALL(*ttl_timer_, enabled()); + deliverDiscoveryResponse(create_resource_with_ttl("foo", "1", std::chrono::seconds(1), false), + {}, "debug1", "nonce1", true, 0); + } + + EXPECT_CALL(callbacks_, onConfigUpdate(_, _, _)); + EXPECT_CALL(*ttl_timer_, disableTimer()); + time_system.setSystemTime(std::chrono::seconds(2)); + + // Invoke the TTL. + ttl_timer_->invokeCallback(); +} + class DeltaSubscriptionStateTestWithResources : public DeltaSubscriptionStateTestBase { protected: DeltaSubscriptionStateTestWithResources( From b7dee94624b3f86de027876dd5867a3fbf8e3f4f Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 13 Aug 2021 16:35:12 +0200 Subject: [PATCH 23/49] Factor out legacy wildcard checks to a separate function Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 64 ++++++++++--------- .../common/config/delta_subscription_state.h | 2 + .../xds_mux/delta_subscription_state.cc | 64 ++++++++++--------- .../config/xds_mux/delta_subscription_state.h | 2 + 4 files changed, 72 insertions(+), 60 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index fb93ec2b9c56..4395a196e531 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -275,36 +275,7 @@ DeltaSubscriptionState::getNextRequestAckless() { must_send_discovery_request_ = false; if (!any_request_sent_yet_in_current_stream_) { any_request_sent_yet_in_current_stream_ = true; - bool is_legacy_wildcard = in_initial_legacy_wildcard_; - if (is_legacy_wildcard) { - requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(containerContains(requested_resource_state_, Wildcard)); - ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); - ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); - } else { - // If we are here, this means that we lost our initial wildcard mode, because we subscribed to - // something in the past. We could still be in the situation now that all we are subscribed to - // now is wildcard resource, so in such case try to send a legacy wildcard subscription - // request anyway. For this to happen, two conditions need to apply: - // - // 1. No change in interest. - // 2. The only requested resource is Wildcard resource. - // - // The invariant of the code here is that this code is executed only when - // subscriptionUpdatePending actually returns true, which in our case can only happen if the - // requested resources state_ isn't empty. - ASSERT(!requested_resource_state_.empty()); - - // If our subscription interest didn't change then the first condition for using legacy - // wildcard subscription is met. - is_legacy_wildcard = names_added_.empty() && names_removed_.empty(); - if (is_legacy_wildcard) { - // If we requested only a wildcard resource then the second condition for using legacy - // wildcard condition is met. - is_legacy_wildcard = requested_resource_state_.size() == 1 && - requested_resource_state_.begin()->first == Wildcard; - } - } + const bool is_legacy_wildcard = isInitialRequestForLegacyWildcard(); // initial_resource_versions "must be populated for first request in a stream". // Also, since this might be a new server, we must explicitly state *all* of our subscription // interest. @@ -344,6 +315,39 @@ DeltaSubscriptionState::getNextRequestAckless() { return request; } +bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { + if (in_initial_legacy_wildcard_) { + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); + ASSERT(containerContains(requested_resource_state_, Wildcard)); + ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); + ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); + return true; + } + + // If we are here, this means that we lost our initial wildcard mode, because we subscribed to + // something in the past. We could still be in the situation now that all we are subscribed to now + // is wildcard resource, so in such case try to send a legacy wildcard subscription request + // anyway. For this to happen, two conditions need to apply: + // + // 1. No change in interest. + // 2. The only requested resource is Wildcard resource. + // + // The invariant of the code here is that this code is executed only when + // subscriptionUpdatePending actually returns true, which in our case can only happen if the + // requested resources state_ isn't empty. + ASSERT(!requested_resource_state_.empty()); + + // If our subscription interest didn't change then the first condition for using legacy wildcard + // subscription is met. + if (!names_added_.empty() || !names_removed_.empty()) { + return false; + } + // If we requested only a wildcard resource then the second condition for using legacy wildcard + // condition is met. + return requested_resource_state_.size() == 1 && + requested_resource_state_.begin()->first == Wildcard; +} + envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { envoy::service::discovery::v3::DeltaDiscoveryRequest request = getNextRequestAckless(); diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 50685edc5ec8..a7707324e55b 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -89,6 +89,8 @@ class DeltaSubscriptionState : public Logger::Loggable { OptRef getRequestedResourceState(absl::string_view resource_name); OptRef getRequestedResourceState(absl::string_view resource_name) const; + bool isInitialRequestForLegacyWildcard(); + // A map from resource name to per-resource version. The keys of this map are exactly the resource // names we are currently interested in. Those in the waitingForServer state currently don't have // any version for that resource: we need to inform the server if we lose interest in them, but we diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 9a85ce1df779..1ffd648186dc 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -232,36 +232,7 @@ DeltaSubscriptionState::getNextRequestInternal() { request->set_type_url(typeUrl()); if (!any_request_sent_yet_in_current_stream_) { any_request_sent_yet_in_current_stream_ = true; - bool is_legacy_wildcard = in_initial_legacy_wildcard_; - if (is_legacy_wildcard) { - requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(containerContains(requested_resource_state_, Wildcard)); - ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); - ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); - } else { - // If we are here, this means that we lost our initial wildcard mode, because we subscribed to - // something in the past. We could still be in the situation now that all we are subscribed to - // now is wildcard resource, so in such case try to send a legacy wildcard subscription - // request anyway. For this to happen, two conditions need to apply: - // - // 1. No change in interest. - // 2. The only requested resource is Wildcard resource. - // - // The invariant of the code here is that this code is executed only when - // subscriptionUpdatePending actually returns true, which in our case can only happen if the - // requested resources state_ isn't empty. - ASSERT(!requested_resource_state_.empty()); - - // If our subscription interest didn't change then the first condition for using legacy - // wildcard subscription is met. - is_legacy_wildcard = names_added_.empty() && names_removed_.empty(); - if (is_legacy_wildcard) { - // If we requested only a wildcard resource then the second condition for using legacy - // wildcard condition is met. - is_legacy_wildcard = requested_resource_state_.size() == 1 && - requested_resource_state_.begin()->first == Wildcard; - } - } + const bool is_legacy_wildcard = isInitialRequestForLegacyWildcard(); // initial_resource_versions "must be populated for first request in a stream". // Also, since this might be a new server, we must explicitly state *all* of our subscription // interest. @@ -300,6 +271,39 @@ DeltaSubscriptionState::getNextRequestInternal() { return request; } +bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { + if (in_initial_legacy_wildcard_) { + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); + ASSERT(containerContains(requested_resource_state_, Wildcard)); + ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); + ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); + return true; + } + + // If we are here, this means that we lost our initial wildcard mode, because we subscribed to + // something in the past. We could still be in the situation now that all we are subscribed to now + // is wildcard resource, so in such case try to send a legacy wildcard subscription request + // anyway. For this to happen, two conditions need to apply: + // + // 1. No change in interest. + // 2. The only requested resource is Wildcard resource. + // + // The invariant of the code here is that this code is executed only when + // subscriptionUpdatePending actually returns true, which in our case can only happen if the + // requested resources state_ isn't empty. + ASSERT(!requested_resource_state_.empty()); + + // If our subscription interest didn't change then the first condition for using legacy wildcard + // subscription is met. + if (!names_added_.empty() || !names_removed_.empty()) { + return false; + } + // If we requested only a wildcard resource then the second condition for using legacy wildcard + // condition is met. + return requested_resource_state_.size() == 1 && + requested_resource_state_.begin()->first == Wildcard; +} + void DeltaSubscriptionState::addResourceStateFromServer( const envoy::service::discovery::v3::Resource& resource) { setResourceTtl(resource); diff --git a/source/common/config/xds_mux/delta_subscription_state.h b/source/common/config/xds_mux/delta_subscription_state.h index 7d889d65c987..feb7cc2ee0ed 100644 --- a/source/common/config/xds_mux/delta_subscription_state.h +++ b/source/common/config/xds_mux/delta_subscription_state.h @@ -79,6 +79,8 @@ class DeltaSubscriptionState OptRef getRequestedResourceState(absl::string_view resource_name); OptRef getRequestedResourceState(absl::string_view resource_name) const; + bool isInitialRequestForLegacyWildcard(); + // Not all xDS resources support heartbeats due to there being specific information encoded in // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just // disable heartbeats for these resources (currently only VHDS). From 61361b80be660882cf88e07c5d31b80e42deaf4f Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 13 Aug 2021 16:35:34 +0200 Subject: [PATCH 24/49] Document the tests Signed-off-by: Krzesimir Nowak --- .../config/delta_subscription_state_test.cc | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 2e067f149f17..9f28a986d8c6 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -173,7 +173,8 @@ INSTANTIATE_TEST_SUITE_P(DeltaSubscriptionStateTestBlank, DeltaSubscriptionState // Checks if subscriptionUpdatePending returns correct value depending on scenario. TEST_P(DeltaSubscriptionStateTestBlank, SubscriptionPendingTest) { - // We should send a request, because nothing has been sent out yet. + // We should send a request, because nothing has been sent out yet. Note that this means + // subscribing to the wildcard resource. EXPECT_TRUE(subscriptionUpdatePending()); getNextRequestAckless(); @@ -208,6 +209,12 @@ TEST_P(DeltaSubscriptionStateTestBlank, SubscriptionPendingTest) { EXPECT_FALSE(subscriptionUpdatePending()); } +// Check if requested resources are dropped from the cache immediately after losing interest in them +// in case we don't have a wildcard subscription. In such case there's no ambiguity whether a +// dropped resource could come from the wildcard subscription. +// +// Dropping from the cache can be seen through the initial_resource_versions field in the initial +// request. TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionNonWildcardFromRequestedToDropped) { updateSubscriptionInterest({"foo", "bar"}, {}); auto req = getNextRequestAckless(); @@ -307,9 +314,8 @@ TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromReques UnorderedElementsAre(Pair("baz", "1"), Pair("wild1", "1"))); } -// Check that foo and bar do not appear in initial versions after we lost interest. Foo won't -// appear, because we got a reply from server confirming dropping the resource. Bar won't appear -// because we never got a reply from server with a version of it. +// Check that we move the resource from wildcard subscription to requested without losing version +// information about it. TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromWildcardToRequested) { updateSubscriptionInterest({}, {}); auto req = getNextRequestAckless(); @@ -327,9 +333,8 @@ TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromWildca UnorderedElementsAre(Pair("foo", "1"), Pair("wild1", "1"))); } -// Check that foo and bar do not appear in initial versions after we lost interest. Foo won't -// appear, because we got a reply from server confirming dropping the resource. Bar won't appear -// because we never got a reply from server with a version of it. +// Check that we move the ambiguous resource to requested without losing version information about +// it. TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromAmbiguousToRequested) { updateSubscriptionInterest({WildcardStr, "foo"}, {}); auto req = getNextRequestAckless(); @@ -349,6 +354,7 @@ TEST_P(DeltaSubscriptionStateTestBlank, ResourceTransitionWithWildcardFromAmbigu UnorderedElementsAre(Pair("foo", "1"), Pair("wild1", "1"))); } +// Check if we correctly decide to send a legacy wildcard initial request. TEST_P(DeltaSubscriptionStateTestBlank, LegacyWildcardInitialRequests) { updateSubscriptionInterest({}, {}); auto req = getNextRequestAckless(); @@ -373,6 +379,7 @@ TEST_P(DeltaSubscriptionStateTestBlank, LegacyWildcardInitialRequests) { EXPECT_TRUE(req->resource_names_unsubscribe().empty()); } +// Check that ambiguous resources may also receive a heartbeat message. TEST_P(DeltaSubscriptionStateTestBlank, AmbiguousResourceTTL) { Event::SimulatedTimeSystem time_system; time_system.setSystemTime(std::chrono::milliseconds(0)); From 63bba650b2fbe315265af53d828cee1bfb0ca1e7 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 13 Aug 2021 18:59:15 +0200 Subject: [PATCH 25/49] Fix formatting Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 2 +- source/common/config/xds_mux/delta_subscription_state.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 4395a196e531..ab2dab3fb3fa 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -345,7 +345,7 @@ bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { // If we requested only a wildcard resource then the second condition for using legacy wildcard // condition is met. return requested_resource_state_.size() == 1 && - requested_resource_state_.begin()->first == Wildcard; + requested_resource_state_.begin()->first == Wildcard; } envoy::service::discovery::v3::DeltaDiscoveryRequest diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 1ffd648186dc..da61c8d997bb 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -301,7 +301,7 @@ bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { // If we requested only a wildcard resource then the second condition for using legacy wildcard // condition is met. return requested_resource_state_.size() == 1 && - requested_resource_state_.begin()->first == Wildcard; + requested_resource_state_.begin()->first == Wildcard; } void DeltaSubscriptionState::addResourceStateFromServer( From e2491337022d1562c1d2bf94610c6a03c9d9388f Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 31 Aug 2021 23:16:38 +0200 Subject: [PATCH 26/49] Fix build after merge Signed-off-by: Krzesimir Nowak --- source/common/config/xds_mux/delta_subscription_state.h | 4 ++-- source/common/config/xds_mux/grpc_mux_impl.cc | 2 +- source/common/config/xds_mux/sotw_subscription_state.h | 2 +- source/common/config/xds_mux/subscription_state.h | 3 +-- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/source/common/config/xds_mux/delta_subscription_state.h b/source/common/config/xds_mux/delta_subscription_state.h index d87398ca631e..f290f2186852 100644 --- a/source/common/config/xds_mux/delta_subscription_state.h +++ b/source/common/config/xds_mux/delta_subscription_state.h @@ -114,8 +114,8 @@ class DeltaSubscriptionStateFactory : public SubscriptionStateFactory makeSubscriptionState(const std::string& type_url, UntypedConfigUpdateCallbacks& callbacks, - OpaqueResourceDecoder&, const bool wildcard) override { - return std::make_unique(type_url, callbacks, dispatcher_, wildcard); + OpaqueResourceDecoder&) override { + return std::make_unique(type_url, callbacks, dispatcher_); } private: diff --git a/source/common/config/xds_mux/grpc_mux_impl.cc b/source/common/config/xds_mux/grpc_mux_impl.cc index c2eba80b4ad8..c30241db12e0 100644 --- a/source/common/config/xds_mux/grpc_mux_impl.cc +++ b/source/common/config/xds_mux/grpc_mux_impl.cc @@ -90,7 +90,7 @@ Config::GrpcMuxWatchPtr GrpcMuxImpl::addWatch( .first; subscriptions_.emplace( type_url, subscription_state_factory_->makeSubscriptionState( - type_url, *watch_maps_[type_url], resource_decoder, resources.empty())); + type_url, *watch_maps_[type_url], resource_decoder)); subscription_ordering_.emplace_back(type_url); } diff --git a/source/common/config/xds_mux/sotw_subscription_state.h b/source/common/config/xds_mux/sotw_subscription_state.h index 86063198f5a7..0376a22c58a7 100644 --- a/source/common/config/xds_mux/sotw_subscription_state.h +++ b/source/common/config/xds_mux/sotw_subscription_state.h @@ -68,7 +68,7 @@ class SotwSubscriptionStateFactory : public SubscriptionStateFactory makeSubscriptionState(const std::string& type_url, UntypedConfigUpdateCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder, const bool) override { + OpaqueResourceDecoder& resource_decoder) override { return std::make_unique(type_url, callbacks, dispatcher_, resource_decoder); } diff --git a/source/common/config/xds_mux/subscription_state.h b/source/common/config/xds_mux/subscription_state.h index 9f9b48cd7723..a440b8a5a889 100644 --- a/source/common/config/xds_mux/subscription_state.h +++ b/source/common/config/xds_mux/subscription_state.h @@ -123,8 +123,7 @@ template class SubscriptionStateFactory { // Note that, outside of tests, we expect callbacks to always be a WatchMap. virtual std::unique_ptr makeSubscriptionState(const std::string& type_url, UntypedConfigUpdateCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder, - const bool wildcard) PURE; + OpaqueResourceDecoder& resource_decoder) PURE; }; } // namespace XdsMux From 88587b0fb43b824d65ec03c011c68fbb4842a830 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 31 Aug 2021 23:22:49 +0200 Subject: [PATCH 27/49] Get rid of containerContains Seems like we are using a newer version of abseil that implements the contains method in maps. Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 40 +++++++------------ .../xds_mux/delta_subscription_state.cc | 40 +++++++------------ 2 files changed, 30 insertions(+), 50 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index ab2dab3fb3fa..abf610a56ccf 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -11,16 +11,6 @@ namespace Envoy { namespace Config { -namespace { - -// Usable with maps and sets. Used for ASSERTs, to avoid too much typing. -template -bool containerContains(const Container& container, const Key& key) { - return container.find(key) != container.end(); -} - -} // namespace - DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, @@ -68,9 +58,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); } - ASSERT(containerContains(requested_resource_state_, a)); - ASSERT(!containerContains(wildcard_resource_state_, a)); - ASSERT(!containerContains(ambiguous_resource_state_, a)); + ASSERT(requested_resource_state_.contains(a)); + ASSERT(!wildcard_resource_state_.contains(a)); + ASSERT(!ambiguous_resource_state_.contains(a)); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -82,7 +72,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // The resource we are interested in could also come from a wildcard subscription. Instead of // removing it outright, mark the resource as not interesting to us any more. The server could // later send us an update. If we don't have a wildcard subscription, just drop it. - if (containerContains(requested_resource_state_, Wildcard)) { + if (requested_resource_state_.contains(Wildcard)) { if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { // Wildcard resources always have a version. If our requested resource has no version, it // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version @@ -95,9 +85,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.erase(r); } - ASSERT(!containerContains(requested_resource_state_, r)); + ASSERT(!requested_resource_state_.contains(r)); // This function shouldn't ever be called for resources that came from wildcard subscription. - ASSERT(!containerContains(wildcard_resource_state_, r)); + ASSERT(!wildcard_resource_state_.contains(r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -318,9 +308,9 @@ DeltaSubscriptionState::getNextRequestAckless() { bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { if (in_initial_legacy_wildcard_) { requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(containerContains(requested_resource_state_, Wildcard)); - ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); - ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); + ASSERT(requested_resource_state_.contains(Wildcard)); + ASSERT(!wildcard_resource_state_.contains(Wildcard)); + ASSERT(!ambiguous_resource_state_.contains(Wildcard)); return true; } @@ -372,18 +362,18 @@ void DeltaSubscriptionState::addResourceStateFromServer( maybe_resource.has_value()) { // It is a resource that we requested. maybe_resource->setVersion(resource.version()); - ASSERT(containerContains(requested_resource_state_, resource.name())); - ASSERT(!containerContains(wildcard_resource_state_, resource.name())); - ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); + ASSERT(requested_resource_state_.contains(resource.name())); + ASSERT(!wildcard_resource_state_.contains(resource.name())); + ASSERT(!ambiguous_resource_state_.contains(resource.name())); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); // The resource could be ambiguous before, but now the ambiguity // is resolved. ambiguous_resource_state_.erase(resource.name()); - ASSERT(!containerContains(requested_resource_state_, resource.name())); - ASSERT(containerContains(wildcard_resource_state_, resource.name())); - ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); + ASSERT(!requested_resource_state_.contains(resource.name())); + ASSERT(wildcard_resource_state_.contains(resource.name())); + ASSERT(!ambiguous_resource_state_.contains(resource.name())); } } diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index da61c8d997bb..52c82e86427c 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -11,16 +11,6 @@ namespace Envoy { namespace Config { namespace XdsMux { -namespace { - -// Usable with maps and sets. Used for ASSERTs, to avoid too much typing. -template -bool containerContains(const Container& container, const Key& key) { - return container.find(key) != container.end(); -} - -} // namespace - DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, Event::Dispatcher& dispatcher) @@ -50,9 +40,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); } - ASSERT(containerContains(requested_resource_state_, a)); - ASSERT(!containerContains(wildcard_resource_state_, a)); - ASSERT(!containerContains(ambiguous_resource_state_, a)); + ASSERT(requested_resource_state_.contains(a)); + ASSERT(!wildcard_resource_state_.contains(a)); + ASSERT(!ambiguous_resource_state_.contains(a)); // If interest in a resource is removed-then-added (all before a discovery request // can be sent), we must treat it as a "new" addition: our user may have forgotten its // copy of the resource after instructing us to remove it, and need to be reminded of it. @@ -64,7 +54,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // The resource we are interested in could also come from a wildcard subscription. Instead of // removing it outright, mark the resource as not interesting to us any more. The server could // later send us an update. If we don't have a wildcard subscription, just drop it. - if (containerContains(requested_resource_state_, Wildcard)) { + if (requested_resource_state_.contains(Wildcard)) { if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { // Wildcard resources always have a version. If our requested resource has no version, it // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version @@ -77,9 +67,9 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } else { requested_resource_state_.erase(r); } - ASSERT(!containerContains(requested_resource_state_, r)); + ASSERT(!requested_resource_state_.contains(r)); // This function shouldn't ever be called for resources that came from wildcard subscription. - ASSERT(!containerContains(wildcard_resource_state_, r)); + ASSERT(!wildcard_resource_state_.contains(r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, @@ -274,9 +264,9 @@ DeltaSubscriptionState::getNextRequestInternal() { bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { if (in_initial_legacy_wildcard_) { requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(containerContains(requested_resource_state_, Wildcard)); - ASSERT(!containerContains(wildcard_resource_state_, Wildcard)); - ASSERT(!containerContains(ambiguous_resource_state_, Wildcard)); + ASSERT(requested_resource_state_.contains(Wildcard)); + ASSERT(!wildcard_resource_state_.contains(Wildcard)); + ASSERT(!ambiguous_resource_state_.contains(Wildcard)); return true; } @@ -312,18 +302,18 @@ void DeltaSubscriptionState::addResourceStateFromServer( maybe_resource.has_value()) { // It is a resource that we requested. maybe_resource->setVersion(resource.version()); - ASSERT(containerContains(requested_resource_state_, resource.name())); - ASSERT(!containerContains(wildcard_resource_state_, resource.name())); - ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); + ASSERT(requested_resource_state_.contains(resource.name())); + ASSERT(!wildcard_resource_state_.contains(resource.name())); + ASSERT(!ambiguous_resource_state_.contains(resource.name())); } else { // It is a resource that is a part of our wildcard request. wildcard_resource_state_.insert({resource.name(), resource.version()}); // The resource could be ambiguous before, but now the ambiguity // is resolved. ambiguous_resource_state_.erase(resource.name()); - ASSERT(!containerContains(requested_resource_state_, resource.name())); - ASSERT(containerContains(wildcard_resource_state_, resource.name())); - ASSERT(!containerContains(ambiguous_resource_state_, resource.name())); + ASSERT(!requested_resource_state_.contains(resource.name())); + ASSERT(wildcard_resource_state_.contains(resource.name())); + ASSERT(!ambiguous_resource_state_.contains(resource.name())); } } From da99fc74ad27e85f971ebdcc0c93542da5143df9 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 1 Sep 2021 08:00:25 +0200 Subject: [PATCH 28/49] Fix formatting Signed-off-by: Krzesimir Nowak --- source/common/config/xds_mux/grpc_mux_impl.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/source/common/config/xds_mux/grpc_mux_impl.cc b/source/common/config/xds_mux/grpc_mux_impl.cc index c30241db12e0..afe9c7a0589a 100644 --- a/source/common/config/xds_mux/grpc_mux_impl.cc +++ b/source/common/config/xds_mux/grpc_mux_impl.cc @@ -88,9 +88,8 @@ Config::GrpcMuxWatchPtr GrpcMuxImpl::addWatch( watch_map = watch_maps_.emplace(type_url, std::make_unique(options.use_namespace_matching_)) .first; - subscriptions_.emplace( - type_url, subscription_state_factory_->makeSubscriptionState( - type_url, *watch_maps_[type_url], resource_decoder)); + subscriptions_.emplace(type_url, subscription_state_factory_->makeSubscriptionState( + type_url, *watch_maps_[type_url], resource_decoder)); subscription_ordering_.emplace_back(type_url); } From d9f08038064f05b101628413c8a1487caaa3c6e8 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 1 Sep 2021 17:57:15 +0200 Subject: [PATCH 29/49] Document the resource state machine Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.h | 51 ++++++++++++++++++ .../non-wildcard-resource-state-machine.png | Bin 0 -> 113030 bytes .../wildcard-resource-state-machine.png | Bin 0 -> 252382 bytes 3 files changed, 51 insertions(+) create mode 100644 source/common/config/non-wildcard-resource-state-machine.png create mode 100644 source/common/config/wildcard-resource-state-machine.png diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index a7707324e55b..6631d21fd34b 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -22,6 +22,57 @@ namespace Config { // There can be multiple DeltaSubscriptionStates active. They will always all be // blissfully unaware of each other's existence, even when their messages are // being multiplexed together by ADS. +// +// There are two scenarios which affect how DeltaSubscriptionState manages the resources. First +// scenario is when we are subscribed to a wildcard resource, and other scenario is when we are not. +// +// Delta subscription state also divides the resources it cached into three categories: requested, +// wildcard and ambiguous. +// +// The "requested" category is for resources that we have explicitly asked for (either through the +// initial set of resources or through the on-demand mechanism). Resources in this category are in +// one of two states: "complete" and "waiting for server". +// +// "Complete" resources are resources about which the server sent us the information we need (for +// now - just resource version). +// +// The "waiting for server" state is either for resources that we have just requested, but we still +// didn't receive any version information from the server, or for the "complete" resources that, +// according to the server, are gone, but we are still interested in them - in such case we strip +// the information from the resource. +// +// The "wildcard" category is for resources that we are not explicitly interested in, but we are +// indirectly interested through the subscription to the wildcard resource. +// +// The "ambiguous" category is for resources that we stopped being interested in, but we may still +// be interested indirectly through the wildcard subscription - resources in these category are +// "waiting" for the config server to confirm their status. +// +// Please refer to drawings (non-wildcard-resource-state-machine.png and +// (wildcard-resource-state-machine.png) for visual depictions of the resource state machine. +// +// In the "no wildcard subscription" scenario all the cached resources should be in the "requested" +// category. Resources are added to the category upon the explicit request and dropped when we +// explicitly unsubscribe from it. Transitions between "complete" and "waiting for server" happen +// when we receive messages from the server - if a resource in the message is in "added resources" +// list (thus contains version information), the resource becomes "complete". If the resource in the +// message is in "removed resources" list, it changes into the "waiting for server" state. If a +// server sends us a resource that we didn't request, it's going to end up in the "wildcard" +// category. Such resources are dropped when the server send us a message with the resource in +// "removed resources" list. But this normally should not happen. +// +// In the "wildcard subscription" scenario, "requested" category is the same as in "no wildcard +// subscription" scenario, with one exception - the unsubscribed "complete" resource is not removed +// from the cache, but it's moved to the "ambiguous" resources instead. At this point we are waiting +// for the server to tell us that this resource should be either moved to the "wildcard" resources, +// or dropped. Resources in "wildcard" category are only added there or dropped from there by the +// server. Resources from both "wildcard" and "ambiguous" categories can become "requested" +// "complete" resources if we subscribe to them again. +// +// The delta subscription state transitions between the two scenarios depending on whether we are +// subscribed to wildcard resource or not. Nothing special happens when we transition from "no +// wildcard subscription" to "wildcard subscription" scenario, but when transitioning in the other +// direction, we drop all the resources in "wildcard" and "ambiguous" categories. class DeltaSubscriptionState : public Logger::Loggable { public: DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, diff --git a/source/common/config/non-wildcard-resource-state-machine.png b/source/common/config/non-wildcard-resource-state-machine.png new file mode 100644 index 0000000000000000000000000000000000000000..999814f6d1422cfcb74d0feef964be629012e107 GIT binary patch literal 113030 zcmc$`Wmr~Q7d5<5K~X}&BBc?eyH&b7q>=6r=~4;l?(UM5?(UG1?hfhBZ*9->{`s!= z|I3ea-JVPLz4lyl%{k^6W8o_!C5-wQ=P?99sG=eQauD=@6@u;)KSBckCz6R@8iHO! zq5|&~91?bBFSO(hr}*~w+z%!;BX$!vjSRC!E^W&a2P6`6c}JEM)Q0}pnIWE~c?BuW z{1#&?Pbrx4o0>utXu%9oBrX+}eRtpJ-FHlZ=O4czdA_r8bx&Eh+D!<5`g(DNH2G}% zizX8b1%*9v=uEQu*_S)Mdte3dk4(-T#o2!!!GAlRG{Z}Se<+`SL;dd)B=hzG?*Bff zs5}C1@bBkc$y2lcef0Q5H1WTWQ2U2sg#Uf^^U}WmzmM4eZy(&WH~Lc2w)yv)GkOSW zYJE%2RBoJ~E2$4bmO?VF__hb&b@yt#^Ki5v&IDF z+7Xos75tlSGqcFeF$mh%0C8w4vYpw?_EFCM9r5&;Ym^+%B|doiwl#wHtr3oK>zUl2 zgz|KxXGy$5Pb7v7kmO}Wus4IVh;|?-<~4}ug7Q|Jvys>jzbwz3jo>?;H)mE5q>Tp_ z$CAUWfQ`Az%q=6SArgm(k&#p~O5K8HC_L4tJ?y=gi4^Jmmi*TY9RuW%_8LUK(WZPA zjnIty)GcqO6-58Vm$8>3?#$++9%;mkQ7FDRCNjn%xDDgR`=Kmc5s$y8i@d<^#533D zy!}lm%T)OcN%%m=_gBaV7ITbeUIMZ@R@jOH9bpFVALM4y+H!1AKK4yRiWJ(7#)sZv zfKB(%Vknzi{T}#+h^lmIg#ZJB*mYSe#JsV*nq@Qi(#UTxPI7d7OtwDoHW~E{$tedm zn=dc7pwre_VOKm#mX;5SLKjUyBt1a*nh{s=gQ|kC!{!+TQT%(dv61fgz$9M(8GewV z6nz~^e&~-#PXaPCQ2iIbBi{@$`;5I*4@u=qvorm%m{*8%A1t1MD1hmCcx;?anF7Wu z@sGdw@s(rxKKfIkK@jr4ZQ4NOVFf|X8MBq%B|?CjD8X~H#l@?Bw}#||l64g;m)z`& zU=1N+=RC8hb1)rX?}ov z|GQGP&jSb&7|klk0?9V~K=ds4_uC%}xXw;Y<|-)d4<9)Fv}u#=YNnBI4x)kG6QLSZ>n*ylf z)l_Jf`FaS@KIy;IW3GA5JK;dAKyzkAK9+7Ocd_nv*dkf&b02c+`WQo`uK6wRdxj;80JzDnmohT$f0KItnnG0=1NKY?(&#fU*TLyvGqQ6 z*!envOR>c6BIFTl+QTD0wOwSL4Kx`<&b32}>oF*R}o~YQaKH0m#bB}ja zk9|K(Jb-u+{;fIrF>ZnqjoEqb2W*SFDi)vJIot@HvR&Qs&<#gK!572v41&k&Ki<|@ z#){cqflc0c7o@qHdfy}3gnA^D)um{~^CRx_@5sMil*IIbgr~l+&jiOPPj_jk_!)$a z`tLgkAER8ixlXbzSz`G7WEC4`9X^Wx6)Wnt#gUGZj^EhumB?gkBl9{=i8DlASR6gV z_uCWYAhuKNo*MqcFsc~23 z-F@!-z?|ZWzy@x|*tD=Oekwu3!>+-aH*b>(G!q#MpYl6!|04Q6H>D=AUi78wrVMI& zq9S3C7v%fmWZ)?+$9pCj{e?Jxu^S$n8@7FWWJv5g=or^MNyn;ghYhO^ku$B<`7bJ0 zgQ(ja?D)wGcwK@TdK-ll$7|Jh3#{vBB|fJy9|z+*j*c$gG^0ZSiIWpsR@KPk*w~Vk zk-7OD??5ex^+R2hq5^f^1|0)x(g%m{o$R5hs&iP}+?QAhix z%qvUjx;E*JMwxL3vxLhv0y$&cH{{EV_+3LIn_JJf7;9NO4mNhn1K#vGXbWOyMzReR z6gF7%t$|IuJ_&lZFwFyLF{-Ad#?^#?B(p)sBJXNA69RR_fFklIhpB8m0_5ibUJ`rJ zCfs+;Eosjx?RKSxIgj2p$SD!sy{c(`{hd_ebmvE#%tXzrrGS-}gdf8{fYo7{2sqJK z25HX43g2C`2lFavq%{-aVqi>~2@DGA`4SwV6DDOKYCV_R7_zxM@gp^;jP|XX?B0Ey zUeL&sx{csrAM%*F)WC;PT;}ojl5p=LjMF(8Ni_k``XuPk6f7Z+N`BC}HH$WEaCvIT zzl>6O!ksM^XPvM@8IQ9!`A%Nv`*a68yIy*rbtJ8#c;ECF@3)N)Fh3~a4qXybZ_H~Y zq*i@8n6S$p^D8W;StNFMmN%qBbc>SyWp6@x*e1xS<{oeM&F5oXQB33faWEsru}kmP zRIIf_DGnmU9lV`Ngu)DZQw}Am-$%P|1!}I*?Ro}zEX?mQK=FTnf&@|CgO8|5Y$V$z z!u!l-qs{=yNvR@=gqd2?>~Ny|z$n*z{1Lg3e#{%5oz{EuMP7|G4d*qjrjcXiwaijF zTt>M4Mn7{I-BQY z(GY`@Bj2wh(l+o0T5yG{?|4t4NtPCkF2zBkW~e~^r$M>P3-X>kv#c(x7n`9hJG8G~ zB)k=ktu)n(6`9La3tK-%S2aFa{LFllEGWOdFNMkh-=k&d2mV=k=v%0ZW3I=T48aBp;fJyJci(6(yMVr?+C zd^uvo+QP5s6Twx3KGh}fUEE4_KKd^Nk-(w08Ml1@ziQUEst;dTo(+QtCn&kkG;()m zX1)fCi@o~tgz5v-)Cd}*i=a2a1jB@~;kQO{8-q=a$dz9GalfKKtGF;hdn<_mS$+d2 zqb|(#SsQQjy`F7M_?vJ2DZe8J(q}xHzq?&RIXOkgzzEG9 zZ`rXaWhflp+~y}OoT!=wb+IWKtcKEbK(&Dwz!n`G?=%id_D>A4v6+-{DUY&-D5J6< zkC}fN+FfL=AnIDpl@!09w&mk+gJpb@D7ZD`ibcBhN~FsciDqi!GeBIGWirb(b;d-R80Ih(JAa zRR{_1>kzPY3=Cef$hiJl{V30AB%24HLS2Y(j1sYHk#E;?SuAQRiibAJ|J0%lUiGW8 zxX*pXtKT@iyINsp>QLt8`AZ*B%=dhcI_f7CM_M(Nf=PZpt`EiIpyn>%t)4!z?AkjxlK6+pme)96f! zH~`;P`yfC5#9DDEE?Z@Sx;Tt|JPSb_Fk&tWmmlksXsRSrx1BEcs`VFPAzrO3Ci6ii z_S{n2rC)x+Wta5hM^IA3R`G}LT%ffvZ=@J@8on6|JF^~T5=b{)9K5*kp%8Z6<$I13 zv#o@i=^S;8iZZmv$K9>$CsH!?W9}l)v+>V7@n>OI7br&kR9qVLgPdWgGWSP-epHsW zxC#`41=lmqOM?pEwIXFwS*gh7M5&GsD!!qASK?ZPl7oG+!W2UUuhrXjTZn&}R2h~G zm)yHwz-}9rG@l^p9@Ldab|t=9yhf!Fy)i~qwtIDyCZj97MCP;pBtAh3SvF9WCDeZN z`DZsB^Y|4oSExEQ|Ul86XEFRCWj_|BqhElW*Hul>FC#gr1s~YGIx^1i2pT0-%IKVm`1mjni zvUv(vj${?HphHS7B_x3aL!ff0h&k;F=5ju_S|v#UyvukFFv+53Qveo}s&pdz6dlv& zYg31GUVCkN?TB6=W?(TLuGt%)>t0b+z2WeaM|D8!^QlyS&Y;&1N+lVNgDZ4t&H|&& zzl5JUZg#vS8s6mUcL^DqsBX3wkkkvhl}oG^I~sLI+WGWaL)wQQB@7IzefKF*f?C=z z$-S(MAGe@FF%s}xk*~;V%NYK5D2U?2SQ%(9cDbe-Gm6%qWv*zX>DXdP2`h7dMZ2Ad zw|8k(mIN{kPM8Z{3FO3W&C-_;;*|~!-`78G@*^GoGjF%ad-GlC(m!Qr(`bz3RNRF? zB~-wDGK?R*&GY&w++`|!&Fo6|A8uFTjbWX0(qGu2Q>bs}IWY#n1xLKm3;$IBWY_Sla*W*$7su zNp|?rRa9$H`XfbTfEt2L@xJu)*xuRL#r8c>v&s26P%idFLzm6< z=kU zR3?`{FL^S`Mo)S+0zruMxN3r_NOn93nZ@&%&li!PswW@;AU66lbh+d1-s9|(SLxob z==S>3bM)|^X&LjEKY*$v^5pn&pOc&wLmh1)Iq$0wAPXM=2bu!SDi&!>8u>m4+L3P> zskI3DiTxP@z1!H9Cf%a*G4xuHYV;i+stim9j3{7(Z(aG%&t@0<)z=&ev|j^T+<(5`edO#0F`%faVE)9Ny^U0GhfwOwhj+MxLk2Ql7KuRQZ7Y zQvV7904ODRXmV3i_QYA(D}IXzb-*YDf;?U<*u8t7gFr9w0{`FNhS3uQy)$URCjy)g z7|0h`0G@to0X60Px3aFBH^RNrGoWjOMR_QL`4KQnaKzaEW9r_Kw-}@XzR3ptVG}iS zye~0W-5YRXz>i)UvR?`qW#LRR*FZ>k%8C^Tm>CV*1emFz``=e1wKV+KcJD8ofaI5` z`s&6EHNF4G2-0_ZW}a;1BP`&6h=c98I14hFBHnrZ021(o_j&uv41S8&kRbEfkqcwl zT%I^#6t1F< zYM+(Q_xO4{lnL95Z$#wMq?GVXfv}v zuwwgb3ZSj-CCWvEixBmUwY0DIAtPo0r*u>1!)dMK0wSF8V&}u6xXbLl} z_dG=5)NS&=Q6FuRu5tgR1a=O4RciiqyPkU1e4!%KqG0?cfqT%6T=EoocmN5K7Rk>) z02lnQ3`XFV;L&|UNc>6kv4=aH;|uMd7-^5fcVpv!tr7$g11K-s!$I;_Zu~bu(51+J znxKPee+yK4zkxt6J5%HC@)?Dm1F)9YjM%g@fMx6Ac?-onfFp0@Ao{IlMuae*7EWb& za&&U|^0>b22K(>epI+9&A5~&*!}#YP8-xk-tMh~1DCvK9REfnk)_P>Qg$PL(il+l? zEs3MyFHERLOF=i&26E-m;%VVV0Rp{al|DStxXhy}m_z5hFr2A$y7Jiok#}0p$9$|3 z`*y4IH8&MXVrhW&B%7)m%HA&Bm#Y)YFDF9)d|!@dXD9OBftHUyp263c70KGJJVc$G zlvSO)J^~h7Ca%GsX#cRoHu3n0B(_U&K$3q~8q6fiXk}g z$HoS7`4D`018ZWYc`C}zstNzY>JSoa-_XyD|#v&Q7)bL9r?~d!4Gkr~+ z!ph@!vAt*JyIbje!g@ov&X04eO-VzlXGaFrnU0MvVmkHr_NLp_QfxXKLF)6pU6)l;Myeaz8`rsGTwinlYQA}f%ddnB zSZS`T14~}b)l=)Hm6D=AO;c`=kg3J%uZ-O`^>WP`AZ z&}wtpJ`wH10cEo5&TPU;g!tHcB{hm;(!sH4Vxn?C3}qZs+2%p^w!*m@c2B^rJyv+_U_xNT#;8@g##fyL@8(xJnUvKt0 zo8i9$2f8)3-43_D(wyy^>rycD^4UlotI`F~rh!-5+AR+e8vj5vAHh(0xBI<}fi4$e zueo3|82wSk&&gC6V_1qRn(Os{qz0I;i62fJwX$i&7w|rfMR6_H?0F|s#Z0#bYiN^u z37AQDSFqi9HYL9Cc1GR6q)nsL)uv! z<@OeU2X1a#!y{y^<&PbnG`x-AcGp^aCY#x(GGoN%?-T{7uk54oHgTIJKU^VHMX|y% zruA!+Sic^!Lb3?wgXcVrj-)6YO4`~4;wpdnnbi-N=Elz!$69$95)>=U{0yl*ch$1e z_Ku1~lWH&NaXo;{D3wg@KsQz<=u-UE(g$IM?C0o$oycaM_1`llh!V!ZR{o`&cEp?% zJtyeLv5`Ab@m!d643!TF@h*g%WhFu>N2}Z&G4&N@GUQyjc3py-N9Dcpf^4xER8T&G2++SuxjJ6D^j)hmJ_b^TpumEX~A zABzv7X*l4C2(<2ZU{I1dh-96LcJy*PL%6EDr$p{gW!QJ~{kUW+tQB8&ZIAY4d2aU2 z$xhLiv>oq}D?MeFk*(JCjqSJfE2aK`M~Q9Jz0xNw1X9W2c**<;cs>Km+r*Y&jTCUm zsdCIS4^8opKg-^i*J$M%mpiRiH5d40SKOPlCSbgwZGjJ_MJ7HqI7qdfAgKt)KOnvuE#< zo#5jm$-e5-`H1a!XE3xM?LxoW+HcR7-KB?9LCtY`Gg>*Lv%78L9(bDM=ENPL(Li*a zrAak$uBJyDxrNWxVFu)#OkC?BvM@#9ZxVHq)^P8Jk2T{2d9K!B3f>(K4Mm>WHHq z;ehE&+E-P0{FaapM#1n*dUcR&_xc?cz z{f}nJzlbTEN!dt>G)7+;J*b4ib!`>4 zmNkK=wP&+GJ$*^h9P6#y7}HrS4r@gcDp15PQ=8t6 z@byICEjvFHi-m;dXGkt<2Xk@mPD{Sq>HJwvUDopZRE~kAIYAd*GJo$}cZ}?zGLEb+ zv#(}&jasX#gd6QA=l(}m@jwvRRQdW9;1}^h%YiHyH-z0}+NykxEUeV6uMC@{<8Qun zh4xy^Z{k>)V$eCBzPoq+90tU!cNaCDv7>Iqz33HyZb|lpgaRz+z<~!$CtiHPB4;Q4*!Qh`~%F zjsy@P_-0ueReYUdNDg$UPf<&t*~tRxWZCKVJ3p%!WwjJ6=5<`SCnu2mr_?Mc*1p~V zu(YGTH=x=*r1}r2_T**U{3;p;>JMt=2X@O;*Gd3AC}_MYiB`z0j;h;OzSq}3d3|)f z?-SZU(|GmT{Q5hRAOB_zRM z_^BPmD_aq(&9VG;(>BMb7j9;A3AmnPfQZT*v`Q$gKorsnkYB{=zo4_|*GWn)z96&b zewHK!%UBm?B&!-Vf?%E-QvRsC(O$rj_BnYnq*>vsl@cMZFko5JcQrpS<5{)6(0V~5 z>&wkhx&ny!)T^DSFEC^PXnOO>7gP;;+pzps!t1jLLtn^CZ#H|mj?TCHrmX#{3zm01 za=vZ@McoLHxtu*UbT-P6tNIhC{}Lczcekn2?F|>mcGByW zGfXEpKr0EFF6W_%`N|ZZz7KWuUCV?dwO|^z53Z zUh;C(vLda9JQWnG%BnkWzf@4)E){)h1y5>|s5^()I$waKgqyY?=5TFS5C;Z(W9KC@ zvA4DFPiX8(sTp>nStOCI3v#nbU;s8arfHCQq#=Qae?}*r_EXHRyFbac;+~#+#WI=b z0&X!1;=5N>XH-$?&^bAr|8~*w5@BQaTb5^UGW+y6&z5MiUVO7^$th@*L@$y`Wz+Rj zC*c1yiF&JwVIA3Le(L%x%M)c!FAS&+z%@bA8xrXKleKix`#$ROUrjOIY04=fMU|4K zO-A(J-~>t~U;V_--ncGDyX6h6>_3i3DOmw;+s6XLvS7s}q5O0np~UY67Lu6tTm??Kx9#~^_oimxRc(P~@*_ogSO4Wq{uGxM z15Zlxhx-;YHrTWizrEYL0QX>p1&0LV?5)z}sRGXp4P!JgH+(?t|F=YLNV4%n%4I4+XcH)MMd zIP*oq)*YBv2Z7A^@>{07VHK?)fJc0!Wdxt~Q9;d|;2ypeK^#1{0xN>B#wiB2#zxfm zLS9?OtO4%a8~xq&VN?}-e1QJjv<4geGAp`Qu8}zyfJ}eqqVWOjmQICp`8Y7Bs0Ar= zpt8ftk(Rzj=nTHkk%0up=VU`Ry_zz~JqP^D%#S-Ydp#6odJj8JNPM5e+SjvelxoTX)4~)t6^YxTzeH1Ch6-e;4S=d<2gBjyg)W}(5~1qT^}Jo zCgD76`UXg(S>Bl=z&&LQ4mYR%IG;X{DfiYtK=h|6`Cg$xNdn?I8S*17e}BG zPE{Qruz%he#2rmI;!qnsN&FJQ#}K*FaZIoikg<@}*L~Ms^Sj4T56q#05pHx<$e=U1 z1P{GXB5Pmwh5A(3gl297K0+sH7tr~W@ya6u8<>ITg5qZxZ0qa)_6T;rS-_bEHGKzY z9ozMg@y7UOLp%xgxyW{NQZ)>(4clY`X2%Ff`%`gNp{4n8g3Vs8O<&2=cS&}NfYC+< z_`8~3KKc$jtzZ}0f6vp#oG@#hUUYL`!}%*QJXGQ7j~)3l{btf>c%$Rbb>xvlP8C}Z zC_`|_Y}1aB2(=@®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 D=aoy3 literal 0 HcmV?d00001 diff --git a/source/common/config/wildcard-resource-state-machine.png b/source/common/config/wildcard-resource-state-machine.png new file mode 100644 index 0000000000000000000000000000000000000000..7447a9caaa764628bde2a626479ab5f1937e12ab GIT binary patch literal 252382 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

GE01F70XwJ5W*6f>F*OFBZ@gHdtD{F@pFOkhm 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%0D=Ia_El);E}10X zOIp<7@V;}IGe_e-k;~uHkoHuC4-VG$B))9FJXnloC{~i2V>`ILObl~mVecnR#Kt}? zA`qGOkJJ`p{Q!FOsS8*CKC{dwa|iiOZ9sG!-nRbj{Hz8v4kP;2db4%3pBYbqzLQ9b zDz=LKguQFQ^Tr?n=sQ=QKI#eG<1XS&VPCnNMxyU@7;JdPseG)+j&InlYS~%@wSLw; z5GuQTlJrK5+Eg%2bGG@ko6h-iwV~XMO7xST>oQ(<>{k$%k;c$Z-k?&lc$#q}na*Ao z=?d_FZS>Bo^MO&)4}*RJV}@JZfYkH1tk@WY4aQOnyCyGEyWPz=v8~P+kH4}F8yray zA>q7!#QyGZewq2nkoZR+T?N`qGeulv5MGjY+{(;t*$0W~Pp*Q=US{X?2QGaUqC*hc zG?7)&CAjnhQHF_XrjqP2=o2$C3Pck{fx#f>oSMSe)TCVGU z)?O;lqTGwqT*&E7@yxm_gQqBUTCqmexBUdj&t*|!yb{%g_TTd{ z`Gf|V6jZ#4W&#YwS&h@ViLl?u*8I>`cx7CmK2UeC;Jv7k|I@y+58xYzXliRvOA?AOG2eun0ggOm(kyEno(+EuyFs_9vx};L3>$!#*nK z^HB&WAW?kKaIG4hC9L|r$GyzE&=9d&R^?Uqg^H}#d2Z#n_I^5n>IwI`I3c&+t>uO5 z(qevv6G>eW9)Y{C#-LD3aP^>@zP+^es#(qtn$i#48|X zw~?}IM$yD2m(6_j^2mvuNW}NF`we{ywjTnU?7m6O=zKn~Pnpc4;omWfe00Q3d|o`c zeBsu40mev$(@NSqp75!ibwIaEJS~3jxRlIJ(xv8il}}R(&N3LortS>*ZEjHtYYuBp z&Xm}bP{AxdF8>Bn4OZ($X%n<(7uH6P3xhVAHkApBQYEI>$Ay9d-fH9Ta*es3VMhA{ z#W~H<;)FBl@O1MS#DdpGlc6_)98)TY^LR!Bm)8)9?pvb)Y}I{F3+^~Y`n{p%-W(7R zC!u{9uFTl5L7RKx*srbne>jU>eUaZ*gzObx(+HhIFYdB6;<{B>@%{8g3s&k<)ES8G z)89i*YmpFEXkSM~5eAnS_Nwopszic>A-oU_;z1;mLuG=kz41u=>?91_MMyx*mg%s( zb8K!d00=73%oxx;`rhbQXqM^Zd9es;bDAa*Fep4aZ){6Ibwl+U*^PczLplE<2y})q ze!{>?b;`mZjeEuz9v&_I>$ZI|*~k!Wj*~~2pUE;FYwNBoZpUE4lfuEMi|vczMbiL2W$P`> z3G@~!0k5VEq!w-5x8`Y^uq>rZCo}jRSYqZM*Z`CXyhC*oY+$uaTV?fn6+4huEi9fz z$=Rj}6BNEvMdFa*ikp3R52;U9NZzSc<`5_>(-bXHKvoJ8d;WG#e->U_1o#7glX74h zZE!5YTTpHXIy_fDMP2gy#>bizIRZp+) zPGX(^-pk?}R!`Y5v|zG0q7G4}c%9$1B&z@0BmHY0ps%EkeE z4_{{C?Y=aMw^81abgK{`&}@%U z&DAb#UQv#Ros6>Z9hNR5)3~tZD13!svuG9byvHctDL;R*db@C$F))`=ECQQY7sCY0 zdbB&PVt{*S-01YOvr3`m#gx(G7ZtmQ$K6izXhDdZI9$rrtWKvZP6AH|VdLL!2GxB~ zJ|LZhi6()S8xA$OP<>Jk&Ehx>xz@O*W3RLiD?zoJ$-?2S;zuL>xEbnfq34*QPTBxq zr1#qQhuQZ|_NW(-Ur#h-RYJ^K!2Pai!rBJR%LJev2K6g4xg5|!T>0bsO8$&iA2}N( z%G|g*(sl<72lhXPWA7CHZUR!dMZ64ur}(qbsvG1U7*GHP_8q4pRz=C>dT6X;iZgo6 zTX{ssFD8x6enzu~nv%Y8ATC$-0__SW^_v5wyEUaM&@l7DV1^q*bzU4&8Im#nu9Yg8 zgSwmeTL2NcL(`M7VMla(`aY~E>CO(Le#ebH`D<=4?ai)*NyY%HK4EPLsKiFqfxZ@N zJ3R3S0?~@nY`uQ~-HKIQ-L$gmr+q=zfE@q>GW=3Ls>)HJlFDI!7-2128)M%n1K?N;O8e4tfY17w6@l(wXMfrr)Eh*O z{;1m}W?bpZ__ti=M|wz#!lpo!rBVIO*Y0^$UkgS*aw z20+Xa4EPaO2W{iNqhAFK3+L^1r5Er0-TtTfngK*m#bwZ$pY@5e0_Pxj5aslQgPz^@ zD;4N9!O<4r(MQ*X$-61{(fIgr{B$1mVfO z?AbM5L2?%4q#E&`!YT%6`FkEPW4S^=JhWIpJtVGpy0^kCDN zaOon8!C+YaKcjY?#!UltoWj*r@stDLps&k-YDjP~`xFPz$^Vn)Qha)dno20-Be2GI zniD?>T&7Wcm*-G`=Vgk&5UKEb#$vmdHSBLpjvtgQ6m3nlh~#9Th=YY=u=n88a#NJ~ z9$H}pp)n2(zk0D!m^cso0R2IoLXlvN#vAJ=3=TH>_xqc`&zH^~LsNiA0eF)dw_D7@ z!@Hd>sUJ4zwSLtUMj5lp(?IW3C84;55+KZV_XnCoAiHt>J16OPAp7%h_<$dEy#1FP zAJHV9#-aryjYqHW+EPi$BUmm$@rR{EmI6_6>u}he-2Btke<>*J2thp=^NXNbgHY!c zG6ESeMX4S;UdH5>tUJ3jvipChTml8j$*%eM^J&w^fMcq0UxW|f z*r#u1AMk);iyUz8(VFr=`S+GD4VTcq~i#a}o^_o$2pne5{w||~s#q~m&%1;O|{V@6h+Wii8nu#b-Qp1FdV2^f`@xYilBGAPG zy^TX5d=DX*dCCB)KN8wdJSN6b3*L}pD(rs5tkGG@WA4d+3SQ}s44G1Fnq-IFf&Xa% znqV23f+Ft0nIhG%T#Lb2G!BdpxRo!!!g26Z6>%le`+We^-sdZbfK_u?fBFCRiny!EaLVD4%>|LoQLY%`Z%7)5+6!Z1l8c&>`uq1o5HXhe zZ^Uyv)boWbZ)QbmZ!!lwS3*~aVcz5ChCA2SkCz#t$qBl6h=_+L^(A(iBX)y)(76!A zp`@Wrl6ICl-(QNC(?2|mj= zV%ra%0`?Ep^|rd+jN8E2pKYrRwBP}pxiqh$m_myjDAP2UOyo%PnYx5h3JWe!rufCVcJ`2oEW;M^VGo* zpo0RrbQ#&$(`r03qq(Xp_M%n;*^nq8sQ4uC}NvOw9I%% zbE~(w(H-wa*xyUG5a)6ua~|5d|hxQMr#?_W>KTMdTg2EI#= zaADmo&9&Ku?q9xklQHhfIC}%zAZOY4!muqXq^=*=V6?YTAgQL6_LC*^hpfYvu< zB)>QWT3#~GP$!BRx0v(Pl}bja&g&X0w74Jpd@1+=6LGNTUH$L~6J{-4+4%R7@I1ko-t|aqxWNSU!OwRz^bPBYSLOwgM6#d{gq)kj0uvKd`!mPM_PI zo{=kzU`9r}ZQVg`hYnQ$7*Zk{H|^#+7Idd2#RD;(apPt|D`t4Okk7x34N=z;e~bHk zUO=Jk*2&W~t||AUQN*Bkubg_%cFhlkKc*cQ`I%s0+aNshvcfryy|G&TCqxBUuKMN} zIK;tig@li5)AQ1$tsvCWJRNkGMqI%*6pV`tV+pn<9?!s)m`NV&57z4lL1SU!S*sH^z-u`2u6lF+yE84=o%LYFkW7XJBL$VYNl|L>N9uI6W9{9tqb#@~ zM`BFCi-gY<*b)|8MG%b#k}4MOMZK+QV9Dq6>7ab&WeiG0xoG> z;_?qq<_i3B70@)|a@vlv3@p?m(PTC?E;Nb+MRdbE_4A|E0@ME3?2A!1PHe7R;84?( zP;~__8JYa8w;fR6N*9s-f|Pligke*NvtrErH&#m0u+t@+0xZa$TFN(+>tJ%Mz%t<2 zYx@PyT={{;`i^cDw0K<$Jm}?PKUy5a(toC06ru);p{whwDO2H7t?%*Qm@JhUYwvI zRO^4fnF;gFob)9(2O}R`;GC!^@`n_)_}nNJKAE&!Vb(DO#OAl~&)}`c_0nbZkMGW7 zKm2~ezTD&AD7VS(ep6{_VM&j8#VYpHvQ1|8xNNoWIj2YR^BP0b!!Lh24ToNcrPk=B zFP;*;RS1y$PONPdQjeHhy_r>PI=rWv@ND0xn|m$V=U}i5iT3d=0dJjJs8m`u9x-$) zhc|kIz-vi|Zap0biP#S2JELr*-8IiWHqLnrgt){6T%>4UMTucn%=qL#r>VmZxvAi9 z)DfeMJU+V9pO#3fy*yVC2)Of*9eE23 znk2tQ%8dY9mqHOa`*Jgh=w&s3=j4Q^)^NEfGkWy`c(OqwEZ#>@Q}&jQtiXMw!dNwi z!rNC-ONHf*Y#kOlWsB&iOwq8Z75^X_HygPPJNZ%OU`yjzI^YXTruUexFnz@A>*k#l zrO)%*4wvR9HPW%61n~yHYdSoX5W(ZDF4u`~S;R}(I~5$Ra(u?|3T3Ai`=D3bzooj& zkBSotv&Wgl0VJub-V?W9Cxs)AX#>zZdnC3i$4BAd>Sto6>N&B2pE-HacP~KvCwkpv zGNiNv`gw!uaVTMd+z0p;4w8_;6?2WKTo018h94ezb>$*QfZMO;V|(7arh9zo?il&$ zJ*|3A$&Ln~bU2p-jHvge7pXKAFxB`9>`n+5d%AIapX#5G(m>Rb&h{C-22>}9 z?%39Qqm5Pz4pr;1Ra84>3hlO9$FgwjD*Y(ZpjK!sgCx_4dKi+bcw~>k8;)9CXcep| ziKNW%|8*RK%Eych@BWD~GkD`j#+HMs(l}*f-QgWA2a-Mgc4wCeb)#*E2f?t$WH{P1 zSv$#u{%nx_27$irl=F6}lBCW{=wePmeox=0H!5T!mw34FAZ-f$*~jp8UlIn(EG{>x z=jfFQ-0?h>BB85f z+7Ulp>56NY`WM~}3$S~o2K7JLy-vn8uumv5nQo$d>41InJ~!JFjzcE;h$itMEnlZZ zBGhyh!BP#ShMu{RgDoeN%eppvEJ4%H2>NX&vd|VHiGa&pM=vGhs=)){Bh^X^E^+iM zY&AzD*VZoG<`|V1_&ShQpO_!g!V#$QEMjAw&A{Fka;tw&EsQzI_`{o}U=H-rAzw&r(jcgCQys{G55_d!RE*b|MuHGc_y<68Rv zHO33gLi{S`uIR4ByNrVLMVipwW4HLSZErrH-1vNyys zT&mkny?OZ}YdM$+1n=lI$1L6!qz2^*x5xXJ++zlZ3hE_z`2~q_7VKo)O<7-|Rrp&h zo=vPVn)ig=eYCEm6P#6iX()w)2dEyF8vHRKh47)0UZ1r%L`lvW?AQ1C6t{8pA=E#p z-vdKyGyT~v$!bi8i}ih@NCa4$n@ptSW4Ssh=RSu#uQUk z-}yyl6{OJBcV$pHcIDKt?!A3-XE*hy&74Q@OrskkNbzJO;pKHzT3?tQ1+~o!O4@`I z`~O9^eqmVQCW*Pp8&n%`w!872+`6Wu^Sd+(kw#nx?+p}WHkL(1tWA5NVuH)Ej`AB4 zqS=1s-?j8oEZkP=V`PK-bb2M@X^>ETLzBE}_q4PU_BYna+K#w_*_B03^^HP)DXt&- zK+|xLtUDgcS~b=$_6qU3%0@e8u*lC-qGpB}f#sldLH~Z+!IVIqtIG46!Cway57}^Y zQ3)x3cesdz)>84~2Ia>`Eco08uf74BL zWBa&5YE@5~u(T zhAvgi?AN6c0j!8DSFp8drfF-s=w%8&W33>OR(b6_^a>tyIKK(ieGbzeM_!z}zi|Ii zFfwjD4;6aZ)Z7)#>L?T~A6%g;QcLvN8~1WxOHuXwu>!0WN^t#`f%RG#liXBWbajKq zR}Frj_`r9VWm@))E!VRSY`G{{Zux^40TSf%sMWMZ72lm73v!#(5LqTDmuEDu*?I)| zt%s#AJd?_R-E`D^L-*)y!1Kb|=Y?j^C)tlbt;TPd@3Ey`vWK`l*9TcS=_c_hf)L$8 zb+K5$iTg9i+~7Dq-d4>oq_mhsv+FX?wqf%C{=Dw*i)v^4PCfo#WO*`5rE<_ZZv9Wg z<4bwd^UTYjSbwmLYB}V7Q*cnSX2#kHr&U%5o0CJ&t1*aNlY_Z741ZQBm2PF(mc+7N4YEPVbua~}a#wd*IsSKsC) z)VsW%nif&&qqbKN9uDy>Q+8jUim}erBZ-neZDq(1$r^;nsC(NmG>=-$>giMJc}QX6 z)-admdNnq`!4N>gO?T3;6MEz;^3U z{^qvYJDRJ;ATK9@2O}ykiSV1jDtefUn`z*p;!aKKt`R{{Ow|Z|A)n2}U<0RGU z=d0E7q(M`+&bDpQa^WT^rX<;m-Lr6N$E1t!>qK3?#_szCmX>`%Y(4bz z;z)qk-ZOoY>XvN+Uw<@D!ih{*uDd-PUbCq~9$ZZDuMj7%=iS{}L$=ox?;CW6bKs0X(QPu=);q(O6Y1?xs!5Z&!{di~0+J^c5yW%le?uPHdpnc2h4cWiksz zb30R;PT*0#Au_HpV}lhAssaD9Tk>YO@5>29VX#vD9rv>WsJHr@GcA7%IwL%Jm_xDe z)6Mfqzk+VH*1fk>5)=71XPHva3XvzAE*?h8ZJCK=Y2g^W7X2&T_>KU*66B5LOB$oS zT8xwZF;+9;Fzd+UjRU*Lc_f-WT|T?JI+MLekwYSmR@0wyX1hL%x9Ih5uNqYTMNbFW z(b$W1;f2k`)QJP7Ss!H$ox(*HBC!ABGh~+asq%;VvN_Jg?l7TWk@NMLrvd0-O+n-S z#c~65$$B)w<&9P|Zmz$j6S=R;E(~Zv(|EYk1xMtb930-1YKLB0Djbv8`&gbelFnR;L7MPL($bv2 z1$g4GWh%f+NCQ52YTSwq+PwQw8CjDU{6}pcBZp zw!3+DDS>BfKSy?!zR(#;o-9Im&PlIe=uP1512~}Y^<$Pk$(*%@f#!QG(wX*YcpA0J2a= zy5pESx0$Gn4}PXIyJD#x85VnWRD55J{py-h^N2=wa6FcJ$*v6ft>#IYr3iPJMSq8n zT@0xFeZ@`FCagNXO_Yq|Q}svNSwdcP7&<1PC#ZI{X9W6{{6z~1NfIwvDhLp^n_wo# z+?O~#e%5E!3Suu~-)CQ!R5{KahR$7e_&V`$Z|)Q927+QVuc_ARcMX`DM&~3RcIaVG z`0l8y`s=EUMcd;b41slvi;f%}dp?ezObMMVRnmIb95AQ#?>!Be7u*ChLh$X5{;2Xw z7PW(PF=hV>TRTnaccwm*v)3XEEiZu&gg$Wj@-=S0p(SH1L~@M)?rR*W0MS*2I%8Pi z&%holVOcd|;FN}g_ol%|O`(4Yh!+;2A$8|oP65lNANeY@Wx$S;yisBzU!mR1h;{z+ zrZ~?U((xmz>|)J^)c{dRvsz1ANR2ptr(szi7}*PC!>t_!@{cpmHw#~E+-O$^gw!dK zz)+ICe~t%_cs2d&H|XUH?{pnD|B79g(*-M73do^=UBqy1m@o-u=?F?rn@5R1$=dgN z0(XY(!MPUQG3V4ujzqKG~|-=Noq0` z^5vNYO6CDnPm+ym+fk6JL;`RkzO`q=aT>3szO7QJi+BBBxa4F?gLvBcpIjwpz*0 zC}=Yzwd8$5CE+bH>R%e-8FfTC%KeX9KCaH(CmqQii^f?(IcrVLobp`_6QH%$qOYrU zt;M=-9@4y4+MP%lJ2pp|=!sz~RON zNpgEzPyLLKmxHf>8P-u>sk?Ed1uJRna>dEKF)1SP7@}h4`w4?Ug$(4Q=a4t}&B;kQ zZTB~Ue~oThA@=1T{1$-v>Vtd^*>esuAL=_i2p4Mc1b7Vj_8Ct28)`$+Ee39*OHbwH zY1ICvfZM48liYW&uyFt^bKIV>SPB(YRet>a`bshN+3Yl#dvYvjcF^`Oy`c}g3`4sVaRgYaf&zHs6&ipwoNd-+qIR*~M&!_lS zDO5hPWL@Vm+YvQ{UQ|LkNHH31ifD4<7zxjrzeg96WJZ{5@MPnbyE}add6gN7bPUTJ z{%Cz(Q}A#nWS(GmL~pZk7I&Q!EX$Nshm>UiULDH=1(`~V6DyJH_;yCuhuwI;Y483X z+s6!Km1XGeZ7BbtHmE6V5%dd_lk%We^sN{~fxk1l>zw0N{VIU6)l`!l+4RFwwh2V`0DQ4onI zg4~o)pVy9qs;3$*OBaAth80<=kzVJ3-u`f7tC|;eh=e1vUh&cTS8yB#TK(ePKpxR> zCTnSuio+|cK&D}i7dp>R&23zbzkz6|Iv(Nq<_Fid?f%p$e#`v);qvkKC*h@vEZ0Z8 zb@BrmEJT9%;E*p~0T&JPL@e@-4F-8S&&7knQ5c8;3TMV^9OCKtZVe}B<DxRhp8*ekerZrhT3PwSeH|g98mE6&e)i0W6#;_v@xIE z{-G;L{8urrwBPA%kb7zRIV72LjZ_1=Ki8V zIsA5A9_BT1x$P)jn%gf~^%9*8hokpL@tb;L09}@R|I)E|5O0k8;D*LWy*tR8WZ`lc zNgo|=-_Y4Qw>Vf~L~%A8_~HjDj1B+|0`$Er4~9Z;nb+CmjSd~Ni}+3j!HR4_75di% zcf9gqthbjzg-2Jh88{YcL}6XU@n3GH=|G|hI9L2cN9cBT#+dfgY=azH9Dgha!B;dt zy5ncRNpNHQ;WY1c&o0`r>inJ`q86$gAYl1yrQ{P;bV7Syfb`m!Z##9^&>LqQ;;R>n@ z#v8E;^M6v3^3HDeE>JdZaNla-X{zXV*K9@=&4~W<4xxYTJKq;JUQahh&*_TaJGKM` z*T1Y%KU`N@SIFwMHGhDURVZkUdNFx|$`zr$*`dnxjf(^n9MTK^1l9g_2+hCI5=70paAS0qRoLE4t^3i4CazadN(>DH^=^_2RxcQP}c z+a9{bd;>XP%qoSZiar4yc&Mign;e8MU$v{DOGB2eC^I{*ZnJ9O+sk^y0Fq7SNs$da zYeEj3E8X$5`6Bc{wFabwI?XVme5=I3GdlAF-NCxWJ~+5JPdTe{J(CMg95S}c1WMo^ zJ%2?Wc7hy4o;P*#!yzZ6Dw(@%ABFt-Qr}QTISOTw;Cubj4hBSnJH4`QZX`|Y8OrO% zqhPywTPtf!hCpVgn@T!Tw(JQd5h*rHDIb%P3aMRw1+XUs1y`U6MNSJAicv}ukn!uw zeF6K`P{iWLJO<*m^y@0lb#yWvfPz^d`B>id=(iv1YJZ8KR%a~89xU3k``U)zo3&6? zL>vH0qSV=BlgjW-mh7Y6#f8_8ILV@D;>~y)s=gEdrd;8kVJ%f{7g;gZfluPIhlAGW zIrr7?u$ouQ`uSM@Ht&Wf!P^Kduxt07lbnCJ@v=PRP>(d2inY9eRlXbb07ze8v#vwT z${#z0VyatZ+fG_FBr)YcGZf8j1)5(#?(nSaJ$LEfW;uN=PEE~&E{x}{Xuf=9Npi+F z7f7#vkg(5R&1czF58fB?F8%~YVbFYr#A_*}&5&WakBwj9*r^d8-fMj9x;pTV=i$KI zy=IJ@4;T0H%6t@q16_Gnw(Lp+!}sq1_|h)%rxsr!Y-b|jh>suC!pF=H^p8=VThgrM z+5fZv#x(ti>>b4uL>r0dj%eH)@$RTD4an3Z#m>$pFE-C{mUAsH0dhwghaR(ZmwVyF_aCUzrwuK*B?temps(rPq$RmY6*nyw-+kAxpQ@YPbCm2G2_C0>+ou9CRvY`aS zh6|d~v!*h(r%w9smTagK+-#bLx=+zdLqubWaJ{CMhIo1Jz33^Pq6xRJt0BkUVFlka zbKb)Wl;CpBmM~r$-0Z*1n;RrS1XZZ2(hhI!WXw6Peb|$%b94cG0l>VE9x|Z3yo$*AvOQ9Sb`5hG^S7X`C39v1)i1VjKJ!-^-D zoZmN(w3>Gg+16re`Um0ky$j-YjcK&qnJKy_F=`tzu}Z!SNMqTwXliyNHL~0E+@uf` zEgiMten_sm56KnZDqY(r!p#Aoqx@!^OWVRU7m#A z5IUBMQ2yX1qXTJ>mn(3Z^cuYN7Bns|g#j#f;cxFVYhicEQ113 zpv4-Wnsr4Ljvv;kB7jAE>h<@l#;3Qg>pLSzA4bq|EGdOU0=dVIhANv5n+Dn#?tB+xFO08o&Pv#L@~Ma=?m(@0*>Ah?|n1R^Fft2 zwICK$t+7;d40k@=^}+FKqG7!qY8NiD)_9~Ic^YfZ*lRJ@8wsnf6;NPn2r%a#Ao#rp zWbc#_HK0WHZs}3Ll7NbApRX!TE%StF;jv?auo=V@Q?KmF+dE@bKrSwfx#qvY`BO_1 z?X@5JUec2Vz~${FO3wi{4v4T0-XW^SYAy`mn&H60j_hn66{v@*NHq6YcNMT4IXhW% zmU8E2sr;FR8neQHU;v;{S?Q3jm7YugD$x6V$XFE+RlIopD|tt{s3F8~2Tk9876Mky}5u=1{G+ zSn7ph77*@r@GL;ms!=Tn_V8n*|9g1Q1ly?dR|ZUU*u`u`py~mN66h6x?rJ@3GyZ9CHk4D_>35>Eolg81bx* z>0cC$X0oU(v8#bSo@r2;&kQ856_+)fndm=8FIj0IXsvOCo313&roBRO?g#wakR_o#*?pM@vj$}u%P^m(a7!S z3_Y-kveR7E#s&@|kY2+#_hFA`;Qhc{11`TXN3m@`E*gtmNXeD~P-8%05Q(>-2ia~? z1o9(i^uHCn*05(&?Nk+eomhbr#C_#vSdKN`cXZcmtU*{fT!grH#TMm8w(rVA9Q47= z%Ul|u_YGbH?Eqq zn0?}-{-x`xAf1}$W>(>t0nuzG?<;CkXTl6TQ(cOK%0n=>l{15h8FFlcIz5>0K zvfEpo^=)TPtyrDzCP|{RxRvwZFJL(`k~}~AiI|{TSP%KoR(zNx^hgag3OPrl8) z4anNRRR)*>h92U7H+u|l1AkD20ge<{6~$ccBj8l@Y2w4G^Tv?v0TV!nVhnul;}_Q6 z5=S5T69QI%fmE5PI$?$}kikA$`nUB~ z3lR$E0?D%Yjni{DnUVo(m|-YhRnfr0*lXVq!Q98?h1@nACGY_2;5_BT?S}tZds_5_ zY1GL6lxCk#1UQ1P>bHMlF-byD9$RQc!-FLNVyNL#S$&9;P(~y)RnVlWvg~Uy4C~tG ziC%ZY?)Kq3~T)Cju%ykW-x zqZz2BB-DsuDFq;8-QZdisZzd`)hM9Nw{we{3oZTyWrY=8f=^wx|MP9rx5uAefs8-F z5Ru+oj{g5E@129erHb)qov45g6X@)K1Tf+gCQw+NT;?j<>5u)LhQ(zj0FVn<5g>aD zj_!gU_&pGa=mH)IIPk1n;=iZD_}>ohbR9kD+DgR(GSx$q*U-TJaXu8bKz7Gri3;Hn zM(_F42c&*5>F2=-DUiD%{KPqy$?r@m`2gEFkQ1;b`pWjDuX^r;&@2Ai9zai!AP2&X zh1t@jcnC-qyUGUS;ojHEJO)n&*z)wU`k}#!Bu}6Xb6%GHQ>+70aKtC$DWk~qML_Rq z`#Ho#)sb!e(kA(0UVoU{J$xtR<%sgpUMs38_Y>@M1)nm#|0ht)DF#ZNx!7C)53H&{ zGHBwG-ulx7Kh#T_rknWEq%?K#04p$EBtITClkWLA+(Hzw<^G&cDKzgiSoe;%=e4|> zjIFphgLTKEqaTdXgdw2OqsqWJ*V{9*1v)bU_XaVANvXhfL;ll)j;TrI!vp6BU$*1} z!)Eq?+JfmrZ`&=Xkl0`fHk~OoX;wGXs&NX2n4UL0wL@Nw!zgeOE1t2}O+Yp2n7aHo zb7ISenUS=`4j)^i}FFolO)7_5wqctrJd2W04WDaV!0_7 zL#=r5V?}C$!3*lrDk3F`h`8_4#cits#Y_D6-|jC+T4fD20hDq8%?46N-m(0QoGM%x ziUN#RYw^WXZCf_bO)!EA8m2s&C+&z-A2aw2A(GsiA~;FQO~M1ssZflA-Gm^J>~ts0EOz{+ft4q)&jG_PYT>Zynl7exxnPuI zF5L%bihQ7l{udL-_5XLT004@A59521LWy5a{SNVP75z5Ql}Z(dhQnfq<{>L^z=U+g z2B7JTi7%UcL2z*Y<K6(jc#uX#zi<`b%xGeUV1MSeO+=9p9o#W0PeX?&xKt9Mb->vw+H~1hn z!C?M0sy7R6a9e*CK^W?luk$!FML~e<_`))rOdU}ej?A2>6)3Yn^nBW^jszEYX!;eX zm;vm;syrstuM0wQe85U&3o^dc9J^zp*vNSPARDw>1OBwhWPdY@4ese{mXZlGiUF$S3K)kYn@YmYYiI~ zXi!iTa_YT5$A35t6ULDr?v|-JePQ$0FiI!}Wh=90(PEgTSW&;l_p&j->0hqXw<_JZ z$~`+?rTs~Q#N96We?&c-&8|+rdY0j}^4R-Ew{fda*SM!ohV52Fs1vT>+-`EvEwRpE z^oO)x?yFpc2l$TZE+`DseaIZXpHp)a@FQVRq%EChi3~CU9c8DU>z*+clpK;iV{F^W z$a+CXe=D$W5WMXAhqt;Q_gZoxw++{tWCRcK`p{H&o?}8ov(rghg(u)ta|>7AYh*O_ ztVui_^?41VKd;<;b{;T~+;S@N(mRHjc~Nd9l#U5z@y~~v%9K0AP|xiI8PW5?I1jOR zJ#6xHUrSw@HLquBmNo6eFYHizSKU?qp-$xODSBnK{#My(hdn9R#91VI5m|rf(I7BI z%Z`>4du3GGRA}vD=`G4mx2eo}^XV>kw;np(8V9dj%T}=PTN9SocCD%k-=BtF!Gy*+ zXU;=EoC*8`T?N$Uy1#PGW!HY*nc~p_g-#y}(yy-Uc96$N+2xltL5R>xlJBV}7%`u; z76?-`W5LwH%7g9!A6v;dhsoZqyJP>`fu!!u)zu%zyH?=5YE1?E7RqC78ml$sfy*c;j|+|AFk>} zfWUD5s@}$4DB1J(u@_o0c9Sc#AIVnN3E++$$eneNZJwaze!QHkZNPTX;%b(H2&sYG z^Vjv`DAlu*{qTqM6e+eo$SqvprXbyE9h|1|tcv-1T#Uh4Q+9ex>HRV6OxP+!-Rz&r z&yl}aIcou|q6)XwtJI+R|2deij5DwtIB0zBe7_zyImufbrjga`mwyvUF2#)Fp<1Mg z+t}-hc`iZc6o9enz?&FA4k3sXrn&?c^e1z%Syd@u)L))-^KE}wZZxE7Q0$U`Dr1-L zMWu48c3+jG61^p1c0e%jXwJ7ZmTyPLUV&}_B+1a^7~o532`D zHe0iYHJZL+pp^3|@7;qlVSWDe>=a&bUHRE#TqpjsgJGD_2ra1GOo;=;ctVJXvP(ht z-!1{o-vX0`LV7mk`&()tc;Vym`RX>FfpNt>s+C_w9=S6b>t9-WuQ4k-&a=E3T9H`y zTwVtXc6o2CGEnO;VOxb^FKHAp@^?_hCqZiCy#3tuPw%$lq@iO*T3j3o<>_jZw^F7i z4Ssi3>7_Fdo`OTX5wY$;!kDs`2&r<1OFR+m@VJCRhpe_4=x)ocYqIsUX|?&?bB|^Y zOsI3O-pp9gq1)^^NM{!&532ceB|_fz;ucC%LFSzl7Cu85Xj(`<8g3CW_MtQUOi!LM z68=+Sx9j7=uhm84MC2LkDa`7?{J_TCxQZAjOQE-^=47v^f|#*Us^NL$^MI6zEochB z@C(sws;SD{)p0|MBz3VYP zk5xTRD0D4kyCt^cJVOZtPL9Yj8-?H1RHA2&;IP7dq}p99N{sM~q+4z1ggxXfNyxhS|j#CzX;*YW4vsgH3| zbueX^u7Q4zp%l~r)L#wR0yyke$+N^N{z8Njh5K6>Kzk}Qi_Ry zsl(oShDg$W`{DJ@=U@2UcEI&T@+MQMn#jfuE0`0gGq$Y6!RSoO1Eh>*lMi5Z@|>oy z>ur~h8(}~#f?o72_gdH#z5a4tu)KMy!uGMD`#P%+UMixBBHi`jAMS<0EYkYY+n|bn z?@bz;9L9gM`8P5AbIAndYg)T!OWjVn!7~1)_A{8MbgQ?WUxGa1yN1$#FcB&pAI|1t z?yu9LGYZoN(;xGkmjeiRz@IVgF(dU+2{DT206!e^9oYK$FLTI9wkl64ry)hHtegMw za1^J?VJe(Nys)O0R-^XTe@S$^b+A{Vv(zB!8SMb~+ra5M2!N2xM7 zo3#bAUgOkqgbHUAP5r~MW@weMmF1JkEANRJ9#3)+Fa2W-Nl;zK=YS!KW zeN>^BB>V#wJVK?6&WT&~({W9WMO;UR1^u8gAF!^;?0?JWBQ-f=XKwg)xU1erW9p^) z(^GK}A3%OA zu>3HWsweWFk8905P2^fb)IRrp1am{riA13!5@lOM6f59ePqAlE;NQV?h_|$vc4NC$ znnqZxTBj>9+|%;-p0|^}7#NY73ta{k@N>3$5_iXC&((#bTJ~n z*Vd);u#6oHCT^}d>3{Vv`x8N@2-PeqkusHIx49p`Re%CpGRIRe>wrhcRBgm8nFs;) zIB!N%iD{(u=i?t0XhILo)(fKkWvo0wb{D>%Bo$VB`eXC@SO)_See1gLb`$f3g+&D~9C-pDlt5To&i$pL) z1NsK6pUnILb!Mp3$+_q*Xr2LCX|d1S3ma)h2sn6jV9Od$^H>QHEztF9y7}SKn9k~h zAkiozGsZHbqjNqBnZ6gsDct?4F#*BTXlA8idiqlAmW|w?1ndY)q@m~Pdyb`bRr$A*dm*8t@MGhL})c~Y`#(ddu^sTYm zJkWcuVGlxPoz;!ul)o447I6is3<4yNri7!#0>yv(N4Yh4-5D*F?;Y-KM|@spg;xMt z|A7kPIRBs82iBi*KgQ2Lk=VR#(yN%71H(>W4)~css|MbJW-Y&$x%Mm(r<%YM9sVa? zYH{1=@YrctU;8wswY!O{>BIl7_86`FDdM~+_$amioNY&c6&hXg$4fj@W6@1r6cn?i z;>!|Tkpj1Z7S$6hzf_(k zd-)*lyc5!Giv{&XCyPR$1+%UbuI;p-d0DpN=tpAdlUK@EBD%eL*o$>ztt%P*me;}H zGP#)J2JtOJk`E6VQu6jJI+&krw~N$qb}U%29Yi=C5<@U!`{-BxuDH{)3MotzRTE+Q z=*#8v^i7w3IzJG?p0=>YmHI^3=0~j%vO*O-cXbJ3d=5KvBDW zE`E}3VPyLGEt9s06x7FFf*t<)QoNQ*na6($r2rA+bd=rGdGgZ(^d8O~BpI5d<_)=V z4h;!k9?rTy*&oKi?KTVuj!k&EOJSQN_&&I8tL)mhY0zZSpdK`q<3B1eA)^0X&)0{x zvHp+oas;B-3U+BcHfzL_m6rkJm3o5`AYjaWcYimnjF|rcT%bLGmQQ=dK6WMskf4X- zGPqMF`lS$R_dN!~8CyIz=Q$$&I$yhZCpndc{pR~s$89xUx|NL3L&9|5<8L94P@NT2 zrOqe!nh5V0rRnKQmW;sMHs@F`9I3XdDJ{}wkMM?OgZEDz9<94F~=HxvWb(?%uMt zm3_SCp*(Ylz{m_8cYHZ_4v)XgqNCWf0OT6JppgiFmD2$sk0nl??1!C=Rmc6CNxLo zD+-Y%!P|N@n%6giFtk}Qfpn34p+6NlvhAV7cR&J1B59WLtY6W%FbUF@T2h&4k|fua z2j$(ml2nVDT1rkw;MYA4F1ScRL^jocm4v;b8t^_}=ehJFG=Lif*7#+>xQzJaR(T9y z|L7a^TovZpHJsJIQ|`><)U`ER`_Lf3EuiT(fzYEicNrHEo01w69p!5j}IR3=a*`fud8auzv0=rXl`>AiQa)2sZa_x-d`2Hd2Z;H>>{K?2kB ztawsFyd|~+w?T<#-+PZG55Us}vdhQ23%ODkcpIy|tnEwtu%ym8-3vP;-RczeK6`(v zm@kgS<{0&xvD7(%VORI>s$jlZ_sWd&&(&( zCF}pVB+1@_q{umZdVyOJCjM|B$j6wb#5uH%$$Vd|+G$0&amQt>(}OQYq-`G$!{Uay=T%(|@$jNiL_FG$ zpLoqA*AXP?7_~WaM{gQLj}uRpuTY96AGchL2~W|b5+?WepJO!!H8Cg!b~~JtGmyEl z7aoD7-0gdAu5S5+X_oFy8e;$Ri-nKhiI35D{J*P9+s2PzBU}&gar^uB=e29Zn+gsfn(rgOfd{b{n{_vL=waH+Yel{yJQ7A->+R@ zT4oK5oanYUx?ABy`58uklFu`VL4v*eWQzdb5;_4I}g>71^=yM-X|ttso~fENORIYHpM;g?Y})^;c^OTUY9h zSrU3W)F$_{d7GcA-#t!eLX&b9kQQ9~sE1kVaOLB5azH$C2@XrfO~1s3HIO9K*nuiO zW}e`AVgEJe_@mDhM!&0_LY-WHPa|?vp*jI66|>w%m?QS!eVFIfG1k?OPs^1Z)z~f_@VMC*-00p?2r!|% zoef57yB37O1ij<%QAus@iVozcbYq*l_s1dxFa|~klcMCcpwXW<&%iuT){G{zac|B~ zeR`oXio3!!gl8L_0dmNflQ)0WKF=9ST`#$;gwp$URnp6Vd~{3(`8R-msR94O@fN|{ zXs$_ZD)}pkUeHKu(7~V0YO?0R*eiqEpm$~e>)TM=CKwU}Kp5a(fen1*L+54*rK!gk z>RtHuffkhU$?LQ4SB8-5HU=RXnUe*WIFQs>oMljlar3}7rQIEEj{u^8>jh0i6iQ*N2qxH`)NT79$hlxvz>)_6xsdf;z){ZM%h<5 z{2j$R%KWeT{?s_s6H;kUa?(w1PK>HVka|@TgEkD%OL?Av%F+JFoCQRUJ~qCiqP^DU zx(G{n#~@=N2(R&+GN6AQH2B9z)pue-hYG^Ao-=52#&Ar(t}+WRpZ?f|?hL(N{0*k( zM49!Iv z`*XYJORqa+^>T`LL#SBA^WX&zEEk#J!S?&RfVoXQj{mvIXYnlfi_POW?`>Y^(TK(j zT*BCOY&{dkN`ryoif!3@*1g6p9@m0}pvJ*1<$>~|6dw40RsHEdRfi@@56vI^9oaCs zq_ZSFdKEmR;(>q17hY#je;L!rlo;Z4)(6iRFdySSlA{L?z;H4UcqL5dVM|IZ&=Y5` zu3%AoA2~DBbu*(@^&*~|WBI6cFhAZ)Vsi%|aPD1{T`4G;4ZzoqIqp9~gK{4Y^ zt=qRlQTf>4g+I9l=?m&jNCUQaZEgU1)}<_32$QC{zrCIa!_AP!9v?FS4VtFtSby38 z^t_jY8-3jS<;x&V=iSv2hUu?&@BqUI$o3&?yiDJ{Bdlht$PU|@E*^AqCZK z;7gpmt#rQkUP&SbIo>!*qDlHA#%az+TTaxjV0sWe-i`bE!KVrzaGIp0rnJd*CNHIS zmsFxrS7t*q8u^q#6wV(7FO+>9L#clK9|4w4afU}PW4Cvk$O%m%C(A|e;CxznYtX$F zvy0{hM!NEgnj6<>0TfB4q9S}p$USn~sUZ&jMZ-P`DVI2+{f+WI%d=+v43(uKK!X}& zDSdlXL9MSrk0WgmZVae2>-E3LVxCK3k)vsnO)o7L`1o3QKFq5_hGz)$QRw~Qefap9 zKhf0@??}*N9N&%rWx}=bxgOQ(a>k)x(CH@fSz~~_FuPRETa6WyR?9P~xTb`4Py19Y z^5&uX9Y%i)ZQRCOuJ0b8=zn7kn#Gr2)^avhYC6q797)Mze{ro(DEoablO6I|_l8jK zwXm|34grU_>gO!92c_m!N@3>RI?i#!;hxlZ6^_<>P)IeVt-c6o5fFbD1}F;PkOu>_ z8$+c8#M^dy6ZKh`8g_7+QxY3(BkZb3ZPVB|jLBD4UN;bn78b>53;T>Z2`3p`sWFYvTH#r+IW26vu+5d@E4Nk=GHI8!yH zTJR6F#yF>!s<#PM&M&_J9t4_MwcnTgkqGKo%}&0@bDIaAH>*MA{&~jx2k;oaU62|8 zMZJ}*C}97kBYA9Fn^116#@%SSvbWa5EmLJaldaZFS!!}mS9d~Gy*p$PkQd!6tUm$Z zfmk_1=>Tg~4G7qs!;jV$_s)gJCHd$g0zXdM&1O@p-CIa`X4{$S`mvW7-Ygsj&mqA- z!BKA&2^%b2Ofo%u`%2gJG5zhTO*Vg&LFj<~e4pyCir{NU_UBy{R$K!fdW0U&xfs*d zz6VU)BuIY$Vs|Zn3Q;{@oOsE8_*e}Po54K}#^Q{~*KsQZ$oPai$&SRP8u-@wB zzF1{rm&E2j5l10G1sB7t6#WOacOqD(DgHxpTYowe{&O8`%6`l4b^GL$YiN1(mn;fm z(L;-Vlr8NcDD((?X7#o4lKlZcxkYjF*prch)ZZ z*iqyD8stcLx?BN_j5+9N&{XD8?aSbR5X4SK>{{8uk(RuH##Qw2A9zktIPbIAuXcS$ zN&7La0uFcj*sydn=x!Njf$k(w!C|gizYvbrvw-nOJ53cxZGH9-;}Y-(b!;dX1_(C_ zYek@bdB2idSDD9?h}A2F+mj%E^JJtJTbbMKbTaMRT z?4NGD*`J<=2&DHtV3iH|`uU6Gc}DQh(M*<>>r`r_Cc=I{mJbazD3~I~bnc?6YE&ozP=LcD=2?>%1Mf zGWt3~Dja)>7j0X`3?4IQWxe@(=2(;Uk?%qDfZBHBv-yx>SFw!o=-LzX0c!W@l4ri5 z#-b|?IeOdwXQrQGI&6(p;}H6Kr+>(@tiBe0xZ-eiMfVvP@Wg%nea zwzv6X{;qx$Bl0dikcky}{M`MzP37o2-h!iS1s%D73cJt!lb;V4*@VZuL*p59#Eunh zhqTn#Yy@>je@3c4_*KZ{X0zckvyUZEm)1TELy;+mcdCjwWx84Q#e$8u80<@aRZ1oc zsMoEosI6C}2=10y9DNWj&cLv=hLZE1Xz)^y)3A*VN$K7^pt5UOJJEg+o0#r5|97E8 zUUuAc_ISWEr*+ayvm>ih;^>mZS^0i*F1}_$ z^|)-Yt8Xo$(o+8hB-Oyn_}B69-VgP!M#oy|i|XbONtsE9m+7q%@>>&r=)bo)iAa95 zd0SNDLCquB@0Y`A9@1W=gP_k;FYXa-VqDzGO0^~HYxfw-L){_U>b?O6k%{|_^yil} zH0G|oADZ2pDRS+ICpw8PBgiI(UmWdD>7rJ_2}{T6Tg1l!O(V!|AbTlaI%)6jn%07P z@s`HID7)>`tf7ojz=@x0nH7?gZL-ZT+%x%B(@LfE4xDNjo&DN0@rJ2IeaRJQ{gRgh z!6ukjD?N=@)BBGqK6p+l{etg$G=xQSa9D^?SM`NrY?^Kpa-`VoC^)|P6$dYB)+BYb zU#oT8U4CJK*Ex4ThRI;anRReZr+`@Ec=2wfzSj&#hQ*7HZS!9%>)Kd4@ zyJEldPfg`yG=31PJ}ViT{qTX&=woJhQUT$oht*O#aUVHn3H{|X?Z_soMj~&JiNMFE zAb|v4yC4a~!jGd4P>U{;wQVPpJ%MYeG}Jv1svK&2o1~d_u-%oq_Wr|Ugphmj4d*hA zujTAa#!sjVKj#~Yj@2Gm?vCliec+r$nRZt_QC3M$4=9Vf4e7##^N=o!UoO?Ai1Cx) zv#kJYqQWU8BpwN>hREN{+T41A?096Jk6rE+!x zKazqNiL&_f;IEuIq^Ex_0tU@M$7Oc64Epnq`}5IGXe)QsAtB*C`(k@Y#X$Fl=NxBE zwFYtZXs3x%NMFqZ@F%?MA%4Jn(Mj@Ff{2XP5USX@wNrM7RFBQ>UUr4nVIFjL zlWL6EfSUF18Y(<|`|?<)^1y-6lnj_G_y!*Rtuxs;A|>sqfd(x{;$8U0$*l;)mnGW6 zd~oIJ0|r(d9kwo=xQpQHbmz|GLZ|5okIpskQ_|whET-DEWznwIF7esOPds8|lOa2| z-W>fozwF~f{d#rihXiE{VcMs6BmH-`dM%n(8b-}crPjPb-Au$z?9vtp0P2Vc+r&Kb zut@ws8SiB12!Da4>@-4qd6ju~=TxAHG2$)JpI5LRwS$bnFUZQNl_h%E1uyC8yS!zk zt5NraRkdHDPoA(;-KB9n!hm%mJI;ZhAbopwET277jk-Da>QNf#OuZB#77f_i_>g{k zb!R{dc|jz8;a7S1OzRd#er>u~rReYZyhASinlYhrb}OywU78YY3iT68Xfw}moE;Mg zSBP`z*>9rIp5y1!^9_X&f|@rfwFO+w*Wi<^lZ6KS0zYZ~&~NJGmj$062xknLKpPF@JUiLua=TBdE+DF*&d5)qtn0oOfno*xwCn(~oOsB5g~m({iu2 z*86&Q-5YmYl8Mc7_Kpg4yCgYLDddk>`K*@1@i`==GZGRe^)NtsidrLExI3OrX=3%+ zIt<1LyGRhvUQ^##WP11+oIg0CVB7H(zcDFD55$YktoJT9Ztna@<;>hikCCl_xCg$? za)a}=zPNW>$^1#*+4(Zb!0^oGM55kKDDW60IjX;#U#K>WjpjGe#5q^GwgVs&U?XF! z_m!DNx$jLkCo>}NYmmY}LwR?0kIzGLTdC*i4RsWgq7qvSVj?RW37>tOoLzUaipF^K zU~XIJEB;xqwp2d~vopxWdL%WW9x*s14BqSOT)+HD&+}9K4e-(ohQIj^X=kFR-^Q)b71#ltZ#NLhM<&;CocZ^?b=&HM33Y@n44@ z3e&~haC9+#Sg42SW>UC+YT8i`8{1w*>+0%qY7qlHi85sT?sJ%+b+y`%mhc?XZc`FjcrE#K*p z!^O;{_HaiJVAwPo!pmqCh!$wTC4_gpGskm0%@t(|!Ar0L^$?$~{a1}IoT>4>g3wic zg2Uy-y8G2pcRVlf{E`>l^0YhNCWCuA(uT+U__z^?bIZ>>GKaT(0cQ<)g~7Yt*j~ug z>ci{lA+@w>&q!t?b+~BywnX1n%{v^di^M3Vsl&EB@14QfUhjwg=Lk!g=qT>ukS=+L z^!hHM+hrb@glvsj3IMDH6LhlnXSDP7rNg3s+QgPU?yvI~KcddVDqMFEjB!X3AM-0`QUtE)3BYs&AcLuh-VZFS89tP3#TOsgrq7Mpwi6LLPB z-JO(_NWs-JXJGN_&u#6J#F2F+hwH`JZjsZaCx!jex+q$dnh;0 z8{lk`!wOP`(GwedydEFK$;ILs_WLzu`mC0vPL}7@=WPvgNj!+&3RW`6?}}{HKoF7T z#6Ez(*7FI%gmW{eaGktPdza>BPSf@1`7*vT2xmrOf2y<{%$7z%XdNVSn>ml-B4ZL} z!BIHuUO3L*MLbGRDf(vT<^~|^o6;x5xf+_{YAI$9PItjMb!NLCtnT9?2m^K4I!oqB zI9Pgwj_2kjXiBrnbbix(fq#{@%Bsem%TevLAb2pZROs$~C1qne-f=q%_S9-|ZlNT2 zW9PyMt|e}EkCO6Nr0xR|eb?wMv&J?R5wr&SR`mStEC9X6v!-n+4bRxKY1aAeAhYJzHgXD4OMlluW+R;Qix?}3j@VQJpb!w0m9x+g69UlG7zzd zfggzpnG$xDxb3*>8LXqCCo*O$jJ@_&!}@D+jwv>P-XBXfF1b)E6G+ApR6Kj|QlXh} z&}l^@!Ld8h<=Z^A*Es*OxfifJAcdKqn;bKL`!TI;t_`;V^rO<>eH|ap$H2x%lADxr zg9_-)uIO&iE%pPMZp@$i`h2z?AvWEdGIKri#BM7hzHYK00HChA2%4Nk^AV^!ZA5}s zf&*R!Kf<$C09yauD`&y|P3HM@@Txiy121&V0O!I+SmO?O;YL$WfyQVM482C$l+T~S zCJ)7Ts;{+jgvq{!`@+B@E?Qe$-P^A?U=-lAsb0Zt-B`4?Bw9%Jm&j2oFH02U%dHlw zSy3r-dvB~15|KTfoTeymQNARA%nk-G04!~$2d)}-js99GdKYIcuNbj%yn59DgFcy| z{N3kl^wNof*~~ROaV$FyL@H{vg(*Q5jIW#`NGUyP)?{#lw;_KoTG$2L151!T2id1Q zl@iC&Va&aq1{O#69_4Tkj>T&kMuFHI>c-YuFs{w<)zV%{--FQ`W82bVH2#7?ZSM}DRF@U ztKTN*T5K>Gmn1%UWHm2e!5_ANT<*%CxcJouf1=-OGk?i_2Ny)ZyWv}{@2XCI-*xj% z_!blSr=E{V`-*B1x{o8qtYBqCNGm57w?kdth!=yQhh!3IeEkNX~Y?X5M4peQ=8%v!iEm z<1*Hdc?ZSPPI&8%6)b5<0CsS$E%|Q7!9z%OEbAFQ28*hA`>vF0LK2;+t>fc5n2RXRE(}0)94wQ{=DmdyIMnw-K=qxMXC@P=k2Ra zFGTC1Q|*JbGyF`MV+Ez9o=HX{1Y<|X8ih10F(3@B9O;K8v9A(6qNo3Dv;6X+`l3-b zLEUfo*w8(N7Pgt@MT*AZ8&DdB*ZU7i-(B>}eV|xx)(MyawIn-H<ygoHbR?JQL?0s=tfhhO^e5W$la0L|J(4h+R;dquo8N!_iaZU;87 zeXuob$QDEc6u#s&`o&_&-u!@Y(H3*-`gMRn3EdPMaOIZy1(bXy!-aF6UP>Q1yqJeO zdV;4FEBUF3i?u1T*lWdYhxN&>M|98enha_UaiqjF8<}0=85Um!t~{$EgQMW;Lb9!H z0ITpyE!QKEFM$;LYL)+7l$ko(WCSM*1fXM~5juhuBEEfw1a#<6QP1`J)TBvT)v`On zShDMJTqJqjCWpqc&QIW)THxL+dUp$J)*ztPJ@zmZ?&L}9DOF=bExNpDUIU(D>R}<~ z(i8Pt1J;J|0_*@{&B1x(2-WYl5+XRh)bIcBWth+e+yVMa_pr$wH^bGNWRGw;b&DJI zz^B0Z^xS2s3#c5%a%8V4G{|6ejrIeC)UeUM+t{aFJTD27J-GkA>W%BDy{gVLe*_!x zYvDiGY-vC8^f^fn4AgQAR+~;9%cfQHK0XDy|2xACKlA-N@cC+uqQKnqgtH6>L;6cN zO|LvbU%)G>+cb7s)IUQ1rcJyDsE2J`rM@X5QHkjHPN)r`l#0@F(eoUJ6u|@d5duwr zw?+$=H3UwuB95!()x%rBAepD3oE^4y7$#ycAG5zz{o7%+rbWxFv0JSGa*3PO$c8qf zkYEhL8a4@C%#@0|)ZtA_hvjuW-^WMCihG6gYhkv(I^aA#4_;IAamE|& z0H6#BT-5h8ZsM7L`k#3ohKAO&eee-h-LJC~dNMqTT0&whqak0vQaL&}-8e`ozOVZf zFg=?GFR;@9iutrGSlg=V`|c(@Y(#&5v_%Gc`FX~ZLH23q#GneuS3gZyZt*u_eGQk0 zI_3)1lY{`3V=zBpzYsla#zQ5Xrx&%;>eN2JmZl35TVv6~5VSy0;J~i-b%=uHdvWJn zjcZYmq0-Cbn5~@QOFYlfez|rsi%Mql2@uIL-gwf+2i|qGgh*QQ#ZuQ2sHcdX#vHsd zNeESu(wAKebe=X14um{<8}Q|Yub=P!yt`QApbFGh)jJuupW@`kmB$TiFM*T3ee~fX zd{AnQ9m5_yfEz&0S~g6u1sv}tFO^Q5l*-f;=z4dzEZXPl3{bltdpDw=0%*BJZYFw! zT807drN+a1S5COV!SHbgC7&*jAr_;fO?0!d$xOd;xd2{Z)A;)#J^dr^-4}A;R!$y1 zjZXQfaf~M&g~rFTL&AV#9zxXNb)Cl z(K5$fDnhKE4Bzn@A-CP+X=ZVa;0(%vprR~0K8N~jw&GljE^7m9GqyG|2Qz7fPVP@R z%s{XI(s9xbu|>RyCVe$gNl*W#agZIT9b8(*1X-{pkdrT+yi64BgL@{9es4Q&Jxc>1 zra@t~L(Ow_fS^XEzXIojkVry2YOe;|y4t+Uk!1rs%g|6UKtf$JsX<(FZ4DL;0@+d0 z#8xJRw=IM{bZTtJqt|v7i#3L2cXqAYyoHCB8?`~G#P{ZtZ~z?eQoPN)C4umBrFrT> z>}OunuVr7^59$kj42zAAhtX<19Ds2VFa&XRuv&cAp5hwTuXX3)Da2s;AH-==K@@~6rlrHv5Mc?@D#Wvt=-gUd>k}HG>7DpBe9g6) zU-;f6p2xjo6t-x?ShqyiU4AGYCNMhHgkD9xS|V=&3~VejRwufKW5 z4n?P(#jv%sc~~(99`vjmb(6<&iL_gc2l^I^eyKu@j$K}X$iZ*H*{@AK$SVt;t)6f4 zJYZd)+H08-6)g}onU*mS@q9;>#Em*zrCyw9$68P4MZ(@0*?Y2zL6t2m~kAo}Iylo)jUQ$_F^Us{}m5pxhw?+^X;>WwO`2 z)gTO{Z7Q7E%Wom?g69|e52P=flpwM+QrTWz>KK`rE*5Mdv#rut;ICPU1nddTFyJ2) z#P3O0BAYVmbkokvVCw1yhV6MWZd>*jNZDUL2W zOjLyVCDUFr8^W&b7NUXILPCgUzxSHbD{0vSE-}%v5p0o z#ORa%8}&Ff zERDCrM6c!6>rBmyP@d^0^uIhaP^QaOz!6vsgxvo;JR+dW_`XwO2~qoiRXGVhZw%5Z z@@o(@i1w{AlGH?9kgr5a@Wcg$=IO-UdIHk2pI`%PT6QE$=xm_7k&d9Q1QZ`Q7b1y| z|GdA{srP?Yb~(_#GD#Oy;|5VY;2<9X*zwVrbLO|sYmK91KvVVCWGeqMwTRZ8V{78| zL6dm^6KL8!buA0QXADA>ooNcNl~cNyUJeAFvP5AvaHi$#49x5pk4OF5-pYbhTl z`&>=FvfR@KtTOmdlJsoPzxn8vYH3M1N!1g$qS3!2_N@RxF!g_6#AU!kG0{EirSjc! znyqvO)d}b;+eCp&TOm4e5II2iAA+rcBl=1p=Pi(NGdE@dxB`7Kx&YOV=7#Rs^^b;I zdQiXg(GgHt%aNo%N__V7H+4&f^oD8@1N$pY9zfp=dGK#_YSwRhZd{N88Re{N^zJt` z2N$flzRFLClicU)Rnkg@ou+d1XI$lFfDqO3BLICe#PB*A4qOs(x4U#$LzSXn!kBP3 znp6L!k92i-i{;CE<0rAF3@QP*+kY9*m3$nj4}FVp*$2kzXsVUo?<-_;^wK&-L z6wto^4(;xlSd8Rk80nrhuJGBEV0z^1{3TIg0Iuo8Q8^qOQv-PV?^J%A$@@zK8_8+W z;Dd6jLyj(-*90=l1EK(k02#`9{s0@}cS;PM%HqNq8h-4}SDC{@cHmUI2A?^C#bf;z zl9~38%pZURZubUMcS&VATTEt3k+^%cUQ*_84ONIEFcPTl2_>s!r17HiLp5cASqH8% zf6>tHzqQ^!#oABTR`cAdL&Qhe`Q$)eE(xfa*_GjVdc~X}2KT{{0apKbhmv{M;F+Ze z6=cVinjtxE1m3Cm&T_R2@!Z$Sa-Nwx3C@nkKFm85?z*4L3L?wCcMk9_8Rr7RacDb$ zNB}8`l!r|GzvTuT;fIw^k9FeCU;8uIloO3EVyKjX6#(93cDz7(-a_>+Co3qF#HWA| zi0pvwNSbyQ8KNPy!N5&w$-d~J*$0#wE~vSFh-Vev zp9=?dBKNj&9gCqFs#oG5Q8X&_N*AmIh*PCo-6y^PTq(`x6UHS57>;E}a3P)~^O;;{ z8^UEl;bqy;4<8gA!WzmH_s2o@2a2?;^96V1p5$}w0fGa56kr_-_~+A2{%5IOzjN!1 zc^Mv9=@WC(t>O1Y%%e3$8$td}mg@Wj)mzm5mz&`Mn^Q;9EyO?jxqDU%0-tnwK108NS5sA$%3Bs&wHr=`b#6_?9S$mgvfuZ6LE^K<4cmjjw5 z?9-?j9?gp1-q854ujt3*WP>0)?@#9cy^|I42^&Ykszw5+r=5pNvz;dg35mxuwC!fA znqL`albIMm?grT5C;9Aq2Ww)Go5A~+sX2g3m{zD?(Hlh}I`Ge*iOGd9mZ!4~C!{v_ z{(l<-mfG&aD1NyWc#A86o#dDPo#^D*iRzt|wWNUfhqCb2znfa9mB(c;oI0R?B}abj zot}I-yyrQV3^U+lBb4|0V{|y|nNOXHa!z9HNKYQlA)FFeq^bkS&+lX^ETWvSKD6O!;u}tzN0Z{35Ml8UEaI|Rf?c^2(k76 zl@$-^(G2j%*0*HRKmMq0oDt&bSv@3I3StfFqaWTK1;80p z+padySas+Dt&Hkh{9L}PepZbJ8p!dK{dMlCB3E3!G-CgGB&gACVgPL_ex~)}Pftb4 z2+F%Lcl!Lb9dzQjje#a)0Z#~uCS5)f_psBT=5}c(A0+pXi*Y9D+7MiOn`bAx^-1o8 z$IwRkB208S5m#UtqXhDN_*N}VJl|gds>H1@%aCJ@>lPwtnWSHuEW>OZe7Hx@bTyM}pOs;CydGDN0;ylo`Qceyr;J1J5@hCuS2uQh<2h z6i@`=c4faw9zQ@EBAVy6y{=!+8;zcHKsmIW5YojpXXL13=M)Wu;q#O%faT=!1+W`z z>=juoFN-L8W#>;(t)+O&&>_X(5%>t119@Oc;ZytPd|mnkZqORA#F`E=Z`pdgpq}2T zh2Z6%fcph<2{>EMbPG4ZEk`%+f_{P27{xVd;7l`1^VD6AtqYd6#^MSu3X=f(aQO6uApnTDg@?7#i)z6R_#US zLcTZIsmb{%un$CXnx}af#1z=Z7s_}_1gm+Mt3rG~tZTm!vvGGhviq+x<`bD=PF*RC;#xT{S! z#}6I4@Z%2_Ul+C4` z_hs5F(91MhtiuAg-Q~|;bt#<2S+dO2Tp;3EAir7Lao_7;`s;bPm176RIrts0g>T=^ zEH$D5yx;;xrrgn1N`uhXYk|7wx~}d>P@ME~BZQk*8YU)pMiBu@JCrrBF+sIQ8^2p# zU@7X{`fZj!XHSzkPCJ8l7f0uhkL&iMQCprTzoL)eqwQkj*HyN_qo{3807_`uO)N7i z=dx3c7{@tgA2xm@KOt1-njyIA+75K06Rq|@cgyOhRzKIu+i=g;EnCa#8!v3y3Y9Mk zzh#A+7DCDXEB>=m5jp5q(foEJ2oWf`=C6P8s^~h!jVu(OR7fFHVENppUkehgx$+pb z2t&7LB@txRr%6?JgKjK9AVHl-@Phhv?*hv@;I5Vq+guxIwiB1Jw3_9<%XaFDz*oXa zZ2+Z|IW|Xr5Hh##kAVEi(XF3u1>XoSS=H+Q4qke|p7mmio6Wdsjn$oD@q=Xj7xFS* z+XkI>BE}UcOmH5YNflaWQzwe-T3S_5>ntdW3>vU|hyS=E8cmsf6+b)7IFI@itac{#QO`+Y^tF@t)qpW}m+`=BCxQ~iCkRRUBGaL>B%VWb1 zN&sDr(;cD{)2iyX`SsGSXzRqwUzFT7Kyl5TMU_TB;=cT`k4gnOJLw)+9NzUa1M93B z?F41R2+%};@s}!{Dr2{jq0)DO((Q7zB6@ng6p>Y(2}e5|R48@0uc)UWR}N?_#J;k2 zF|mp?*L|*2g6Z$1OXIW9L4jqLkVLd&Z5=oz(*n{Qx__1(I0 z;mCS~W`6>H)b85@4tvO@R!e7;VO0zH5M@9g{;+3*f?h$_SCV(7AaZK!15}(AuR6nY z5}+FpWR^EBKOsvfV`pQw9W;mm$s3dpf|rwkhOj5(Y;`Ell^7*8{8r>B!V$3xza@O) z)A$?{aFAl-4P{?Tth5NZ5oFv?0;RS)@OX_u3He!V;PyWbtVKeQwW zwGz(iHHVwE18zmMh4ACPe^i8y$c%zAtTg1)1cR7XZWMvR1VgZEVsu^#O#|hPUx$c` z`kfQKtz-vkTBB-4*B)?=YNF_dTyTNiplu3I-2Tk8?g6$upUM(+QUIK4BBfjfdS~B& z#B@{;>1-3}s4qQDG+6!B4tr^KAGD}whU)H19ei#PF82twd~xI}#?gX4cn0F9vbRB; z{$Hn%mtXCw07!ECsYy~@S}}G)MmRy_#Jkck=D6`K9XRBhF{{6G|UfU89^k2w8d@0mnmG8IXjEVpqAS&CV9)+Hh z+a{HxGo1$*2%f0>Im~`@xL#d5)q1q0im1Gh)eX^2p_Cyp_mdvki0Up>hk*|kU7;27 z+aHByNlFC+$keYhWTu&Sj!Is>HZaf{cpuS|op!zbU%JUS($LVsj!0^TNCUN;@%cA4 zmGVH?`8@?H7nLtOha_lzmfxs?>Qg58HP96XWY^A0k2EwOSlV2BrruGt8Gc+)rn4Ll zB2s-yHwJo5J80F7_Doh2?K#}n1Ko$$s=K2wUPvDltpUa67kYX?a_wCU21yca%H-w^(y}3F9ovTf3@rC zC96?KZq8C+J>Ob+4CvlQ6yi?7Y`|HdB{ywmLC%h2r9uCU)&Jw}t>dbSy0Ag)0Hxr{!FV$vVdJ~Y^pV|4-sWr58bVy+;R(UM9 z8UfLzHiKjmCxup1rk5dW?T0l*xFQaOD_V+Xs~h}1$r1GjfA(8<|JjzEMBYI~)l#?2 zB@fWK0yar~tD=T-`Fk=NTwW?~GL>@fKq~HQbftfv&LKz8xKLnS5#~rPf#DT@+`d5?^JTJoxCoQ6 zvYY-dA@a;=3+`mnZ%$&i|Cm;)2|g7j^qXPThe{?Et2~CPTGNN4Imz#sG1Khu^T9~4 zlurbl<5=9-uZzBA@RRP1kzl;j@3$edtAocWmydqF<0pntU(6df9vGhgo=qsu-manE zSD^QK&^$`^t=}l%F(g_4a=8$%Hh5)_|h(otdEwmx^7G;`&uuCZhnw zj=z!Ed?aiugO+gnhld^={&>t#01lL~5 zn6c5TynZ2+@N;;gORDF9itxN3R6YO+zTw#L#8|vC>(1A9lVH!jt6bwLp*I|d~cvc zs5XL3&Qv86idsfHrdg4(3h+-*3m{N#ZRv*c(waJ26e4X3smw^vIx|6S=C^yRMV{(c zjJI7=K8Cy<@H*wx90st=^|^_t>l@y|HxG+?#Q8j%`XhtF3<}sxZwF+=j=dQZkRb_k z&QgOV&hU5{kWq2u*3-ZpfF$MxZ4dV+eeabGr#gmO|6$@{@58#E24{-o8hiK{&S^E0 z`87rI3T(EO-YLrIT`s%V1_`^a_M;jf8(oSifV`=U?UHGZFi(mr=w|~VPw#2a)ap@9 zNT$xDUg83HwtZM)=Y=p)3oa=>9A$PyL~Do|os#FaI4b*sYTy%iMv<-{1v&PkoX4{l zKeyeDMW(`@!`Ms8OAj7!c<4A*EWO|zdxdhqOrifBT1Z0V8hWF0QlBcCyr(Le~1t14}u?^D3`-hDM9Xo_FZk zht%A)54V(ldmoU$arl+H1puDE>t(t5`I&2{v?83f3P_+ zx~`m(mEHOz;&pXy^vNNKr-cRAP>n;wExLey^RsXx6dSXG2|In7gMmJC?Z*Bg9pD3x zJ<#zI9ZsokOF6CZ3ovd+a+RF>kesB8O8iiQ|5q+4+w76jYoFtDWCB z83AR_s5$%i+h5a6{Xu_Le)OeFnd&cYYP}Gj$3AUEjAq;cvOw|qCI=5^il>P8raMVg z6?^jMKt`ONvPww4Bq|A@n@as?;FaXeBHy`6v0bpT2>iwm8F4=spC_6x6Y>2?w`&o1 zAVK7+j~YXX<5qg{ep-AzQV)L3B;sHT4WTLwU~t#iqw7UkUo9p%d+yS?T%({obPHKS zOb-fo#WBXl-_`}7ObJ#(W$ha0uYmSaq1!58ON|E;f#-nRo7&pudFU@T(!MEscu3+a z6%v-VP!WZi>sO!(jtZa$^V|d!j(5VnXCa#EcKHE?-ZS+0>pqBpN_xmQ`P9%?00{3^{LW7FMCBZ9<^NFN7Z_7Sl@C${3wjqSTLw(HLKE3m!v-Rp@fXG*F`0W@_P2NlD@b~(F$ zdHSX*7rG~LA>LfSll&Oq!Ii|*r%v{H;UQyXmPAQEKQ|7gn&4s0bs|$FI5Ec^kraeD zFZ#&qqq|Fe#g<9YXF?IQ0cm_GV3KB*ylK-RMYmn)1o>@*29wq|9!Q}J88QG6x&M{d zG3STp0+FsyX`%|&k^`cfcSH#Q0{j%Wg4FZZrr1Iyij#}lCq*EhE2uV>n`JZRQL@&A zS5|^v{(X6e zyF>Moo=b#e|9JCZ+5{&{daQW zLm~JB_*?;f^`mwzv-ADy0vUs*P~@W(MbQet3K$C66i+ve8D+Zrm;={9oWSl!fU`y0 zxQjB$B)(ZT4FY%R9yPwx|9jLAeP`nYB@cQ9%AI<@pX{~2&*ym(SN{GbWQ>K@ei)3M zY7s@};s**Gz2C!ffG9M3vbQ{MY@5s-JXLk)p)GNwH0Vg5WiOb2n4SYl?Kx%wmwb8n z)F~l!L5ES$Oj7e5^lE!{X?4z}LO)1+V?&;tLlefqBu)w{Za8l#6AkWD&grbo4QkLgWA@;*Z+CysO*tMspB_6{1oN_~D6*CJ)JL9z!jNfVkLl2a~>681C z4m<@R_%ewqviE8Iq8@q|<*aFAPblaxW8yW_p}OneZypBq%U8ZCLUaVonXwZUM&>kP z&H_bH5vEiEe(DPQ+KCIbsLq&CjzLdI&8a5iXyvms$5l04!vPEl`j9+egPn-kho+fXk2? zoG97{$wWkkhtL6ek0VT5r#RS-V*zx(8RG5~RK12URn@%&@h%MeTPV?qY9UVhgGcbn z#?5)sU zE|3UHV(EOK7L3H$h7OcPxk6YQQZkH?MyfDv9sO=EZQ4?bCA|&3bc)=EAfT>N43H15 z;oDW6s!j$%MJP%HIB8NqR?79L^TTi;i55~K8))#CuOh&SmguLFV!(=;b4+OQ7!~yZ zgTVr+51}>24?BI6#^B8LBTJ3nZzTeu{_Em%kHq;WHS%NM*K2I%TT3s0$$5Afti2#I z??;JMuSbu;7&ME5x`16HDQTT;n zQ8%k7BwvH-0^L4T2k-h?biQ_1Gy-FN>}feT;yL{XReQy?os!pMla&G+c-P7d(r!t! zBMEBd)8gZ`y%|x1CsVs9_KBfxxEie@Cb=8TeAAwss?Ue+O&N1Ayz|nT`2eB4czPe* z;oHA`NDMN9vmEDY*?7_qe2Asqslpt&+jx6Fm>>5lLsW%uD7?99)XfrPfQ^|veXiul zZRiv!HDH|tiHZb6@%-MMEMgu%k1oqD8GrOgPe=*0mKR@ z18{)o|JaMG_c|NvUrp1n^`tuqT$`@QZ(pjpCupp8_o*X+A6~*sI)^?I8S#<20Mj!1 zD;0^f2PIqsE4tC=l~&NkLe$XZ(x_mVJxGX01$;Y~4sUWp6njl5a~RAdyrDuH<>>2K z^S+Y##UmQOO41aI`d3rAxo)_Ii%R6>JzIHR&FRcqQ9h^cAS;qj^dSBZVeAkz*x&fR zX?bHp7W517=xQ-u+u{~THBx8<#!xma*Pvt0tivelHN5%wz@|A8`iJ>o_thNm!RYxx zRu>|WkkS(Ul^={{AIklAbBfj=Jn%0ijL8|#+dOfRnXy0h0i9GS6ms7F2GNu(_(R>^ zpAaKP$=(vH8kU6K;rXE{Ss*$Fhkg`H|JX^Bl3V1*4hL-wH<_5NxCLeI#BL2A_dI-d zcYU+3nEOrV-J??Diw*U!CI-ttH;-&@_{J_(^GR$pNFFh^F>}S`Z8TqSVR8uB$k2c> zTt~-XOB{o^LqXW``6Xc3PD244n4Dfy!CNlCX@Pk|Yy$)n0YJGnf4P@g1Bq$CMBl*N zLD`vd+{X*Z9mSY|q`1q8rw${qa_PgyBKeZ)#H{drAko}a-Os&fm!0(TN+d9BaV5}n zmR=^hg;VHjp(J0bz%3Xz)9HN;wpdjF@!9u6f(@x9(gz9>ec)EUPm9j-FcWneU}nZN zK@Z<1WLCUG5|{CmBOIb2X3y_Z=8Yyrgl$(`>kRTp<9i0K|bEn*#g(9oV_y+zBcvaH$!-kiC? z^0$QP2Gl7q@Rxq%k_yYcd(1dLqqoz!so2u6%4!_4Qph4jb8XsW9wGfkI25(1bMN!^ zmU762rYh}-8%2U;JZZeHm+vn|T(hL2l$e>A)mU*QeLkIDUD=Yr_GlPE2ISZl7G@VC(Ajl~li=4a)& z+nj9j;nVax-fFk^9a!u?{dwun`imT4^`uq)cX{ounx4x|BAv41IyYFT9tgTpySRPa zbt}7^VHw`|c*6MWxIqqk^A0r^@3}|96wd_^7dJ$P8f1((VlbMy0)Qw5D<`Z~6SVRM zP%R<@AQ?c>XEa{MoT(I0Oe(*ro0s%$!s%qH)E(8ZCxsJP-%MlferW6~D7?QhQz~XZ z$gJse+@~)@k8P0If$vI0*o7PmVODWUq`RxAi6J3n#%Ck}bdTmDt-o^T5XYh};VkE` zz%Yp8Le&+!ihxmM1@(_ybcM@U%vmQXjh3zMMQw* zr=+ljc7o0rN0+-{{m~ES};Zx@MpO(cs31erZY%X@(*vK25^9~JFat6UenUS5$ixY#Ea3KD!LJ-2Q+{6Zzi$+Yjig1T#> z)0awt=cq3UPbz@K$jbNpt?6_SGQp1m3!)yX7iWqV#>vcB9`c(}P;Oj;w_I03&{&;> z?sw(V;SIut4tBl0k?(O!OI;&*TSgT~x@UT&-F`1qGsEKkYP-4hbZW)i;8HVo)lFW# zNKCie`7V-S`#!N{6E|)fP#)=6J;Ja!`}{6rnt5oUa~Y)^-e?&AJ!4taLs+|@R8o92 ze8YD-hRexC50g;kCOhxW&@j{R2!CSgc6kmLBSE|({9=o6B`h3+-GZj(x>N4EVp|U> z!+qDxiuYe5?DhV+b@U~;do-7iR0BCR@@}+ROc&aZM9oEpT@bGE2ix}mKNiWee%Yn% z$lN}rk4P! z_$oPB$Ft;#O;zR_NQ0T)W*q?OR_(`+7v4SQ{{R>QadF@)jDeqnNak#H18C~+c<4h9P#J=dJ;h=Lnu@uxqUFt$*F<-LOOw3hH%z5}$2#cb9RSDpCn_1AUg zX_d#DdDF8?CX(eky%aV^Kjpiu`v2(fQrjCYYP_QO*>~jlFffvSjFx0v>?H5ACkzAJV0`l z|KI%YH(os^BQl_RF+X>~i%0e%!C=HH0WVyvk%KJ~$mto`P7oDJ%UFCFa&nW@W%Ek& z6VHwrrd0ZxLpk`pl~v1O4NW0dVlN)hcsUG|3^!B~=|AAnL&fR7E;U3lm z{BQSnU?kK;>N8$%UaG9yU#&U#<&mssP#42Kl2hmZGnBCizO-k7gtq%ftY)`#r^x*8 z>z2%^8NXV^>?-MA-iwDt9bh&pLW@VZ=>=XR@a|9M(v2`W zD%l&|m)y6Tc4!WnbQV|k=sWdIy-BFZmDDU54XR;Wn!QjLp1b?Wt!i-M0EzeB;6PlJ zr0T#P%^u(F?|+mFIyddC3baJ5s&L6TD^YWtRY|GF*}|iAu~+>qpIv=+^Ugk{gAvAg zZ-k9#X-|c|exCI@?)3}U2%--&;>=kf=M`&u_tq%y%@-jWyp!i}--O4!gI~h9Q^mq1 z?^NQ0emOzWl~4IBP|)Pi%}vY3w2t|~i|2T5H(n5xatz$sEq}PW*5FtvqGTPdx5pOS zG_N}+%hH~ulkR46FFaoBO1jq^f3mP0L+Q{TmZaT*^`$_@%UTZfb#v<{)Uz+vp6vR5 zHrC-MMkM`x(^&Dx{Ue%E9urqqFGT7O7}&fIriTdhA+g|yR^E8c3%kL|-+O{Kwvc zryF11-^0cHA)k?6@1k)K)qS;<$xvHKoXg?grPix&F4wqg-zudhM0&)HtrLD$p63Zx z#w>-6z43K9Onzovsx5HtXYis8^^eQ8mO2ipiMwTfje~mlzI2*$yN-O1_|MczFMPF< z9^XF)J)pHe^Fm^s|Bi6~z$MY5a+&up0^4|}i_6cV7;CO4w z6Evt2W%uR+rwhBCg!b}MDjarZlY5^8^-iq1ENxR1^`ey?*U)?kjD;<;y!Dh8sdGOc zyLgVJaAG6vh77YArnC24s12o8c1HJ@dnQ${Q?Hj`^sb9);PAlWc0-EG`H;J}+YCR? zi0(4Ra5CRceKjNOR@>8_nSd*=6!CH560qs#GgkBVEacQunK&5JW#7%GX+4v#ZRIvp zH6hTy<@QNZANlyn5Lz({M9Hi(_}g*8ruTkysoivppyW1An752d3n%7w9ry7LIU9e5)4md);2;t0 zk=CA_$FiZNZf-}iC=%2&QzIp(TYjDpi(!YZF=*v5bbSY;W;^KwkD+_seC!3zhd(&F z@gK65BvgWm`?PkJ!@@S}>TX0RI~r>&-^?&;o#P7X9TYC()MC6B@3cFrYuTnHn~e-S z9(z7feMf=J;!4e8_4tT2es(c=_}1$O_azY?vX+8qRv+GuSe*+H(N1DbJha)+MK8Q= zk~Y71qDw(td`hD!nZtwN`(7xDw&9mfYJ^(bd+XlS@MIw0elDx$Pb(HeANp?N)>ZF0 z;w-$PvYjbQbLZDiv~5>R$^57i8oG$&^S=ZUVJJy-OXQUwmp{ESn|^n@f8;{e&&mTf zQ)`(#xqOZT&Sekm#$q@dsEnzNn300v78@H{m#ibE(#K=MWY zyLD7v{iW5hMP=NHADtOu-C8u!nV=Q{4LDUpZM>@jLKH{;{nspPla!-XcuDF;GCNA zxy+?J)uBP(1DMV0=m$^W(eQbzJ*z+dVC0u_AAywbw!Rn;1UP^l^SpyI zGoX%3|8rl?;uN<$^S$gfzqu+*q%3wUNr~O&22}+uZRhVIl^$k@*j;Ou0wSsXPe{tU%KSc4i}vj48&)H2oqnfgc2wy4hK6I2-s`DA z+Ae}%h4b648FuZkl_IMfE6|nvT0!*upa|D(bdj7$NNi91(-&7%bh#~OD!|ZAF1EnHQtcCFa;(vh2%%rOtW@q?Uo9RiWU9V z5=dvk=8?;<2&RDqM{Aiws*OFTRnrLjmmM?y1`NmZ;cTo_QsW1s-()RG$5s?>TrY zeCB2H>6o$`@_O{))3Yx!-w6ZvWc|;Meu@5(2?fL6*d3$Cqu!#g_pEwf+Qp9eoCv-( zo-0`B`oMzFle}K8#R@pV^KWhIKKxMFkHiEX6>ygQx!+@Qg*B@NPvu5uSk z?shWk%Mng>?HrvjkDTU_-q90D%6vI_(%N@VtbA)b8iF6H`Q&f~zfd3xf{O3=oG;>Z zA9>}t`g_)=E2E@mxK&7R7b6YUT};a_S4&SlThl$C?W#JVncPlbZ-Vdoq@&$Vbj6@` zRKwL3uk$aa#>m6CZEQ;CqbY{g34Z5&gL}$8GiRHFr;I6Nl0bj~IJ=c!0XGpJ#(Quq z{e8ycj)6lkCprT6)BQdKBsqiM0R|SlT09N-+Yge$BqI*T+`VMrljS?=4O0LzvCIz;zFX0-T%i%(0MN zXXsN+Lt@o`5MHDB@57z8cvLXG_I97OSG1RvX+SksrfMgDKHE9z>FKFXVb8C=F%!Nm z6uAQQdLgXG@B`~-YN}}Db2vLL%d8K}h^rO!(o8OR2x~B(j8|2@6IZAI`ZM^eiuF4m z>Yg^y)4bY$U(Zpq!5aU%w0u*~Ga)tobFatW4%2)*`YZ>Wl}nD7_%3_da<7n+)96u& z%-*8b;ROfA<4FJ5ZO5tim`+v|Z-5@M947ykbjGA2_B%HLA+18F{B= zX5kb5=CQY^ML#O66;M_`4Z8E#H|AAkearlBg9@jsDwPTGT?s|>Q|cB5tL+p@Zk~qE zc1IFEgwQo*e(YK$WC^);jZu|<+9=k#RVtZ(SVy=5xg>o}rQzCYcdo&`I9%)QN6wwE zX210M-z_+WXdqRIF>^3RKEM;c*$;mGU=F)KQOA7t zEo5UAem!-xceAPGv7>Y`Saxneqks{8O(ontwwQA)xL(jyX`Rh%XpCd4TcaT?P^b2J zO;Yxp_iVz1q^u?S`8Mr)xM!hrJA0}x{YtEkpMvTnW2)lR*F%y=u<%<3L)8DVA#@e_ zUprb`T`KZcp9cuflG+tAL-=jO-1Jt`JxS~nZ`b+p1E~lJNyL3!{go5E{{cszTlU1hy6N-nX|E<*KQ`K z>6)@*BOHaV#*7-a0w5$ctTXV~;!mhpuJ1Z)J~L}4z$=o|g>l{;a15z*%AfOR;{H`5 zpt$@EoIr{b5(5RNsFSU~!{M_-t~JL#lU49h14m|omVZLRH3oi{0KvO#UVp1ROuWbE zgJ`Lu z$~*IUS(Zy{gy826N0dAoS`K{JouFKOAtrsu>5AwAjTLFSK^{BVF2L_%<9~2ULPu_m zL3l&$d9U}7Dts>~yLeg&_qOc*rTEpAZQ6;4-%A|6sd_JF>@h~*3rcWrHEg@j@gGc8 zjj07YzZAmO*6(@8y3BAuARH&FE{HAvJ_*i)tl6#9dj7d7)q%+7U<&dwwLOP7V6ZRF z2X$`l8m#8-6AZb6wS5r6a&>Nwo7!$y+Z7FQ z45m`ey>o41C&Z|m*#{ChMgg6BYajU+J;-UeyHP~DDY1LLo?NTjuCfwL?D(^}Z>D^{ zYv2)A^eVQH zsdlc=H@j?>wdTG{bRy|?n=7>$Sp6JZ zJ0o-*P%Xzl1XhcEtP=~$4lYi|r2`Qo<$&zsCtU)V-wE7*&+GT|mhI+G6;Dxv@8At| zn4es4PZ_!5@yg-LpUZ*8VOm)BK?MbPfXX=nQ+Bk^2R+-TCY1GKF*|W}r$IU3umLGGnf{-q|h-bFUvy@KeafE!$Zd2>fw$A)` zd#iCfm~U=S)w`&gW5*~EDy`=? zqh~EzyFNLju!Xe*GSw8{h?FQ6u1dzUHFn2MbJA<9MA!-9eq>SA>>!@=yN`qZZpBFW zu)tvReR4Ch;}%-nrY;%Q%RJ4CTg%TVW;!VTESm-B|9VA3pOMtCb5Y!?eRJoBCW3=&lAc#4$|?5<@2$pOt-HxusR@e2cZpChwau-!!>|7b*>G={ztd`@_lgdFyl3Rh3AWgQ4ZomcDRWSX-(xmYWHmhD zt18Lq@?B#S_x({0f_aArtE6sHua(z{L7c9?3zoR}@&t`XMa>NGJHGBy$z&-_X@kS{*A?>Lg_l`HDb>+U34fvJOD(IJo zVAA&A>V@$b%gpQ26B#k9BgSt4Va<9^{90kmv1qqU%}(8#@B`Me%yiCqY6BkBWsrmU zHkV6W*PHO;MbxUXdV zEBRLK_E1n#Fo(@NSAxwdusLDE@0KnEM?V+72V@nv>h~k-xdsMdrP?D|e8)rS0)5Po-ecTjb;!x~eWzfHh|2W^; zjlQ?iBIkw6Z^|Xqo?}e?APpc54E3}{>C|K%V?psiM<*xfa^d~ukUf~1Qm@LAYabR$ zEv#R6Y1xySHf0rzQq^(`SpDqm7{I|wQnr!L<;=<135ZH-J^q_zww=@Ut=@j~gA997 zdSEkVZu3Vo%2;ch?k>ix1!DoYO5U!A$@0BLPX;9t+nl72$V?t7Tkx!#x*WUE zc=Gamf_CT%IK*HrGAo@FWCp4RANJj~{ki+=bmNYM!~DTmpwL9MFXGZ2em!l@v&rl0 zIwqaN?MOxDEFX6^4MOV9mg&c-7r~9K{>P2U8)`>+jv8csO4NnePCX|!I~M#={;hn{vfgg0fPLwiyBV#!eYMq?Pa#JKk>nhFZE0M4+~)&hTisK4 zikbvfUB5dHG}vZ$m#OG<*bBA~yb#dy4c$=d;s@7^3nAsj`?JO`4 zqcl1n4@i#zNBQ?rX1NGrxaQm!Yjk~RSV>jCM_3%FOu~JZgr@UnXIKDz82_oV| zk!~PUdAbQ@1tNO=oU;W~!Edq`E~hTX06NU?3LR;6cEC{!-wIVu#5<=}2?-AMHa5=^eY^yP3&J+VXS#z9$ia3)&+ zW$T!zLV6~GLA$Gh#{*!h#cr^ZPLamdVsn!pA)X;uV^Oy<7 zf>Jk}g%r;c6TDM-RUNq#TtE?iurQ9Y>$1)H&LHZZbD=pCUEik@!OnnbMR2!G*LSnd zL`&mH9ye^}=Q4Fa6((*Mg&4%IC-727HK#K6L}{d0$9jk_&raq66VEUg#NL|n`U&;@ zbVXg`c7JmqUHdHmjr;6?pZ>&QQeA{H1UtLN5czXn-`N(@D?1Q~vRU-cUHAxz6eK3w zdZKDrRw;M0bAk68`K_YL8DN>jHs=eQu6TrDvk|oeP7R685b~uRUuQO+-F{rIBF#dL z-~d0CmF+wU=Y6ujLfD?3SLyLZ>$GO)fv(Nf9u1&k{DkU&)IezUonmk00y_b=ed zU69XFSTp?tLC36IEOYDj{Q?-Xo5v$vARlKv13pq~O+Hex0JOQ!xF}$3IFJB-!=cP;tNY?rF=nkuL0bQlQZF#Ij&%o zbZXa}UV~3&3*QenuEOAI{>VL*;p3&H79BGy}Pv9lxSI$_)8}Bwrr>}a7ZId|2QP4x~zr$ zxsZiav9(8do;4!R^S#xh(;})(hAMwkDma}PG34~pnIX|NiGV&I;TY84X!0DtpV<=Im*RGUT0hl1_kokq4dGq|Z|XMd1pxqwob4?UVVBsJ8*rAlEP8W7T>- z%;V>$&hE-@oOKmNRl*i&eBJmzg(D{O@!UoFG7j!Vp2b6HjK5q_h9|>srvl<|Ye6l~ zJR#*nY_~k?XL?p@FHEHbrxcwuqN;uIQ9b4vZ>*pDF9Fd@p}J%>Of>o~I z08V^qM1NWUB#Rb!1L}OP+PDw0I9L-0N2=r~UP^NWS3xWm)$Hs{h$(%;G!vx5g+vnB z;k(q*-wVVR=qv+((O%6SkPH=!JgBSBxa zESnhWDnJj#_<4XtZTJf)r0YuZxlZ_@jwH`=mbSma3EBS2vVJo*O*xj%fn}g(y>_81 zASEeP^4g{$4Y`YtYgZGh`1+F;KyiyT|M{ouA0fd$ww}8SENI3u#?`jVB5HRq#?Y5$ zFBR-oQ#4)jIUObS=2b4+HrY!nb%e8|hBR7~(|Oj$9%TpyiArpOL=ZwU2bgOK#hXGj zmEHttfV{QX!Q0vS8VChg_qj^<(oFG0pVQd{^%4bkFc>t z!kYAtD$6vxBE6RPGi!FEk2gHx$9YG?KKf^wx4UKkKX5Z97t20vx*%S>FW|qT1UhQB zt>LvW^R*`XY`odY(#fAQXIdBF$E~Nn@cW_)_b65Nw!tb# zkyjWJ9e`8%nAEBm`_Px=E(Ipdrq2*F1Q=`&rI>8rOU z{s>(&{a%1e$&NAByCnD=(AmZLgz2XTUe>`d$2Fbq0eWT4>>s^i zHjmOPTl9G?wqC%PydGyk7=%ma;t5VwR_{_bT|n^#6`>O*h-O7aoGTa}4k8>iKq;z4 zYLpiX^n{J3h_8%Z`2 z;-S1@^L%E57Cc+i{DY`{^5{BkgAvF!H2Yp-%#5*mRY zDUMR_{-_F3`=28(-Hhy_J*#MSp{_s;BAMygwb^p`GK#l0v4EfCVRebg9LMv+Dkg_q z^E+cd-_z3>tegb`J9t-IxaB{5$RC6ck!*tUjf?Z-&bx2u<1)>Wpo%Jf3)t$sH7vOx zQjrnqf$-P;zOImmBrO1)lG9#Vt%~;)tpDqx;Xz+#>!rP3qK^x4DCz{R-BD<7>9vKl zGU#ez*M%!tv@k5be+;c0e-K2FwjTfo{DAN}&UmuN%yNdRK)~6td!?bXj@#WOL(;U1 z8_4#Zp(iSoZCKms+cj5na@os0acJe)r*uC%$=SQvRKz(%M@1~X4&YRl;e?CiML zrSEJ0d2w=crtqw}`GWJ-JWhW?D0}sDL@Q)b@g*51HC20VR#{29dnDG7HE+)Hx8It7 z`g^V|bm#4XrU@j|1%6<2=+RRknIKA<%2UhHiJ5iZDM2U){#OV%!vDv_zPp~f>#3BU zqwHzsSJdLF1YWQzummaUe4d#POMGNG(-v~x_heab>Vg|2J+6sQvzPtJP^0o z`NOu1p-U^|@qL9Zm;%Y-@kTL&hW*v$wp$`VrW;ao)l4RA5c1IGc?&Ls_LXjv`VoSF6+@3t=hNO)@cJG79?4V{N0L`w zH2?j_224de@Lrcg3S%wiCR>4hL&FZJ&VZnvu)YT+7hf9DpD+P=)dzSi!{K0Vlt`Se%$lJ1OJ@Q zp8LzuR|$YbPr`$gDz9+M>EB4wrz!da$BqbCgiGi*6M+h&KuUZtpQd`v;lqbtzBU10 zRq7!Lm1xh}mU zA|QUM*^E_K%AK4lN(z5cX^j>OJ?!`=x8?Pa%_@Y5+)%dq2-y)Nx3~JV1mgS~777cl z3e?wLgFOPtJJNsnFQ`Mrf@vyJA)?w<6hg=cd8++a5cGw)drn#HaZ;s_E>h&B<#baU zYK{u7WhjCbHwRO=z-U0qov%d^uZbhCX)mu3xHfh^8a-a)wb7$NHR|DcNSEGUr2Ghr{Sn?Yins{ zsPAZIW6kbtZNz5mYN&5%W@NAL!fftfV@={t=WJ~60B7lL(Q(kR&>34B+8CKxo5HSv zgQGpN#X@IjV`Xb^?BHN*M0d;4-pTmiWymg_>$^FfZsn05SSJjIgVQ4+$JSO4*~ak6 z8>@El+RSdzji%k?27VZ3*2O7 zkIGX<@j{xKOI2RzE!&*R`qUey|LprPHfT$JHaF-TW&)og@5i7+Vftti)SnP7kBOxctr&4jyPPCxm&IM8CVnIZA;BFb~yAbpU?dAI_pZGUkld0xH3mLac1N1gpRd8 zJ9?qXOmh2(-`w28vOb|=%l>eMLdiO1pSYfStfLKU_LgOklbgF~op&j*DQnx+9mn_9 zZZmV8b9H{xx3jB_iJkCREACY$E1*}I$c$TmcK&dm$#0OIjg5I2?T2@ejo4qIjqF@a z%5OsNW~`2wj$T6XxBCm5o+>e6emx;;YyCB?^J{bB=%rj0_MKI|LR#u*s|H)zTZN8= zTB0oRtU?QWrt1qiq9Z{W=z?KPHto=hc#W+-+(~?m$~}j1T$80@?58Jox4Rxxv@jD} ziDK6FOOBlx+a~U!f4S{LZlD|b;rhSCOuk|b?mP259YPNY5U+di4*8Gakao4gbZMAH zjH1fKVZ6pEQr=@mAU?Q$h;=c4?7DNsZ*y++Hk0eV?*6Q$w}%G%HifI}n%A1qR(R~! zbfes)ZPCOf7w492eRi&t3G0K{_p0-`s$X$HLP3+T_rmM>dO-cCu>Eg8FK7yk&^%VUqc^ zUGJr6pagMVc>Eqm*RV__X<;4FCX8J+o=#`SY48rsnwSyRZExWlOJweH5jBAO`6C7+ z6K=!LPImV8m0oP4gox=~tJJa^#=EU@H?h`&HUckAWYKo`x6psw6GXFw!6>c!rY$4K zFg)2`=tm@;#C5TgOEbZN5EGgL{AlUoGx~#uL_2)l9L3`jf zC2Y@UDG{wE-vn-Mb9k+&hr{b#DDUm-=rnT}F$CRs1|vo^T3^!5V!_=Vy_Ki&J(nVS zl&@^9_i0ZZERCHUddgsf#}hrnY4HgC&+)>=zawEdi>T&Yk#de~kK)_#>d)>M&8QRQ z(CAk5%W*EO$>OU!EhQn}3#C`ay3m_3yl9)89D;@IK9%JLu}=(Qm?guMn(8ys!WTz= zqi2n!ak(?TQsUy4)lbnL1&Axeftpv0iC`FU9DNm@9Fy#PJ?I`!z`aLAaW?pXwN836 zs^%L$)PkNetS_4Hc=b!=I3r+N}I4IP62OJ#E6T;;hp>jX#&IRKZQ2MN%+{(haSO%irq=V1vsW_&aSH8 z!l@-GMB4o}%kTHG803u&Gb|aE`!tWanmes0_zwqG*CUD^jH$TgtsxdA7^7)*5Ef$ zLb&$qCAa8@zhT=_Sc6)#`ub2tCh(*N6QPpvq4Wtx&4Z%7FYXSBHii~g^{1*$usszH z8&z!2S(~VdOU!-MdS{e9`j<#aXgt~><|p%HiM(uZE7q~YG&eQA;|_5g|2)paC3J&> z104qDyJ#)(^uLi`i$;%gz18LWi0S2PxMw++zcOyUxZP75%DDVdwdXa~AmY8E6nYbf z7#6Gaq5fOkTdZT;=6gj4%ojykCNsm$f0nO-Xxe$PSSeZ58F65emvg?wy?Tk>PV`F# z^9+uS0dlljxvIPC%i)*5MmE zMuXqu-%4KG%~k$M+@0`EM4L4k#$s(l@AS?t=S*o1mZQh?{S11N+{e;_RMA~--J0ii ziZ|z#!|rlntwr;(j>##y=oz94^C`+{vB>MAM-1sv%gw=~?z>ro9KmpNC}B;;uyh5t zC4|vSI07ErzRA38$)7LAMCcQV@X}NH(VG=5g6H}g;Qj7)fOykQVp9{}rDgcgP5xh@ zL`TcP8)|1#$;*1Z`e8AF2W_9z#_sml4s!_BSD>d&Zof>=Eh4replNR|c~zrhC5a`S*u;j(l9a>spRTDQ=)OTH z3vo~ej?$8$6}`$i=wU-{8PCK)tfPa82xuYwNU0>hU`G+$k4r*Yn=M7!k_}^w>O@w(=R%Y_Xj;?jQ-`TCK?`NadSD_nksJD8$K;oC|u&;*lzSsp?wfS zK#dRv1v?x99o6W4`iyz)`P;Q}>G>`mL`gB}vm{ia=S&u`SiShT@j+w{WAZj%8QtJ` zrO9QavPFwt!<%HBv>NLDOoi_O(OYoU)F@m-PjOaSg=nnnFoZ29=|p0&4^y~{(EB-! zob6&z*~1vTQO|ck4{|GD9Z?;^7*8rQ5^W&YBm76SB_0;BA)*HGe8d{qdS+$bEW!>w9A&y>L&Ju{^j^~o7fQg zm*13dm$hsrfyZYxGuF^E8+FPYJlXSXV^#GL+m=}C3G_P7P0lW6`Nk4Ehg^<#nGD55 z18>MNkumEk^Y~6L)+B9%;NYOu&qV+EGujXD_vF{z7IA7B7O8HZ9dBtc^4hRzR=GcB z$#$?oo8${!k8+4gdxe@xj6raGuCmrW^bE(+=4M7bmp=AA5C59S5wIn|5s!cgy6k5 z2?64;F98%}IUsOR2+5Ug^4LZMabQ4@O+yF_I4nZgRZwJ0SOnyOM1p`UD%-HDpgYVRef@D<6n^|PP~QNF3NvMWUof=!3=CAZt{kv?b>qD|0gI2 z3_3H`?hVuYZ0LwBS;S!8O2ogxUa$KXnue}=0FZ?Tkeclo_w{$KZxvc{07|8N&lCvW zzPEgIz8@IIF@QDZT}-8BUl_uG*C`uZOo3@`MNI_|bkT)nAdKPZVBFzFgncgYtyf=~ z7LHU6H{1-o(1Bw3RoQ=S zjh;DF{}aobxl~lj@`vXftw1Z$OP%juK7Z- zOX=+o`McTfJapkPc3Zz3xewY*Hc`gs!AcB)HA(qb1I-b$yAsFd}y z#j(hP16wgsXHcfdwIaO$+PM0}hq(Zm-CYjWRcj6m*&C zS3OGol9#aBDFzLgGT|E_iI&f`H%SsbGE*a!!ei{L?o%ToTb;Ns1%F!`{%Krk|1OS6 z3d*a_4%Q8TG|I2cvEu7el-3v(W$%+_C5zU%=AMR~7h(*jU~vW+V`fA~jjQ7q%6n9# zsIF*kf2Dn-?rdXW4S<*uQj<%~P9`)#kilSaoCq7$A~h{&bBB?Ip64GUTi3i5)l>;k&2d*rs$tiv`(^aj9lb}+k=EcZIOE~U9ZSCNQo6f)!D@mRUm=j$X|yj% za+<2{54x}AIf5S3D=DPrttp=69WPD!0|E;c-T;D-$3zVC82(N_dw8|PN*l2~Fj;aiAKoyv z-qq5w_G{HD0B|f;Boq^;Uch(AEhEVsX-y-AufLD)15M0dz8#?+p^Vt0{MYpr_EAm( zJ6)kq*5}t`R5Jq*sHKzK%PA|vsVE?;5wU0#DS6)U9pXaKevH*MT0EQ{_RTAcpxAffkoF3fXYNeYs8 z&ZTrBwJ1sAmYb8qWK8ZC3AU0?RF`?a6-?{TW(A;3!4gZ5>mr*2)3o~vNzKEof6|}9 zB7BZk^RvGve5KRDs#Y6XUE8+oiRx%KXG;##K^>%~XUOS2lxaV8#U0ofz90Ts<+im6H>j zHdoZYP@jNO2q!-2_qYOv=(76@1R2%VKXTu6^)4^3PkKGmxuHrS1ixQ1`nuXZ@j&CI zUivXf$$aWssUPGvoL3J{iJLlD;7XT0 zW{=y>8&Z(eJ!516RtJ`M)oR;Mg~dtNX*(c})8$NkG$&tzo;lK-%??F8dhghiiia6D z9X_-{0KhPS;N~YCMl8+4kWkKZBGQ`7p#Z_~B4U%4cBVJ#&gCMvWQUM4z?c*m)Fn9| z_Nrf!V5sCK`G&f<-Ot-Zmst~WELsOzer(out!v)`47ifWT?fe;(BOEdMJMB0S}8ad zMi&`>*A6rTOE9|r8I3?~*B_Sd{VP^|7KuBFCz2SIx9}GyPPF}!vnbDjwOF}BZh1iA z3X=~dY=A*P$B=(REr78MVS>0d13fc!l&%V2DY~1pf9WPoYv8V8B#iGh-(a83s9j7_ zkb)s_F^poy{{R6h&G0^#x3M0=c|2WkuG+Mn7L-qF&PuDwDwFxy&j#?g4MBfLyaHLqZx!XJ!ZiMPbQt`70=TS^cCNL1p^DnvM>Z-yMSrtldgUYHHpd T=wN?1`ToGs&c(LDIw1Lf_LO}~ literal 0 HcmV?d00001 From 74532e951814c6d49ca68baaff632a4154ec9536 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 3 Sep 2021 17:54:37 +0200 Subject: [PATCH 30/49] Ignore resources that we did not request Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 14 ++++++++++++-- source/common/config/delta_subscription_state.h | 4 +--- .../config/xds_mux/delta_subscription_state.cc | 14 ++++++++++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index abf610a56ccf..610e713ef0cf 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -215,8 +215,18 @@ void DeltaSubscriptionState::handleGoodResponse( { const auto scoped_update = ttl_.scopedTtlUpdate(); - for (const auto& resource : message.resources()) { - addResourceStateFromServer(resource); + if (requested_resource_state_.contains(Wildcard)) { + for (const auto& resource : message.resources()) { + addResourceStateFromServer(resource); + } + } else { + // We are not subscribed to wildcard, so we only take resources that we explicitly requested + // and ignore the others. + for (const auto& resource : message.resources()) { + if (requested_resource_state_.contains(resource.name())) { + addResourceStateFromServer(resource); + } + } } } diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 6631d21fd34b..b94ff23b898b 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -57,9 +57,7 @@ namespace Config { // when we receive messages from the server - if a resource in the message is in "added resources" // list (thus contains version information), the resource becomes "complete". If the resource in the // message is in "removed resources" list, it changes into the "waiting for server" state. If a -// server sends us a resource that we didn't request, it's going to end up in the "wildcard" -// category. Such resources are dropped when the server send us a message with the resource in -// "removed resources" list. But this normally should not happen. +// server sends us a resource that we didn't request, it's going to be ignored. // // In the "wildcard subscription" scenario, "requested" category is the same as in "no wildcard // subscription" scenario, with one exception - the unsubscribed "complete" resource is not removed diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 52c82e86427c..9add43a471c7 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -185,8 +185,18 @@ void DeltaSubscriptionState::handleGoodResponse( { const auto scoped_update = ttl_.scopedTtlUpdate(); - for (const auto& resource : message.resources()) { - addResourceStateFromServer(resource); + if (requested_resource_state_.contains(Wildcard)) { + for (const auto& resource : message.resources()) { + addResourceStateFromServer(resource); + } + } else { + // We are not subscribed to wildcard, so we only take resources that we explicitly requested + // and ignore the others. + for (const auto& resource : message.resources()) { + if (requested_resource_state_.contains(resource.name())) { + addResourceStateFromServer(resource); + } + } } } From 3d55048c3ad57f1e2cf91036bcdf5fa7ad3e2c3e Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 3 Sep 2021 18:00:01 +0200 Subject: [PATCH 31/49] Add a test for ignoring superfluous resources Signed-off-by: Krzesimir Nowak --- .../config/delta_subscription_state_test.cc | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 9f28a986d8c6..24ab7f8e2d69 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -436,6 +436,26 @@ TEST_P(DeltaSubscriptionStateTestBlank, AmbiguousResourceTTL) { ttl_timer_->invokeCallback(); } +// Checks that we ignore resources that we haven't asked for. +TEST_P(DeltaSubscriptionStateTestBlank, IgnoreSuperfluousResources) { + updateSubscriptionInterest({"foo", "bar"}, {}); + auto req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo", "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_TRUE(req->initial_resource_versions().empty()); + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"bar", "1"}, {"did-not-want", "1"}, {"spam", "1"}}, {}, "d1"); + + // Force a reconnection and resending of the "initial" message. If the initial_resource_versions + // in the message contains resources like did-not-want or spam, we haven't ignored that as we + // should. + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo", "bar")); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + EXPECT_THAT(req->initial_resource_versions(), + UnorderedElementsAre(Pair("foo", "1"), Pair("bar", "1"))); +} + class DeltaSubscriptionStateTestWithResources : public DeltaSubscriptionStateTestBase { protected: DeltaSubscriptionStateTestWithResources( From f8b758fd20fa93e4421771ed7c41a93a12c8f405 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 3 Sep 2021 18:20:18 +0200 Subject: [PATCH 32/49] Fix formatting Signed-off-by: Krzesimir Nowak --- test/common/config/delta_subscription_state_test.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 24ab7f8e2d69..bc17e02e65d8 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -443,7 +443,8 @@ TEST_P(DeltaSubscriptionStateTestBlank, IgnoreSuperfluousResources) { EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo", "bar")); EXPECT_TRUE(req->resource_names_unsubscribe().empty()); EXPECT_TRUE(req->initial_resource_versions().empty()); - deliverSimpleDiscoveryResponse({{"foo", "1"}, {"bar", "1"}, {"did-not-want", "1"}, {"spam", "1"}}, {}, "d1"); + deliverSimpleDiscoveryResponse({{"foo", "1"}, {"bar", "1"}, {"did-not-want", "1"}, {"spam", "1"}}, + {}, "d1"); // Force a reconnection and resending of the "initial" message. If the initial_resource_versions // in the message contains resources like did-not-want or spam, we haven't ignored that as we From fd76a691e5597da111ecd2c83aa263a9971409b6 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 9 Sep 2021 19:43:13 +0200 Subject: [PATCH 33/49] Drop one assert We likely don't want to crash when someone "unsubscribes" from the wildcard resource - just make it a noop. Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 2 -- source/common/config/xds_mux/delta_subscription_state.cc | 2 -- 2 files changed, 4 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 610e713ef0cf..da251896a3b9 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -86,8 +86,6 @@ void DeltaSubscriptionState::updateSubscriptionInterest( requested_resource_state_.erase(r); } ASSERT(!requested_resource_state_.contains(r)); - // This function shouldn't ever be called for resources that came from wildcard subscription. - ASSERT(!wildcard_resource_state_.contains(r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 9add43a471c7..9042e399aacf 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -68,8 +68,6 @@ void DeltaSubscriptionState::updateSubscriptionInterest( requested_resource_state_.erase(r); } ASSERT(!requested_resource_state_.contains(r)); - // This function shouldn't ever be called for resources that came from wildcard subscription. - ASSERT(!wildcard_resource_state_.contains(r)); // Ideally, when interest in a resource is added-then-removed in between requests, // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" // in the request. However, the removed-then-added case *does* need to go in the request, From 4d20de129aa8cdadd877ff7dd4125bb1ad2a121e Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Thu, 9 Sep 2021 20:12:59 +0200 Subject: [PATCH 34/49] Try to preserve the legacy wildcard status on ineffective unsubscriptions Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 10 +++++++--- .../common/config/xds_mux/delta_subscription_state.cc | 10 +++++++--- test/common/config/delta_subscription_state_test.cc | 8 ++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index da251896a3b9..003a08a93c0f 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -68,7 +68,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.insert(a); } for (const auto& r : cur_removed) { - in_initial_legacy_wildcard_ = false; + auto actually_erased = false; // The resource we are interested in could also come from a wildcard subscription. Instead of // removing it outright, mark the resource as not interesting to us any more. The server could // later send us an update. If we don't have a wildcard subscription, just drop it. @@ -81,9 +81,10 @@ void DeltaSubscriptionState::updateSubscriptionInterest( ambiguous_resource_state_.insert({it->first, it->second.version()}); } requested_resource_state_.erase(it); + actually_erased = true; } } else { - requested_resource_state_.erase(r); + actually_erased = (requested_resource_state_.erase(r) > 0); } ASSERT(!requested_resource_state_.contains(r)); // Ideally, when interest in a resource is added-then-removed in between requests, @@ -92,7 +93,10 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // and due to how we accomplish that, it's difficult to distinguish remove-add-remove from // add-remove (because "remove-add" has to be treated as equivalent to just "add"). names_added_.erase(r); - names_removed_.insert(r); + if (actually_erased) { + names_removed_.insert(r); + in_initial_legacy_wildcard_ = false; + } } // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from // cache. Also drop the ambiguous resources - we aren't interested in those, but we didn't know if diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index 9042e399aacf..dd1c065e916f 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -50,7 +50,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( names_added_.insert(a); } for (const auto& r : cur_removed) { - in_initial_legacy_wildcard_ = false; + auto actually_erased = false; // The resource we are interested in could also come from a wildcard subscription. Instead of // removing it outright, mark the resource as not interesting to us any more. The server could // later send us an update. If we don't have a wildcard subscription, just drop it. @@ -63,9 +63,10 @@ void DeltaSubscriptionState::updateSubscriptionInterest( ambiguous_resource_state_.insert({it->first, it->second.version()}); } requested_resource_state_.erase(it); + actually_erased = true; } } else { - requested_resource_state_.erase(r); + actually_erased = (requested_resource_state_.erase(r) > 0); } ASSERT(!requested_resource_state_.contains(r)); // Ideally, when interest in a resource is added-then-removed in between requests, @@ -74,7 +75,10 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // and due to how we accomplish that, it's difficult to distinguish remove-add-remove from // add-remove (because "remove-add" has to be treated as equivalent to just "add"). names_added_.erase(r); - names_removed_.insert(r); + if (actually_erased) { + names_removed_.insert(r); + in_initial_legacy_wildcard_ = false; + } } // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from // cache. diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index bc17e02e65d8..8f4f1d3b3b50 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -362,6 +362,14 @@ TEST_P(DeltaSubscriptionStateTestBlank, LegacyWildcardInitialRequests) { EXPECT_TRUE(req->resource_names_unsubscribe().empty()); deliverSimpleDiscoveryResponse({{"wild1", "1"}}, {}, "d1"); + // unsubscribing from unknown resource should keep the legacy + // wildcard mode + updateSubscriptionInterest({}, {"unknown"}); + markStreamFresh(); + req = getNextRequestAckless(); + EXPECT_TRUE(req->resource_names_subscribe().empty()); + EXPECT_TRUE(req->resource_names_unsubscribe().empty()); + updateSubscriptionInterest({"foo"}, {}); req = getNextRequestAckless(); EXPECT_THAT(req->resource_names_subscribe(), UnorderedElementsAre("foo")); From ab651250f3621bb4ba1a3a8ace9ffe8bcfd517f9 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 17 Sep 2021 18:47:50 +0200 Subject: [PATCH 35/49] Expand a bit more on the ambiguous resource category in comments Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 12 +++++------- source/common/config/delta_subscription_state.h | 7 +++++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 003a08a93c0f..7b3c3b304bca 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -69,14 +69,15 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } for (const auto& r : cur_removed) { auto actually_erased = false; - // The resource we are interested in could also come from a wildcard subscription. Instead of - // removing it outright, mark the resource as not interesting to us any more. The server could - // later send us an update. If we don't have a wildcard subscription, just drop it. + // The resource we have lost the interest in could also come from our wildcard subscription. We + // just don't know it at this point. Instead of removing it outright, mark the resource as not + // interesting to us any more and the server will send us an update. If we don't have a wildcard + // subscription then there is no ambiguity and just drop the resource. if (requested_resource_state_.contains(Wildcard)) { if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { // Wildcard resources always have a version. If our requested resource has no version, it // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version - // attached to it. + // attached to it, so it will not be moved to ambiguous category. if (!it->second.isWaitingForServer()) { ambiguous_resource_state_.insert({it->first, it->second.version()}); } @@ -134,9 +135,6 @@ bool DeltaSubscriptionState::subscriptionUpdatePending() const { // because even if it's empty, it won't be interpreted as legacy wildcard subscription, which can // only for the first request in the stream. So sending an empty request at this point should be // harmless. - // - // If sending empty requests at this point is actually harmful, we would need to add "&& - // !requested_resource_state_.empty()" to the return below. return must_send_discovery_request_; } diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index b94ff23b898b..88de2cc5ec64 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -45,8 +45,11 @@ namespace Config { // indirectly interested through the subscription to the wildcard resource. // // The "ambiguous" category is for resources that we stopped being interested in, but we may still -// be interested indirectly through the wildcard subscription - resources in these category are -// "waiting" for the config server to confirm their status. +// be interested indirectly through the wildcard subscription. This situation happens because of the +// xDS protocol limitation - the server isn't able to tell us that the resource we subscribed to is +// also a part of our wildcard subscription. So when we unsubscribe from the resource, we need to +// receive a confirmation from the server whether to keep the resource (which means that it was a +// part of our wildcard subscription) or to drop it. // // Please refer to drawings (non-wildcard-resource-state-machine.png and // (wildcard-resource-state-machine.png) for visual depictions of the resource state machine. From c726d04f423f507a677cf19dec37fce6edb60ead Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 20 Sep 2021 07:18:49 +0200 Subject: [PATCH 36/49] Update the comments in the xds_mux variant too Signed-off-by: Krzesimir Nowak --- .../config/xds_mux/delta_subscription_state.cc | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index dd1c065e916f..fb9a2a7a0388 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -51,14 +51,15 @@ void DeltaSubscriptionState::updateSubscriptionInterest( } for (const auto& r : cur_removed) { auto actually_erased = false; - // The resource we are interested in could also come from a wildcard subscription. Instead of - // removing it outright, mark the resource as not interesting to us any more. The server could - // later send us an update. If we don't have a wildcard subscription, just drop it. + // The resource we have lost the interest in could also come from our wildcard subscription. We + // just don't know it at this point. Instead of removing it outright, mark the resource as not + // interesting to us any more and the server will send us an update. If we don't have a wildcard + // subscription then there is no ambiguity and just drop the resource. if (requested_resource_state_.contains(Wildcard)) { if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { // Wildcard resources always have a version. If our requested resource has no version, it // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version - // attached to it. + // attached to it, so it will not be moved to ambiguous category. if (!it->second.isWaitingForServer()) { ambiguous_resource_state_.insert({it->first, it->second.version()}); } @@ -115,9 +116,6 @@ bool DeltaSubscriptionState::subscriptionUpdatePending() const { // because even if it's empty, it won't be interpreted as legacy wildcard subscription, which can // only for the first request in the stream. So sending an empty request at this point should be // harmless. - // - // If sending empty requests at this point is actually harmful, we would need to add "&& - // !requested_resource_state_.empty()" to the return below. return dynamicContextChanged(); } From f9d846d6a40e3e9ece05b37ab8d503ddd1de5d20 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 28 Sep 2021 19:59:53 +0200 Subject: [PATCH 37/49] Constify some variables Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 6 +++--- source/common/config/xds_mux/delta_subscription_state.cc | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 7b3c3b304bca..3c57cb433e98 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -26,8 +26,8 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); removed_resources.Add(std::string(resource)); - } else if (auto erased_count = wildcard_resource_state_.erase(resource) + - ambiguous_resource_state_.erase(resource); + } else if (const auto erased_count = wildcard_resource_state_.erase(resource) + + ambiguous_resource_state_.erase(resource); erased_count > 0) { removed_resources.Add(std::string(resource)); } @@ -247,7 +247,7 @@ void DeltaSubscriptionState::handleGoodResponse( if (auto maybe_resource = getRequestedResourceState(resource_name); maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); - } else if (auto erased_count = ambiguous_resource_state_.erase(resource_name); + } else if (const auto erased_count = ambiguous_resource_state_.erase(resource_name); erased_count == 0) { wildcard_resource_state_.erase(resource_name); } diff --git a/source/common/config/xds_mux/delta_subscription_state.cc b/source/common/config/xds_mux/delta_subscription_state.cc index fb9a2a7a0388..fc2e52228f31 100644 --- a/source/common/config/xds_mux/delta_subscription_state.cc +++ b/source/common/config/xds_mux/delta_subscription_state.cc @@ -217,7 +217,7 @@ void DeltaSubscriptionState::handleGoodResponse( if (auto maybe_resource = getRequestedResourceState(resource_name); maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); - } else if (auto erased_count = ambiguous_resource_state_.erase(resource_name); + } else if (const auto erased_count = ambiguous_resource_state_.erase(resource_name); erased_count == 0) { wildcard_resource_state_.erase(resource_name); } @@ -343,8 +343,8 @@ void DeltaSubscriptionState::ttlExpiryCallback(const std::vector& e if (auto maybe_resource = getRequestedResourceState(resource); maybe_resource.has_value()) { maybe_resource->setAsWaitingForServer(); removed_resources.Add(std::string(resource)); - } else if (auto erased_count = wildcard_resource_state_.erase(resource) + - ambiguous_resource_state_.erase(resource); + } else if (const auto erased_count = wildcard_resource_state_.erase(resource) + + ambiguous_resource_state_.erase(resource); erased_count > 0) { removed_resources.Add(std::string(resource)); } From bae864310364395d4efac8b9bcabea341eee6196 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 28 Sep 2021 20:00:16 +0200 Subject: [PATCH 38/49] Drop unused member Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.h | 1 - 1 file changed, 1 deletion(-) diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 88de2cc5ec64..1f663eddc39c 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -165,7 +165,6 @@ class DeltaSubscriptionState : public Logger::Loggable { UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; Event::Dispatcher& dispatcher_; - std::chrono::milliseconds init_fetch_timeout_; bool in_initial_legacy_wildcard_{true}; bool any_request_sent_yet_in_current_stream_{}; From d10ec21fc3583bd2036044d689f3b9005817fdcf Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 1 Oct 2021 20:21:17 +0200 Subject: [PATCH 39/49] Gate the explicit wildcard resource support Signed-off-by: Krzesimir Nowak --- source/common/config/BUILD | 12 +- .../common/config/delta_subscription_state.cc | 431 +++--------------- .../common/config/delta_subscription_state.h | 156 +------ .../config/new_delta_subscription_state.cc | 409 +++++++++++++++++ .../config/new_delta_subscription_state.h | 181 ++++++++ .../config/old_delta_subscription_state.cc | 248 ++++++++++ .../config/old_delta_subscription_state.h | 124 +++++ source/common/runtime/runtime_features.cc | 1 + 8 files changed, 1045 insertions(+), 517 deletions(-) create mode 100644 source/common/config/new_delta_subscription_state.cc create mode 100644 source/common/config/new_delta_subscription_state.h create mode 100644 source/common/config/old_delta_subscription_state.cc create mode 100644 source/common/config/old_delta_subscription_state.h diff --git a/source/common/config/BUILD b/source/common/config/BUILD index f9e0dab659df..a1e6bd8c2d62 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -86,8 +86,16 @@ envoy_cc_library( envoy_cc_library( name = "delta_subscription_state_lib", - srcs = ["delta_subscription_state.cc"], - hdrs = ["delta_subscription_state.h"], + srcs = [ + "delta_subscription_state.cc" + "new_delta_subscription_state.cc" + "old_delta_subscription_state.cc" + ], + hdrs = [ + "delta_subscription_state.h" + "new_delta_subscription_state.h" + "old_delta_subscription_state.h" + ], deps = [ ":api_version_lib", ":pausable_ack_queue_lib", diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 3c57cb433e98..80cc1b11fb79 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -1,408 +1,103 @@ #include "source/common/config/delta_subscription_state.h" -#include "envoy/event/dispatcher.h" -#include "envoy/service/discovery/v3/discovery.pb.h" - -#include "source/common/common/assert.h" -#include "source/common/common/hash.h" -#include "source/common/config/utility.h" #include "source/common/runtime/runtime_features.h" namespace Envoy { namespace Config { +namespace { -DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, - UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, - Event::Dispatcher& dispatcher) - // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on - // empty resources as updates. - : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), - ttl_( - [this](const auto& expired) { - Protobuf::RepeatedPtrField removed_resources; - for (const auto& resource : expired) { - if (auto maybe_resource = getRequestedResourceState(resource); - maybe_resource.has_value()) { - maybe_resource->setAsWaitingForServer(); - removed_resources.Add(std::string(resource)); - } else if (const auto erased_count = wildcard_resource_state_.erase(resource) + - ambiguous_resource_state_.erase(resource); - erased_count > 0) { - removed_resources.Add(std::string(resource)); - } - } - - watch_map_.onConfigUpdate({}, removed_resources, ""); - }, - dispatcher, dispatcher.timeSource()), - type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), - dispatcher_(dispatcher) {} - -void DeltaSubscriptionState::updateSubscriptionInterest( - const absl::flat_hash_set& cur_added, - const absl::flat_hash_set& cur_removed) { - for (const auto& a : cur_added) { - if (in_initial_legacy_wildcard_ && a != Wildcard) { - in_initial_legacy_wildcard_ = false; - } - // If the requested resource existed as a wildcard resource, - // transition it to requested. Otherwise mark it as a resource - // waiting for the server to receive the version. - if (auto it = wildcard_resource_state_.find(a); it != wildcard_resource_state_.end()) { - requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); - wildcard_resource_state_.erase(it); - } else if (it = ambiguous_resource_state_.find(a); it != ambiguous_resource_state_.end()) { - requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); - ambiguous_resource_state_.erase(it); - } else { - requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); - } - ASSERT(requested_resource_state_.contains(a)); - ASSERT(!wildcard_resource_state_.contains(a)); - ASSERT(!ambiguous_resource_state_.contains(a)); - // If interest in a resource is removed-then-added (all before a discovery request - // can be sent), we must treat it as a "new" addition: our user may have forgotten its - // copy of the resource after instructing us to remove it, and need to be reminded of it. - names_removed_.erase(a); - names_added_.insert(a); - } - for (const auto& r : cur_removed) { - auto actually_erased = false; - // The resource we have lost the interest in could also come from our wildcard subscription. We - // just don't know it at this point. Instead of removing it outright, mark the resource as not - // interesting to us any more and the server will send us an update. If we don't have a wildcard - // subscription then there is no ambiguity and just drop the resource. - if (requested_resource_state_.contains(Wildcard)) { - if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { - // Wildcard resources always have a version. If our requested resource has no version, it - // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version - // attached to it, so it will not be moved to ambiguous category. - if (!it->second.isWaitingForServer()) { - ambiguous_resource_state_.insert({it->first, it->second.version()}); - } - requested_resource_state_.erase(it); - actually_erased = true; - } - } else { - actually_erased = (requested_resource_state_.erase(r) > 0); - } - ASSERT(!requested_resource_state_.contains(r)); - // Ideally, when interest in a resource is added-then-removed in between requests, - // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" - // in the request. However, the removed-then-added case *does* need to go in the request, - // and due to how we accomplish that, it's difficult to distinguish remove-add-remove from - // add-remove (because "remove-add" has to be treated as equivalent to just "add"). - names_added_.erase(r); - if (actually_erased) { - names_removed_.insert(r); - in_initial_legacy_wildcard_ = false; - } - } - // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from - // cache. Also drop the ambiguous resources - we aren't interested in those, but we didn't know if - // those came from wildcard subscription or not, but now it's not important any more. - if (cur_removed.contains(Wildcard)) { - wildcard_resource_state_.clear(); - ambiguous_resource_state_.clear(); +DeltaSubscriptionStateVariant get_state(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher) { + if (Runtime::runtimeFeatureEnabled()) { + return OldDeltaSubscriptionState(std::move(type_url), watch_map, local_info, dispatcher); + } else { + return NewDeltaSubscriptionState(std::move(type_url), watch_map, local_info, dispatcher); } } -// Not having sent any requests yet counts as an "update pending" since you're supposed to resend -// the entirety of your interest at the start of a stream, even if nothing has changed. -bool DeltaSubscriptionState::subscriptionUpdatePending() const { - if (!names_added_.empty() || !names_removed_.empty()) { - return true; - } - // At this point, we have no new resources to subscribe to or any - // resources to unsubscribe from. - if (!any_request_sent_yet_in_current_stream_) { - // If we haven't sent anything on the current stream, but we are actually interested in some - // resource then we obviously need to let the server know about those. - if (!requested_resource_state_.empty()) { - return true; - } - // So there are no new names and we are interested in nothing. This may either mean that we want - // the legacy wildcard subscription to kick in or we actually unsubscribed from everything. If - // the latter is true, then we should not be sending any requests. In such case the initial - // wildcard mode will be false. Otherwise it means that the legacy wildcard request should be - // sent. - return in_initial_legacy_wildcard_; - } +} // namespace - // At this point, we have no changes in subscription resources and this isn't a first request in - // the stream, so even if there are no resources we are interested in, we can send the request, - // because even if it's empty, it won't be interpreted as legacy wildcard subscription, which can - // only for the first request in the stream. So sending an empty request at this point should be - // harmless. - return must_send_discovery_request_; -} +DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher) + : state_(get_state(std::move(type_url), watch_map, local_info, dispatcher)) {} -UpdateAck DeltaSubscriptionState::handleResponse( - const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { - // We *always* copy the response's nonce into the next request, even if we're going to make that - // request a NACK by setting error_detail. - UpdateAck ack(message.nonce(), type_url_); - TRY_ASSERT_MAIN_THREAD { handleGoodResponse(message); } - END_TRY - catch (const EnvoyException& e) { - handleBadResponse(e, ack); +void DeltaSubscriptionState::updateSubscriptionInterest(const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed) +{ + if (auto* state = absl::get_if(state_); state != nullptr) { + state->updateSubscriptionInterest(cur_added, cur_removed); + return; } - return ack; + auto& state = absl::get(state_); + state.updateSubscriptionInterest(cur_added, cur_removed); } -bool DeltaSubscriptionState::isHeartbeatResponse( - const envoy::service::discovery::v3::Resource& resource) const { - if (!supports_heartbeats_ && - !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { - return false; +void DeltaSubscriptionState::addAliasesToResolve(const absl::flat_hash_set& aliases) { + if (auto* state = absl::get_if(state_); state != nullptr) { + state->addAliasesToResolve(aliases); + return; } - if (resource.has_resource()) { - return false; - } - - if (const auto maybe_resource = getRequestedResourceState(resource.name()); - maybe_resource.has_value()) { - return !maybe_resource->isWaitingForServer() && resource.version() == maybe_resource->version(); - } - - if (const auto itr = wildcard_resource_state_.find(resource.name()); - itr != wildcard_resource_state_.end()) { - return resource.version() == itr->second; - } - - if (const auto itr = ambiguous_resource_state_.find(resource.name()); - itr != wildcard_resource_state_.end()) { - // In theory we should move the ambiguous resource to wildcard, because probably we shouldn't be - // getting heartbeat responses about resources that we are not interested in, but the server - // could have sent this heartbeat before it learned about our lack of interest in the resource. - return resource.version() == itr->second; - } - - return false; + auto& state = absl::get(state_); + state.addAliasesToResolve(aliases); } -void DeltaSubscriptionState::handleGoodResponse( - const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { - absl::flat_hash_set names_added_removed; - Protobuf::RepeatedPtrField non_heartbeat_resources; - for (const auto& resource : message.resources()) { - if (!names_added_removed.insert(resource.name()).second) { - throw EnvoyException( - fmt::format("duplicate name {} found among added/updated resources", resource.name())); - } - if (isHeartbeatResponse(resource)) { - continue; - } - non_heartbeat_resources.Add()->CopyFrom(resource); - // DeltaDiscoveryResponses for unresolved aliases don't contain an actual resource - if (!resource.has_resource() && resource.aliases_size() > 0) { - continue; - } - if (message.type_url() != resource.resource().type_url()) { - throw EnvoyException(fmt::format("type URL {} embedded in an individual Any does not match " - "the message-wide type URL {} in DeltaDiscoveryResponse {}", - resource.resource().type_url(), message.type_url(), - message.DebugString())); - } +void DeltaSubscriptionState::setMustSendDiscoveryRequest() { + if (auto* state = absl::get_if(state_); state != nullptr) { + state->setMustSendDiscoveryRequest(); + return; } - for (const auto& name : message.removed_resources()) { - if (!names_added_removed.insert(name).second) { - throw EnvoyException( - fmt::format("duplicate name {} found in the union of added+removed resources", name)); - } - } - - { - const auto scoped_update = ttl_.scopedTtlUpdate(); - if (requested_resource_state_.contains(Wildcard)) { - for (const auto& resource : message.resources()) { - addResourceStateFromServer(resource); - } - } else { - // We are not subscribed to wildcard, so we only take resources that we explicitly requested - // and ignore the others. - for (const auto& resource : message.resources()) { - if (requested_resource_state_.contains(resource.name())) { - addResourceStateFromServer(resource); - } - } - } - } - - watch_map_.onConfigUpdate(non_heartbeat_resources, message.removed_resources(), - message.system_version_info()); - - // If a resource is gone, there is no longer a meaningful version for it that makes sense to - // provide to the server upon stream reconnect: either it will continue to not exist, in which - // case saying nothing is fine, or the server will bring back something new, which we should - // receive regardless (which is the logic that not specifying a version will get you). - // - // So, leave the version map entry present but blank if we are still interested in the resource. - // It will be left out of initial_resource_versions messages, but will remind us to explicitly - // tell the server "I'm cancelling my subscription" when we lose interest. In case of resources - // received as a part of the wildcard subscription or resources we already lost interest in, we - // just drop them. - for (const auto& resource_name : message.removed_resources()) { - if (auto maybe_resource = getRequestedResourceState(resource_name); - maybe_resource.has_value()) { - maybe_resource->setAsWaitingForServer(); - } else if (const auto erased_count = ambiguous_resource_state_.erase(resource_name); - erased_count == 0) { - wildcard_resource_state_.erase(resource_name); - } - } - ENVOY_LOG(debug, "Delta config for {} accepted with {} resources added, {} removed", type_url_, - message.resources().size(), message.removed_resources().size()); -} - -void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { - // Note that error_detail being set is what indicates that a DeltaDiscoveryRequest is a NACK. - ack.error_detail_.set_code(Grpc::Status::WellKnownGrpcStatus::Internal); - ack.error_detail_.set_message(Config::Utility::truncateGrpcStatusMessage(e.what())); - ENVOY_LOG(warn, "delta config for {} rejected: {}", type_url_, e.what()); - watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, &e); + auto& state = absl::get(state_); + state.setMustSendDiscoveryRequest(); } -void DeltaSubscriptionState::handleEstablishmentFailure() { - watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure, - nullptr); -} - -envoy::service::discovery::v3::DeltaDiscoveryRequest -DeltaSubscriptionState::getNextRequestAckless() { - envoy::service::discovery::v3::DeltaDiscoveryRequest request; - must_send_discovery_request_ = false; - if (!any_request_sent_yet_in_current_stream_) { - any_request_sent_yet_in_current_stream_ = true; - const bool is_legacy_wildcard = isInitialRequestForLegacyWildcard(); - // initial_resource_versions "must be populated for first request in a stream". - // Also, since this might be a new server, we must explicitly state *all* of our subscription - // interest. - for (auto const& [resource_name, resource_state] : requested_resource_state_) { - // Populate initial_resource_versions with the resource versions we currently have. - // Resources we are interested in, but are still waiting to get any version of from the - // server, do not belong in initial_resource_versions. (But do belong in new subscriptions!) - if (!resource_state.isWaitingForServer()) { - (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); - } - // We are going over a list of resources that we are interested in, so add them to - // resource_names_subscribe. - names_added_.insert(resource_name); - } - for (auto const& [resource_name, resource_version] : wildcard_resource_state_) { - (*request.mutable_initial_resource_versions())[resource_name] = resource_version; - } - for (auto const& [resource_name, resource_version] : ambiguous_resource_state_) { - (*request.mutable_initial_resource_versions())[resource_name] = resource_version; - } - // If this is a legacy wildcard request, then make sure that the resource_names_subscribe is - // empty. - if (is_legacy_wildcard) { - names_added_.clear(); - } - names_removed_.clear(); +bool DeltaSubscriptionState::subscriptionUpdatePending() const { + if (auto* state = absl::get_if(state_); state != nullptr) { + return state->subscriptionUpdatePending(); } - std::copy(names_added_.begin(), names_added_.end(), - Protobuf::RepeatedFieldBackInserter(request.mutable_resource_names_subscribe())); - std::copy(names_removed_.begin(), names_removed_.end(), - Protobuf::RepeatedFieldBackInserter(request.mutable_resource_names_unsubscribe())); - names_added_.clear(); - names_removed_.clear(); - - request.set_type_url(type_url_); - request.mutable_node()->MergeFrom(local_info_.node()); - return request; + auto& state = absl::get(state_); + return state.subscriptionUpdatePending(); } -bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { - if (in_initial_legacy_wildcard_) { - requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); - ASSERT(requested_resource_state_.contains(Wildcard)); - ASSERT(!wildcard_resource_state_.contains(Wildcard)); - ASSERT(!ambiguous_resource_state_.contains(Wildcard)); - return true; +void DeltaSubscriptionState::markStreamFresh() { + if (auto* state = absl::get_if(state_); state != nullptr) { + state->markStreamFresh(); + return; } - - // If we are here, this means that we lost our initial wildcard mode, because we subscribed to - // something in the past. We could still be in the situation now that all we are subscribed to now - // is wildcard resource, so in such case try to send a legacy wildcard subscription request - // anyway. For this to happen, two conditions need to apply: - // - // 1. No change in interest. - // 2. The only requested resource is Wildcard resource. - // - // The invariant of the code here is that this code is executed only when - // subscriptionUpdatePending actually returns true, which in our case can only happen if the - // requested resources state_ isn't empty. - ASSERT(!requested_resource_state_.empty()); - - // If our subscription interest didn't change then the first condition for using legacy wildcard - // subscription is met. - if (!names_added_.empty() || !names_removed_.empty()) { - return false; - } - // If we requested only a wildcard resource then the second condition for using legacy wildcard - // condition is met. - return requested_resource_state_.size() == 1 && - requested_resource_state_.begin()->first == Wildcard; + auto& state = absl::get(state_); + state.markStreamFresh(); } -envoy::service::discovery::v3::DeltaDiscoveryRequest -DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { - envoy::service::discovery::v3::DeltaDiscoveryRequest request = getNextRequestAckless(); - request.set_response_nonce(ack.nonce_); - if (ack.error_detail_.code() != Grpc::Status::WellKnownGrpcStatus::Ok) { - // Don't needlessly make the field present-but-empty if status is ok. - request.mutable_error_detail()->CopyFrom(ack.error_detail_); +UpdateAck DeltaSubscriptionState::handleResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { + if (auto* state = absl::get_if(state_); state != nullptr) { + return state->handleResponse(message); } - return request; + auto& state = absl::get(state_); + return state.handleResponse(message); } -void DeltaSubscriptionState::addResourceStateFromServer( - const envoy::service::discovery::v3::Resource& resource) { - if (resource.has_ttl()) { - ttl_.add(std::chrono::milliseconds(DurationUtil::durationToMilliseconds(resource.ttl())), - resource.name()); - } else { - ttl_.clear(resource.name()); - } - - if (auto maybe_resource = getRequestedResourceState(resource.name()); - maybe_resource.has_value()) { - // It is a resource that we requested. - maybe_resource->setVersion(resource.version()); - ASSERT(requested_resource_state_.contains(resource.name())); - ASSERT(!wildcard_resource_state_.contains(resource.name())); - ASSERT(!ambiguous_resource_state_.contains(resource.name())); - } else { - // It is a resource that is a part of our wildcard request. - wildcard_resource_state_.insert({resource.name(), resource.version()}); - // The resource could be ambiguous before, but now the ambiguity - // is resolved. - ambiguous_resource_state_.erase(resource.name()); - ASSERT(!requested_resource_state_.contains(resource.name())); - ASSERT(wildcard_resource_state_.contains(resource.name())); - ASSERT(!ambiguous_resource_state_.contains(resource.name())); +void DeltaSubscriptionState::handleEstablishmentFailure() { + if (auto* state = absl::get_if(state_); state != nullptr) { + state->handleEstablishmentFailure(); + return; } + auto& state = absl::get(state_); + state.handleEstablishmentFailure(); } -OptRef -DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) { - auto itr = requested_resource_state_.find(resource_name); - if (itr == requested_resource_state_.end()) { - return {}; +envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestAckless() { + if (auto* state = absl::get_if(state_); state != nullptr) { + return state->getNextRequestAckless(); } - return {itr->second}; + auto& state = absl::get(state_); + return state.getNextRequestAckless(); } -OptRef -DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) const { - auto itr = requested_resource_state_.find(resource_name); - if (itr == requested_resource_state_.end()) { - return {}; +envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { + if (auto* state = absl::get_if(state_); state != nullptr) { + return state->getNextRequestWithAck(); } - return {itr->second}; + auto& state = absl::get(state_); + return state.getNextRequestWithAck(); } } // namespace Config diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 1f663eddc39c..6f13d5144add 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -1,180 +1,42 @@ #pragma once #include "envoy/config/subscription.h" -#include "envoy/event/dispatcher.h" -#include "envoy/grpc/status.h" #include "envoy/local_info/local_info.h" #include "envoy/service/discovery/v3/discovery.pb.h" -#include "source/common/common/assert.h" #include "source/common/common/logger.h" -#include "source/common/config/api_version.h" -#include "source/common/config/pausable_ack_queue.h" -#include "source/common/config/ttl.h" -#include "source/common/config/watch_map.h" +#include "source/common/config/old_delta_subscription_state.h" +#include "source/common/config/new_delta_subscription_state.h" -#include "absl/container/node_hash_map.h" +#include "absl/container/flat_hash_set.h" +#include "absl/types/variant.h" namespace Envoy { namespace Config { -// Tracks the xDS protocol state of an individual ongoing delta xDS session, i.e. a single type_url. -// There can be multiple DeltaSubscriptionStates active. They will always all be -// blissfully unaware of each other's existence, even when their messages are -// being multiplexed together by ADS. -// -// There are two scenarios which affect how DeltaSubscriptionState manages the resources. First -// scenario is when we are subscribed to a wildcard resource, and other scenario is when we are not. -// -// Delta subscription state also divides the resources it cached into three categories: requested, -// wildcard and ambiguous. -// -// The "requested" category is for resources that we have explicitly asked for (either through the -// initial set of resources or through the on-demand mechanism). Resources in this category are in -// one of two states: "complete" and "waiting for server". -// -// "Complete" resources are resources about which the server sent us the information we need (for -// now - just resource version). -// -// The "waiting for server" state is either for resources that we have just requested, but we still -// didn't receive any version information from the server, or for the "complete" resources that, -// according to the server, are gone, but we are still interested in them - in such case we strip -// the information from the resource. -// -// The "wildcard" category is for resources that we are not explicitly interested in, but we are -// indirectly interested through the subscription to the wildcard resource. -// -// The "ambiguous" category is for resources that we stopped being interested in, but we may still -// be interested indirectly through the wildcard subscription. This situation happens because of the -// xDS protocol limitation - the server isn't able to tell us that the resource we subscribed to is -// also a part of our wildcard subscription. So when we unsubscribe from the resource, we need to -// receive a confirmation from the server whether to keep the resource (which means that it was a -// part of our wildcard subscription) or to drop it. -// -// Please refer to drawings (non-wildcard-resource-state-machine.png and -// (wildcard-resource-state-machine.png) for visual depictions of the resource state machine. -// -// In the "no wildcard subscription" scenario all the cached resources should be in the "requested" -// category. Resources are added to the category upon the explicit request and dropped when we -// explicitly unsubscribe from it. Transitions between "complete" and "waiting for server" happen -// when we receive messages from the server - if a resource in the message is in "added resources" -// list (thus contains version information), the resource becomes "complete". If the resource in the -// message is in "removed resources" list, it changes into the "waiting for server" state. If a -// server sends us a resource that we didn't request, it's going to be ignored. -// -// In the "wildcard subscription" scenario, "requested" category is the same as in "no wildcard -// subscription" scenario, with one exception - the unsubscribed "complete" resource is not removed -// from the cache, but it's moved to the "ambiguous" resources instead. At this point we are waiting -// for the server to tell us that this resource should be either moved to the "wildcard" resources, -// or dropped. Resources in "wildcard" category are only added there or dropped from there by the -// server. Resources from both "wildcard" and "ambiguous" categories can become "requested" -// "complete" resources if we subscribe to them again. -// -// The delta subscription state transitions between the two scenarios depending on whether we are -// subscribed to wildcard resource or not. Nothing special happens when we transition from "no -// wildcard subscription" to "wildcard subscription" scenario, but when transitioning in the other -// direction, we drop all the resources in "wildcard" and "ambiguous" categories. +using DeltaSubscriptionStateVariant = absl::variant; + class DeltaSubscriptionState : public Logger::Loggable { public: DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); - // Update which resources we're interested in subscribing to. void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed); void addAliasesToResolve(const absl::flat_hash_set& aliases); - void setMustSendDiscoveryRequest() { must_send_discovery_request_ = true; } - - // Whether there was a change in our subscription interest we have yet to inform the server of. + void setMustSendDiscoveryRequest(); bool subscriptionUpdatePending() const; - - void markStreamFresh() { any_request_sent_yet_in_current_stream_ = false; } - + void markStreamFresh(); UpdateAck handleResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); - void handleEstablishmentFailure(); - - // Returns the next gRPC request proto to be sent off to the server, based on this object's - // understanding of the current protocol state, and new resources that Envoy wants to request. envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestAckless(); - - // The WithAck version first calls the Ack-less version, then adds in the passed-in ack. envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestWithAck(const UpdateAck& ack); DeltaSubscriptionState(const DeltaSubscriptionState&) = delete; DeltaSubscriptionState& operator=(const DeltaSubscriptionState&) = delete; private: - bool isHeartbeatResponse(const envoy::service::discovery::v3::Resource& resource) const; - void handleGoodResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); - void handleBadResponse(const EnvoyException& e, UpdateAck& ack); - - class ResourceState { - public: - // Builds a ResourceState in the waitingForServer state. - ResourceState() = default; - // Builds a ResourceState with a specific version - ResourceState(absl::string_view version) : version_(version) {} - // Self-documenting alias of default constructor. - static ResourceState waitingForServer() { return ResourceState(); } - // Self-documenting alias of constructor with version. - static ResourceState withVersion(absl::string_view version) { return ResourceState(version); } - - // If true, we currently have no version of this resource - we are waiting for the server to - // provide us with one. - bool isWaitingForServer() const { return version_ == absl::nullopt; } - - void setAsWaitingForServer() { version_ = absl::nullopt; } - void setVersion(absl::string_view version) { version_ = std::string(version); } - - // Must not be called if waitingForServer() == true. - std::string version() const { - ASSERT(version_.has_value()); - return version_.value_or(""); - } - - private: - absl::optional version_; - }; - - void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); - OptRef getRequestedResourceState(absl::string_view resource_name); - OptRef getRequestedResourceState(absl::string_view resource_name) const; - - bool isInitialRequestForLegacyWildcard(); - - // A map from resource name to per-resource version. The keys of this map are exactly the resource - // names we are currently interested in. Those in the waitingForServer state currently don't have - // any version for that resource: we need to inform the server if we lose interest in them, but we - // also need to *not* include them in the initial_resource_versions map upon a reconnect. - absl::node_hash_map requested_resource_state_; - // A map from resource name to per-resource version. The keys of this map are resource names we - // have received as a part of the wildcard subscription. - absl::node_hash_map wildcard_resource_state_; - // Used for storing resources that we lost interest in, but could - // also be a part of wildcard subscription. - absl::node_hash_map ambiguous_resource_state_; - - // Not all xDS resources supports heartbeats due to there being specific information encoded in - // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just - // disable heartbeats for these resources (currently only VHDS). - const bool supports_heartbeats_; - TtlManager ttl_; - - const std::string type_url_; - UntypedConfigUpdateCallbacks& watch_map_; - const LocalInfo::LocalInfo& local_info_; - Event::Dispatcher& dispatcher_; - - bool in_initial_legacy_wildcard_{true}; - bool any_request_sent_yet_in_current_stream_{}; - bool must_send_discovery_request_{}; - - // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. - // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. - // Feel free to change to an unordered container once we figure out how to make it work. - std::set names_added_; - std::set names_removed_; + absl::variant state_; }; } // namespace Config diff --git a/source/common/config/new_delta_subscription_state.cc b/source/common/config/new_delta_subscription_state.cc new file mode 100644 index 000000000000..3c57cb433e98 --- /dev/null +++ b/source/common/config/new_delta_subscription_state.cc @@ -0,0 +1,409 @@ +#include "source/common/config/delta_subscription_state.h" + +#include "envoy/event/dispatcher.h" +#include "envoy/service/discovery/v3/discovery.pb.h" + +#include "source/common/common/assert.h" +#include "source/common/common/hash.h" +#include "source/common/config/utility.h" +#include "source/common/runtime/runtime_features.h" + +namespace Envoy { +namespace Config { + +DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, + UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, + Event::Dispatcher& dispatcher) + // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on + // empty resources as updates. + : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), + ttl_( + [this](const auto& expired) { + Protobuf::RepeatedPtrField removed_resources; + for (const auto& resource : expired) { + if (auto maybe_resource = getRequestedResourceState(resource); + maybe_resource.has_value()) { + maybe_resource->setAsWaitingForServer(); + removed_resources.Add(std::string(resource)); + } else if (const auto erased_count = wildcard_resource_state_.erase(resource) + + ambiguous_resource_state_.erase(resource); + erased_count > 0) { + removed_resources.Add(std::string(resource)); + } + } + + watch_map_.onConfigUpdate({}, removed_resources, ""); + }, + dispatcher, dispatcher.timeSource()), + type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), + dispatcher_(dispatcher) {} + +void DeltaSubscriptionState::updateSubscriptionInterest( + const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed) { + for (const auto& a : cur_added) { + if (in_initial_legacy_wildcard_ && a != Wildcard) { + in_initial_legacy_wildcard_ = false; + } + // If the requested resource existed as a wildcard resource, + // transition it to requested. Otherwise mark it as a resource + // waiting for the server to receive the version. + if (auto it = wildcard_resource_state_.find(a); it != wildcard_resource_state_.end()) { + requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); + wildcard_resource_state_.erase(it); + } else if (it = ambiguous_resource_state_.find(a); it != ambiguous_resource_state_.end()) { + requested_resource_state_.insert_or_assign(a, ResourceState::withVersion(it->second)); + ambiguous_resource_state_.erase(it); + } else { + requested_resource_state_.insert_or_assign(a, ResourceState::waitingForServer()); + } + ASSERT(requested_resource_state_.contains(a)); + ASSERT(!wildcard_resource_state_.contains(a)); + ASSERT(!ambiguous_resource_state_.contains(a)); + // If interest in a resource is removed-then-added (all before a discovery request + // can be sent), we must treat it as a "new" addition: our user may have forgotten its + // copy of the resource after instructing us to remove it, and need to be reminded of it. + names_removed_.erase(a); + names_added_.insert(a); + } + for (const auto& r : cur_removed) { + auto actually_erased = false; + // The resource we have lost the interest in could also come from our wildcard subscription. We + // just don't know it at this point. Instead of removing it outright, mark the resource as not + // interesting to us any more and the server will send us an update. If we don't have a wildcard + // subscription then there is no ambiguity and just drop the resource. + if (requested_resource_state_.contains(Wildcard)) { + if (auto it = requested_resource_state_.find(r); it != requested_resource_state_.end()) { + // Wildcard resources always have a version. If our requested resource has no version, it + // won't be a wildcard resource then. If r is Wildcard itself, then it never has a version + // attached to it, so it will not be moved to ambiguous category. + if (!it->second.isWaitingForServer()) { + ambiguous_resource_state_.insert({it->first, it->second.version()}); + } + requested_resource_state_.erase(it); + actually_erased = true; + } + } else { + actually_erased = (requested_resource_state_.erase(r) > 0); + } + ASSERT(!requested_resource_state_.contains(r)); + // Ideally, when interest in a resource is added-then-removed in between requests, + // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" + // in the request. However, the removed-then-added case *does* need to go in the request, + // and due to how we accomplish that, it's difficult to distinguish remove-add-remove from + // add-remove (because "remove-add" has to be treated as equivalent to just "add"). + names_added_.erase(r); + if (actually_erased) { + names_removed_.insert(r); + in_initial_legacy_wildcard_ = false; + } + } + // If we unsubscribe from wildcard resource, drop all the resources that came from wildcard from + // cache. Also drop the ambiguous resources - we aren't interested in those, but we didn't know if + // those came from wildcard subscription or not, but now it's not important any more. + if (cur_removed.contains(Wildcard)) { + wildcard_resource_state_.clear(); + ambiguous_resource_state_.clear(); + } +} + +// Not having sent any requests yet counts as an "update pending" since you're supposed to resend +// the entirety of your interest at the start of a stream, even if nothing has changed. +bool DeltaSubscriptionState::subscriptionUpdatePending() const { + if (!names_added_.empty() || !names_removed_.empty()) { + return true; + } + // At this point, we have no new resources to subscribe to or any + // resources to unsubscribe from. + if (!any_request_sent_yet_in_current_stream_) { + // If we haven't sent anything on the current stream, but we are actually interested in some + // resource then we obviously need to let the server know about those. + if (!requested_resource_state_.empty()) { + return true; + } + // So there are no new names and we are interested in nothing. This may either mean that we want + // the legacy wildcard subscription to kick in or we actually unsubscribed from everything. If + // the latter is true, then we should not be sending any requests. In such case the initial + // wildcard mode will be false. Otherwise it means that the legacy wildcard request should be + // sent. + return in_initial_legacy_wildcard_; + } + + // At this point, we have no changes in subscription resources and this isn't a first request in + // the stream, so even if there are no resources we are interested in, we can send the request, + // because even if it's empty, it won't be interpreted as legacy wildcard subscription, which can + // only for the first request in the stream. So sending an empty request at this point should be + // harmless. + return must_send_discovery_request_; +} + +UpdateAck DeltaSubscriptionState::handleResponse( + const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { + // We *always* copy the response's nonce into the next request, even if we're going to make that + // request a NACK by setting error_detail. + UpdateAck ack(message.nonce(), type_url_); + TRY_ASSERT_MAIN_THREAD { handleGoodResponse(message); } + END_TRY + catch (const EnvoyException& e) { + handleBadResponse(e, ack); + } + return ack; +} + +bool DeltaSubscriptionState::isHeartbeatResponse( + const envoy::service::discovery::v3::Resource& resource) const { + if (!supports_heartbeats_ && + !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { + return false; + } + if (resource.has_resource()) { + return false; + } + + if (const auto maybe_resource = getRequestedResourceState(resource.name()); + maybe_resource.has_value()) { + return !maybe_resource->isWaitingForServer() && resource.version() == maybe_resource->version(); + } + + if (const auto itr = wildcard_resource_state_.find(resource.name()); + itr != wildcard_resource_state_.end()) { + return resource.version() == itr->second; + } + + if (const auto itr = ambiguous_resource_state_.find(resource.name()); + itr != wildcard_resource_state_.end()) { + // In theory we should move the ambiguous resource to wildcard, because probably we shouldn't be + // getting heartbeat responses about resources that we are not interested in, but the server + // could have sent this heartbeat before it learned about our lack of interest in the resource. + return resource.version() == itr->second; + } + + return false; +} + +void DeltaSubscriptionState::handleGoodResponse( + const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { + absl::flat_hash_set names_added_removed; + Protobuf::RepeatedPtrField non_heartbeat_resources; + for (const auto& resource : message.resources()) { + if (!names_added_removed.insert(resource.name()).second) { + throw EnvoyException( + fmt::format("duplicate name {} found among added/updated resources", resource.name())); + } + if (isHeartbeatResponse(resource)) { + continue; + } + non_heartbeat_resources.Add()->CopyFrom(resource); + // DeltaDiscoveryResponses for unresolved aliases don't contain an actual resource + if (!resource.has_resource() && resource.aliases_size() > 0) { + continue; + } + if (message.type_url() != resource.resource().type_url()) { + throw EnvoyException(fmt::format("type URL {} embedded in an individual Any does not match " + "the message-wide type URL {} in DeltaDiscoveryResponse {}", + resource.resource().type_url(), message.type_url(), + message.DebugString())); + } + } + for (const auto& name : message.removed_resources()) { + if (!names_added_removed.insert(name).second) { + throw EnvoyException( + fmt::format("duplicate name {} found in the union of added+removed resources", name)); + } + } + + { + const auto scoped_update = ttl_.scopedTtlUpdate(); + if (requested_resource_state_.contains(Wildcard)) { + for (const auto& resource : message.resources()) { + addResourceStateFromServer(resource); + } + } else { + // We are not subscribed to wildcard, so we only take resources that we explicitly requested + // and ignore the others. + for (const auto& resource : message.resources()) { + if (requested_resource_state_.contains(resource.name())) { + addResourceStateFromServer(resource); + } + } + } + } + + watch_map_.onConfigUpdate(non_heartbeat_resources, message.removed_resources(), + message.system_version_info()); + + // If a resource is gone, there is no longer a meaningful version for it that makes sense to + // provide to the server upon stream reconnect: either it will continue to not exist, in which + // case saying nothing is fine, or the server will bring back something new, which we should + // receive regardless (which is the logic that not specifying a version will get you). + // + // So, leave the version map entry present but blank if we are still interested in the resource. + // It will be left out of initial_resource_versions messages, but will remind us to explicitly + // tell the server "I'm cancelling my subscription" when we lose interest. In case of resources + // received as a part of the wildcard subscription or resources we already lost interest in, we + // just drop them. + for (const auto& resource_name : message.removed_resources()) { + if (auto maybe_resource = getRequestedResourceState(resource_name); + maybe_resource.has_value()) { + maybe_resource->setAsWaitingForServer(); + } else if (const auto erased_count = ambiguous_resource_state_.erase(resource_name); + erased_count == 0) { + wildcard_resource_state_.erase(resource_name); + } + } + ENVOY_LOG(debug, "Delta config for {} accepted with {} resources added, {} removed", type_url_, + message.resources().size(), message.removed_resources().size()); +} + +void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { + // Note that error_detail being set is what indicates that a DeltaDiscoveryRequest is a NACK. + ack.error_detail_.set_code(Grpc::Status::WellKnownGrpcStatus::Internal); + ack.error_detail_.set_message(Config::Utility::truncateGrpcStatusMessage(e.what())); + ENVOY_LOG(warn, "delta config for {} rejected: {}", type_url_, e.what()); + watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, &e); +} + +void DeltaSubscriptionState::handleEstablishmentFailure() { + watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure, + nullptr); +} + +envoy::service::discovery::v3::DeltaDiscoveryRequest +DeltaSubscriptionState::getNextRequestAckless() { + envoy::service::discovery::v3::DeltaDiscoveryRequest request; + must_send_discovery_request_ = false; + if (!any_request_sent_yet_in_current_stream_) { + any_request_sent_yet_in_current_stream_ = true; + const bool is_legacy_wildcard = isInitialRequestForLegacyWildcard(); + // initial_resource_versions "must be populated for first request in a stream". + // Also, since this might be a new server, we must explicitly state *all* of our subscription + // interest. + for (auto const& [resource_name, resource_state] : requested_resource_state_) { + // Populate initial_resource_versions with the resource versions we currently have. + // Resources we are interested in, but are still waiting to get any version of from the + // server, do not belong in initial_resource_versions. (But do belong in new subscriptions!) + if (!resource_state.isWaitingForServer()) { + (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); + } + // We are going over a list of resources that we are interested in, so add them to + // resource_names_subscribe. + names_added_.insert(resource_name); + } + for (auto const& [resource_name, resource_version] : wildcard_resource_state_) { + (*request.mutable_initial_resource_versions())[resource_name] = resource_version; + } + for (auto const& [resource_name, resource_version] : ambiguous_resource_state_) { + (*request.mutable_initial_resource_versions())[resource_name] = resource_version; + } + // If this is a legacy wildcard request, then make sure that the resource_names_subscribe is + // empty. + if (is_legacy_wildcard) { + names_added_.clear(); + } + names_removed_.clear(); + } + std::copy(names_added_.begin(), names_added_.end(), + Protobuf::RepeatedFieldBackInserter(request.mutable_resource_names_subscribe())); + std::copy(names_removed_.begin(), names_removed_.end(), + Protobuf::RepeatedFieldBackInserter(request.mutable_resource_names_unsubscribe())); + names_added_.clear(); + names_removed_.clear(); + + request.set_type_url(type_url_); + request.mutable_node()->MergeFrom(local_info_.node()); + return request; +} + +bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { + if (in_initial_legacy_wildcard_) { + requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); + ASSERT(requested_resource_state_.contains(Wildcard)); + ASSERT(!wildcard_resource_state_.contains(Wildcard)); + ASSERT(!ambiguous_resource_state_.contains(Wildcard)); + return true; + } + + // If we are here, this means that we lost our initial wildcard mode, because we subscribed to + // something in the past. We could still be in the situation now that all we are subscribed to now + // is wildcard resource, so in such case try to send a legacy wildcard subscription request + // anyway. For this to happen, two conditions need to apply: + // + // 1. No change in interest. + // 2. The only requested resource is Wildcard resource. + // + // The invariant of the code here is that this code is executed only when + // subscriptionUpdatePending actually returns true, which in our case can only happen if the + // requested resources state_ isn't empty. + ASSERT(!requested_resource_state_.empty()); + + // If our subscription interest didn't change then the first condition for using legacy wildcard + // subscription is met. + if (!names_added_.empty() || !names_removed_.empty()) { + return false; + } + // If we requested only a wildcard resource then the second condition for using legacy wildcard + // condition is met. + return requested_resource_state_.size() == 1 && + requested_resource_state_.begin()->first == Wildcard; +} + +envoy::service::discovery::v3::DeltaDiscoveryRequest +DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { + envoy::service::discovery::v3::DeltaDiscoveryRequest request = getNextRequestAckless(); + request.set_response_nonce(ack.nonce_); + if (ack.error_detail_.code() != Grpc::Status::WellKnownGrpcStatus::Ok) { + // Don't needlessly make the field present-but-empty if status is ok. + request.mutable_error_detail()->CopyFrom(ack.error_detail_); + } + return request; +} + +void DeltaSubscriptionState::addResourceStateFromServer( + const envoy::service::discovery::v3::Resource& resource) { + if (resource.has_ttl()) { + ttl_.add(std::chrono::milliseconds(DurationUtil::durationToMilliseconds(resource.ttl())), + resource.name()); + } else { + ttl_.clear(resource.name()); + } + + if (auto maybe_resource = getRequestedResourceState(resource.name()); + maybe_resource.has_value()) { + // It is a resource that we requested. + maybe_resource->setVersion(resource.version()); + ASSERT(requested_resource_state_.contains(resource.name())); + ASSERT(!wildcard_resource_state_.contains(resource.name())); + ASSERT(!ambiguous_resource_state_.contains(resource.name())); + } else { + // It is a resource that is a part of our wildcard request. + wildcard_resource_state_.insert({resource.name(), resource.version()}); + // The resource could be ambiguous before, but now the ambiguity + // is resolved. + ambiguous_resource_state_.erase(resource.name()); + ASSERT(!requested_resource_state_.contains(resource.name())); + ASSERT(wildcard_resource_state_.contains(resource.name())); + ASSERT(!ambiguous_resource_state_.contains(resource.name())); + } +} + +OptRef +DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) { + auto itr = requested_resource_state_.find(resource_name); + if (itr == requested_resource_state_.end()) { + return {}; + } + return {itr->second}; +} + +OptRef +DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) const { + auto itr = requested_resource_state_.find(resource_name); + if (itr == requested_resource_state_.end()) { + return {}; + } + return {itr->second}; +} + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/new_delta_subscription_state.h b/source/common/config/new_delta_subscription_state.h new file mode 100644 index 000000000000..1f663eddc39c --- /dev/null +++ b/source/common/config/new_delta_subscription_state.h @@ -0,0 +1,181 @@ +#pragma once + +#include "envoy/config/subscription.h" +#include "envoy/event/dispatcher.h" +#include "envoy/grpc/status.h" +#include "envoy/local_info/local_info.h" +#include "envoy/service/discovery/v3/discovery.pb.h" + +#include "source/common/common/assert.h" +#include "source/common/common/logger.h" +#include "source/common/config/api_version.h" +#include "source/common/config/pausable_ack_queue.h" +#include "source/common/config/ttl.h" +#include "source/common/config/watch_map.h" + +#include "absl/container/node_hash_map.h" + +namespace Envoy { +namespace Config { + +// Tracks the xDS protocol state of an individual ongoing delta xDS session, i.e. a single type_url. +// There can be multiple DeltaSubscriptionStates active. They will always all be +// blissfully unaware of each other's existence, even when their messages are +// being multiplexed together by ADS. +// +// There are two scenarios which affect how DeltaSubscriptionState manages the resources. First +// scenario is when we are subscribed to a wildcard resource, and other scenario is when we are not. +// +// Delta subscription state also divides the resources it cached into three categories: requested, +// wildcard and ambiguous. +// +// The "requested" category is for resources that we have explicitly asked for (either through the +// initial set of resources or through the on-demand mechanism). Resources in this category are in +// one of two states: "complete" and "waiting for server". +// +// "Complete" resources are resources about which the server sent us the information we need (for +// now - just resource version). +// +// The "waiting for server" state is either for resources that we have just requested, but we still +// didn't receive any version information from the server, or for the "complete" resources that, +// according to the server, are gone, but we are still interested in them - in such case we strip +// the information from the resource. +// +// The "wildcard" category is for resources that we are not explicitly interested in, but we are +// indirectly interested through the subscription to the wildcard resource. +// +// The "ambiguous" category is for resources that we stopped being interested in, but we may still +// be interested indirectly through the wildcard subscription. This situation happens because of the +// xDS protocol limitation - the server isn't able to tell us that the resource we subscribed to is +// also a part of our wildcard subscription. So when we unsubscribe from the resource, we need to +// receive a confirmation from the server whether to keep the resource (which means that it was a +// part of our wildcard subscription) or to drop it. +// +// Please refer to drawings (non-wildcard-resource-state-machine.png and +// (wildcard-resource-state-machine.png) for visual depictions of the resource state machine. +// +// In the "no wildcard subscription" scenario all the cached resources should be in the "requested" +// category. Resources are added to the category upon the explicit request and dropped when we +// explicitly unsubscribe from it. Transitions between "complete" and "waiting for server" happen +// when we receive messages from the server - if a resource in the message is in "added resources" +// list (thus contains version information), the resource becomes "complete". If the resource in the +// message is in "removed resources" list, it changes into the "waiting for server" state. If a +// server sends us a resource that we didn't request, it's going to be ignored. +// +// In the "wildcard subscription" scenario, "requested" category is the same as in "no wildcard +// subscription" scenario, with one exception - the unsubscribed "complete" resource is not removed +// from the cache, but it's moved to the "ambiguous" resources instead. At this point we are waiting +// for the server to tell us that this resource should be either moved to the "wildcard" resources, +// or dropped. Resources in "wildcard" category are only added there or dropped from there by the +// server. Resources from both "wildcard" and "ambiguous" categories can become "requested" +// "complete" resources if we subscribe to them again. +// +// The delta subscription state transitions between the two scenarios depending on whether we are +// subscribed to wildcard resource or not. Nothing special happens when we transition from "no +// wildcard subscription" to "wildcard subscription" scenario, but when transitioning in the other +// direction, we drop all the resources in "wildcard" and "ambiguous" categories. +class DeltaSubscriptionState : public Logger::Loggable { +public: + DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); + + // Update which resources we're interested in subscribing to. + void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed); + void addAliasesToResolve(const absl::flat_hash_set& aliases); + void setMustSendDiscoveryRequest() { must_send_discovery_request_ = true; } + + // Whether there was a change in our subscription interest we have yet to inform the server of. + bool subscriptionUpdatePending() const; + + void markStreamFresh() { any_request_sent_yet_in_current_stream_ = false; } + + UpdateAck handleResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); + + void handleEstablishmentFailure(); + + // Returns the next gRPC request proto to be sent off to the server, based on this object's + // understanding of the current protocol state, and new resources that Envoy wants to request. + envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestAckless(); + + // The WithAck version first calls the Ack-less version, then adds in the passed-in ack. + envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestWithAck(const UpdateAck& ack); + + DeltaSubscriptionState(const DeltaSubscriptionState&) = delete; + DeltaSubscriptionState& operator=(const DeltaSubscriptionState&) = delete; + +private: + bool isHeartbeatResponse(const envoy::service::discovery::v3::Resource& resource) const; + void handleGoodResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); + void handleBadResponse(const EnvoyException& e, UpdateAck& ack); + + class ResourceState { + public: + // Builds a ResourceState in the waitingForServer state. + ResourceState() = default; + // Builds a ResourceState with a specific version + ResourceState(absl::string_view version) : version_(version) {} + // Self-documenting alias of default constructor. + static ResourceState waitingForServer() { return ResourceState(); } + // Self-documenting alias of constructor with version. + static ResourceState withVersion(absl::string_view version) { return ResourceState(version); } + + // If true, we currently have no version of this resource - we are waiting for the server to + // provide us with one. + bool isWaitingForServer() const { return version_ == absl::nullopt; } + + void setAsWaitingForServer() { version_ = absl::nullopt; } + void setVersion(absl::string_view version) { version_ = std::string(version); } + + // Must not be called if waitingForServer() == true. + std::string version() const { + ASSERT(version_.has_value()); + return version_.value_or(""); + } + + private: + absl::optional version_; + }; + + void addResourceStateFromServer(const envoy::service::discovery::v3::Resource& resource); + OptRef getRequestedResourceState(absl::string_view resource_name); + OptRef getRequestedResourceState(absl::string_view resource_name) const; + + bool isInitialRequestForLegacyWildcard(); + + // A map from resource name to per-resource version. The keys of this map are exactly the resource + // names we are currently interested in. Those in the waitingForServer state currently don't have + // any version for that resource: we need to inform the server if we lose interest in them, but we + // also need to *not* include them in the initial_resource_versions map upon a reconnect. + absl::node_hash_map requested_resource_state_; + // A map from resource name to per-resource version. The keys of this map are resource names we + // have received as a part of the wildcard subscription. + absl::node_hash_map wildcard_resource_state_; + // Used for storing resources that we lost interest in, but could + // also be a part of wildcard subscription. + absl::node_hash_map ambiguous_resource_state_; + + // Not all xDS resources supports heartbeats due to there being specific information encoded in + // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just + // disable heartbeats for these resources (currently only VHDS). + const bool supports_heartbeats_; + TtlManager ttl_; + + const std::string type_url_; + UntypedConfigUpdateCallbacks& watch_map_; + const LocalInfo::LocalInfo& local_info_; + Event::Dispatcher& dispatcher_; + + bool in_initial_legacy_wildcard_{true}; + bool any_request_sent_yet_in_current_stream_{}; + bool must_send_discovery_request_{}; + + // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. + // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. + // Feel free to change to an unordered container once we figure out how to make it work. + std::set names_added_; + std::set names_removed_; +}; + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/old_delta_subscription_state.cc b/source/common/config/old_delta_subscription_state.cc new file mode 100644 index 000000000000..7bf5e109f981 --- /dev/null +++ b/source/common/config/old_delta_subscription_state.cc @@ -0,0 +1,248 @@ +#include "source/common/config/old_delta_subscription_state.h" + +#include "envoy/event/dispatcher.h" +#include "envoy/service/discovery/v3/discovery.pb.h" + +#include "source/common/common/assert.h" +#include "source/common/common/hash.h" +#include "source/common/config/utility.h" +#include "source/common/runtime/runtime_features.h" + +namespace Envoy { +namespace Config { + +OldDeltaSubscriptionState::OldDeltaSubscriptionState(std::string type_url, + UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, + Event::Dispatcher& dispatcher) + // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on + // empty resources as updates. + : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), + ttl_( + [this](const auto& expired) { + Protobuf::RepeatedPtrField removed_resources; + for (const auto& resource : expired) { + setResourceWaitingForServer(resource); + removed_resources.Add(std::string(resource)); + } + + watch_map_.onConfigUpdate({}, removed_resources, ""); + }, + dispatcher, dispatcher.timeSource()), + type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), + dispatcher_(dispatcher) {} + +void DeltaSubscriptionState::updateSubscriptionInterest( + const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed) { + if (wildcard_set_) { + wildcard_set_ = true; + wildcard_ = cur_added.empty() && cur_removed.empty(); + } + for (const auto& a : cur_added) { + setResourceWaitingForServer(a); + // If interest in a resource is removed-then-added (all before a discovery request + // can be sent), we must treat it as a "new" addition: our user may have forgotten its + // copy of the resource after instructing us to remove it, and need to be reminded of it. + names_removed_.erase(a); + names_added_.insert(a); + } + for (const auto& r : cur_removed) { + removeResourceState(r); + // Ideally, when interest in a resource is added-then-removed in between requests, + // we would avoid putting a superfluous "unsubscribe [resource that was never subscribed]" + // in the request. However, the removed-then-added case *does* need to go in the request, + // and due to how we accomplish that, it's difficult to distinguish remove-add-remove from + // add-remove (because "remove-add" has to be treated as equivalent to just "add"). + names_added_.erase(r); + names_removed_.insert(r); + } +} + +// Not having sent any requests yet counts as an "update pending" since you're supposed to resend +// the entirety of your interest at the start of a stream, even if nothing has changed. +bool DeltaSubscriptionState::subscriptionUpdatePending() const { + return !names_added_.empty() || !names_removed_.empty() || + !any_request_sent_yet_in_current_stream_ || must_send_discovery_request_; +} + +UpdateAck DeltaSubscriptionState::handleResponse( + const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { + // We *always* copy the response's nonce into the next request, even if we're going to make that + // request a NACK by setting error_detail. + UpdateAck ack(message.nonce(), type_url_); + TRY_ASSERT_MAIN_THREAD { handleGoodResponse(message); } + END_TRY + catch (const EnvoyException& e) { + handleBadResponse(e, ack); + } + return ack; +} + +bool DeltaSubscriptionState::isHeartbeatResponse( + const envoy::service::discovery::v3::Resource& resource) const { + if (!supports_heartbeats_ && + !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { + return false; + } + const auto itr = resource_state_.find(resource.name()); + if (itr == resource_state_.end()) { + return false; + } + + return !resource.has_resource() && !itr->second.waitingForServer() && + resource.version() == itr->second.version(); +} + +void DeltaSubscriptionState::handleGoodResponse( + const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { + absl::flat_hash_set names_added_removed; + Protobuf::RepeatedPtrField non_heartbeat_resources; + for (const auto& resource : message.resources()) { + if (!names_added_removed.insert(resource.name()).second) { + throw EnvoyException( + fmt::format("duplicate name {} found among added/updated resources", resource.name())); + } + if (isHeartbeatResponse(resource)) { + continue; + } + non_heartbeat_resources.Add()->CopyFrom(resource); + // DeltaDiscoveryResponses for unresolved aliases don't contain an actual resource + if (!resource.has_resource() && resource.aliases_size() > 0) { + continue; + } + if (message.type_url() != resource.resource().type_url()) { + throw EnvoyException(fmt::format("type URL {} embedded in an individual Any does not match " + "the message-wide type URL {} in DeltaDiscoveryResponse {}", + resource.resource().type_url(), message.type_url(), + message.DebugString())); + } + } + for (const auto& name : message.removed_resources()) { + if (!names_added_removed.insert(name).second) { + throw EnvoyException( + fmt::format("duplicate name {} found in the union of added+removed resources", name)); + } + } + + { + const auto scoped_update = ttl_.scopedTtlUpdate(); + for (const auto& resource : message.resources()) { + if (wildcard_ || resource_state_.contains(resource.name())) { + // Only consider tracked resources. + // NOTE: This is not gonna work for xdstp resources with glob resource matching. + addResourceState(resource); + } + } + } + + watch_map_.onConfigUpdate(non_heartbeat_resources, message.removed_resources(), + message.system_version_info()); + + // If a resource is gone, there is no longer a meaningful version for it that makes sense to + // provide to the server upon stream reconnect: either it will continue to not exist, in which + // case saying nothing is fine, or the server will bring back something new, which we should + // receive regardless (which is the logic that not specifying a version will get you). + // + // So, leave the version map entry present but blank. It will be left out of + // initial_resource_versions messages, but will remind us to explicitly tell the server "I'm + // cancelling my subscription" when we lose interest. + for (const auto& resource_name : message.removed_resources()) { + if (resource_names_.find(resource_name) != resource_names_.end()) { + setResourceWaitingForServer(resource_name); + } + } + ENVOY_LOG(debug, "Delta config for {} accepted with {} resources added, {} removed", type_url_, + message.resources().size(), message.removed_resources().size()); +} + +void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { + // Note that error_detail being set is what indicates that a DeltaDiscoveryRequest is a NACK. + ack.error_detail_.set_code(Grpc::Status::WellKnownGrpcStatus::Internal); + ack.error_detail_.set_message(Config::Utility::truncateGrpcStatusMessage(e.what())); + ENVOY_LOG(warn, "delta config for {} rejected: {}", type_url_, e.what()); + watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, &e); +} + +void DeltaSubscriptionState::handleEstablishmentFailure() { + watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure, + nullptr); +} + +envoy::service::discovery::v3::DeltaDiscoveryRequest +DeltaSubscriptionState::getNextRequestAckless() { + envoy::service::discovery::v3::DeltaDiscoveryRequest request; + must_send_discovery_request_ = false; + if (!any_request_sent_yet_in_current_stream_) { + any_request_sent_yet_in_current_stream_ = true; + // initial_resource_versions "must be populated for first request in a stream". + // Also, since this might be a new server, we must explicitly state *all* of our subscription + // interest. + for (auto const& [resource_name, resource_state] : resource_state_) { + // Populate initial_resource_versions with the resource versions we currently have. + // Resources we are interested in, but are still waiting to get any version of from the + // server, do not belong in initial_resource_versions. (But do belong in new subscriptions!) + if (!resource_state.waitingForServer()) { + (*request.mutable_initial_resource_versions())[resource_name] = resource_state.version(); + } + // As mentioned above, fill resource_names_subscribe with everything, including names we + // have yet to receive any resource for unless this is a wildcard subscription, for which + // the first request on a stream must be without any resource names. + if (!wildcard_) { + names_added_.insert(resource_name); + } + } + // Wildcard subscription initial requests must have no resource_names_subscribe. + if (wildcard_) { + names_added_.clear(); + } + names_removed_.clear(); + } + std::copy(names_added_.begin(), names_added_.end(), + Protobuf::RepeatedFieldBackInserter(request.mutable_resource_names_subscribe())); + std::copy(names_removed_.begin(), names_removed_.end(), + Protobuf::RepeatedFieldBackInserter(request.mutable_resource_names_unsubscribe())); + names_added_.clear(); + names_removed_.clear(); + + request.set_type_url(type_url_); + request.mutable_node()->MergeFrom(local_info_.node()); + return request; +} + +envoy::service::discovery::v3::DeltaDiscoveryRequest +DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { + envoy::service::discovery::v3::DeltaDiscoveryRequest request = getNextRequestAckless(); + request.set_response_nonce(ack.nonce_); + if (ack.error_detail_.code() != Grpc::Status::WellKnownGrpcStatus::Ok) { + // Don't needlessly make the field present-but-empty if status is ok. + request.mutable_error_detail()->CopyFrom(ack.error_detail_); + } + return request; +} + +void DeltaSubscriptionState::addResourceState( + const envoy::service::discovery::v3::Resource& resource) { + if (resource.has_ttl()) { + ttl_.add(std::chrono::milliseconds(DurationUtil::durationToMilliseconds(resource.ttl())), + resource.name()); + } else { + ttl_.clear(resource.name()); + } + + resource_state_[resource.name()] = ResourceState(resource); + resource_names_.insert(resource.name()); +} + +void DeltaSubscriptionState::setResourceWaitingForServer(const std::string& resource_name) { + resource_state_[resource_name] = ResourceState(); + resource_names_.insert(resource_name); +} + +void DeltaSubscriptionState::removeResourceState(const std::string& resource_name) { + resource_state_.erase(resource_name); + resource_names_.erase(resource_name); +} + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/old_delta_subscription_state.h b/source/common/config/old_delta_subscription_state.h new file mode 100644 index 000000000000..db8dbf6b2a5f --- /dev/null +++ b/source/common/config/old_delta_subscription_state.h @@ -0,0 +1,124 @@ +#pragma once + +#include "envoy/config/subscription.h" +#include "envoy/event/dispatcher.h" +#include "envoy/grpc/status.h" +#include "envoy/local_info/local_info.h" +#include "envoy/service/discovery/v3/discovery.pb.h" + +#include "source/common/common/assert.h" +#include "source/common/common/logger.h" +#include "source/common/config/api_version.h" +#include "source/common/config/pausable_ack_queue.h" +#include "source/common/config/ttl.h" +#include "source/common/config/watch_map.h" + +#include "absl/container/node_hash_map.h" + +namespace Envoy { +namespace Config { + +// Tracks the xDS protocol state of an individual ongoing delta xDS session, i.e. a single type_url. +// There can be multiple DeltaSubscriptionStates active. They will always all be +// blissfully unaware of each other's existence, even when their messages are +// being multiplexed together by ADS. +class OldDeltaSubscriptionState : public Logger::Loggable { +public: + OldDeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); + + // Update which resources we're interested in subscribing to. + void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed); + void addAliasesToResolve(const absl::flat_hash_set& aliases); + void setMustSendDiscoveryRequest() { must_send_discovery_request_ = true; } + + // Whether there was a change in our subscription interest we have yet to inform the server of. + bool subscriptionUpdatePending() const; + + void markStreamFresh() { any_request_sent_yet_in_current_stream_ = false; } + + UpdateAck handleResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); + + void handleEstablishmentFailure(); + + // Returns the next gRPC request proto to be sent off to the server, based on this object's + // understanding of the current protocol state, and new resources that Envoy wants to request. + envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestAckless(); + + // The WithAck version first calls the Ack-less version, then adds in the passed-in ack. + envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestWithAck(const UpdateAck& ack); + + DeltaSubscriptionState(const DeltaSubscriptionState&) = delete; + DeltaSubscriptionState& operator=(const DeltaSubscriptionState&) = delete; + +private: + bool isHeartbeatResponse(const envoy::service::discovery::v3::Resource& resource) const; + void handleGoodResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message); + void handleBadResponse(const EnvoyException& e, UpdateAck& ack); + + class ResourceState { + public: + ResourceState(const envoy::service::discovery::v3::Resource& resource) + : version_(resource.version()) {} + + // Builds a ResourceState in the waitingForServer state. + ResourceState() = default; + + // If true, we currently have no version of this resource - we are waiting for the server to + // provide us with one. + bool waitingForServer() const { return version_ == absl::nullopt; } + + // Must not be called if waitingForServer() == true. + std::string version() const { + ASSERT(version_.has_value()); + return version_.value_or(""); + } + + private: + absl::optional version_; + }; + + // Use these helpers to ensure resource_state_ and resource_names_ get updated together. + void addResourceState(const envoy::service::discovery::v3::Resource& resource); + void setResourceWaitingForServer(const std::string& resource_name); + void removeResourceState(const std::string& resource_name); + + void populateDiscoveryRequest(envoy::service::discovery::v3::DeltaDiscoveryResponse& request); + + // A map from resource name to per-resource version. The keys of this map are exactly the resource + // names we are currently interested in. Those in the waitingForServer state currently don't have + // any version for that resource: we need to inform the server if we lose interest in them, but we + // also need to *not* include them in the initial_resource_versions map upon a reconnect. + absl::node_hash_map resource_state_; + + // Not all xDS resources supports heartbeats due to there being specific information encoded in + // an empty response, which is indistinguishable from a heartbeat in some cases. For now we just + // disable heartbeats for these resources (currently only VHDS). + const bool supports_heartbeats_; + TtlManager ttl_; + // The keys of resource_versions_. Only tracked separately because std::map does not provide an + // iterator into just its keys. + absl::flat_hash_set resource_names_; + + const std::string type_url_; + // Is the subscription is for a wildcard request. + bool wildcard_set_ {}; + bool wildcard_ {}; + UntypedConfigUpdateCallbacks& watch_map_; + const LocalInfo::LocalInfo& local_info_; + Event::Dispatcher& dispatcher_; + std::chrono::milliseconds init_fetch_timeout_; + + bool any_request_sent_yet_in_current_stream_{}; + bool must_send_discovery_request_{}; + + // Tracks changes in our subscription interest since the previous DeltaDiscoveryRequest we sent. + // TODO: Can't use absl::flat_hash_set due to ordering issues in gTest expectation matching. + // Feel free to change to an unordered container once we figure out how to make it work. + std::set names_added_; + std::set names_removed_; +}; + +} // namespace Config +} // namespace Envoy diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index 765f41c26607..df7b650e55f6 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -64,6 +64,7 @@ constexpr const char* runtime_features[] = { "envoy.reloadable_features.disable_tls_inspector_injection", "envoy.reloadable_features.dont_add_content_length_for_bodiless_requests", "envoy.reloadable_features.enable_compression_without_content_length_header", + "envoy.restart_features.explicit_wildcard_resource", "envoy.reloadable_features.grpc_bridge_stats_disabled", "envoy.reloadable_features.grpc_web_fix_non_proto_encoded_response_handling", "envoy.reloadable_features.grpc_json_transcoder_adhere_to_buffer_limits", From 726d655538238deb3caebd00db6b69bfc88e985c Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Fri, 1 Oct 2021 20:59:08 +0200 Subject: [PATCH 40/49] Fixes Signed-off-by: Krzesimir Nowak --- source/common/config/BUILD | 12 ++++---- .../common/config/delta_subscription_state.cc | 29 ++++++++++++------- .../common/config/delta_subscription_state.h | 5 ++-- .../config/new_delta_subscription_state.cc | 2 +- .../config/old_delta_subscription_state.h | 4 +-- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/source/common/config/BUILD b/source/common/config/BUILD index a1e6bd8c2d62..ef5d99552076 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -87,14 +87,14 @@ envoy_cc_library( envoy_cc_library( name = "delta_subscription_state_lib", srcs = [ - "delta_subscription_state.cc" - "new_delta_subscription_state.cc" - "old_delta_subscription_state.cc" + "delta_subscription_state.cc", + "new_delta_subscription_state.cc", + "old_delta_subscription_state.cc", ], hdrs = [ - "delta_subscription_state.h" - "new_delta_subscription_state.h" - "old_delta_subscription_state.h" + "delta_subscription_state.h", + "new_delta_subscription_state.h", + "old_delta_subscription_state.h", ], deps = [ ":api_version_lib", diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 80cc1b11fb79..47adf46a111b 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -6,8 +6,10 @@ namespace Envoy { namespace Config { namespace { -DeltaSubscriptionStateVariant get_state(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher) { +DeltaSubscriptionStateVariant get_state(std::string type_url, + UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, + Event::Dispatcher& dispatcher) { if (Runtime::runtimeFeatureEnabled()) { return OldDeltaSubscriptionState(std::move(type_url), watch_map, local_info, dispatcher); } else { @@ -17,13 +19,15 @@ DeltaSubscriptionStateVariant get_state(std::string type_url, UntypedConfigUpdat } // namespace -DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher) - : state_(get_state(std::move(type_url), watch_map, local_info, dispatcher)) {} +DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, + UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, + Event::Dispatcher& dispatcher) + : state_(get_state(std::move(type_url), watch_map, local_info, dispatcher)) {} -void DeltaSubscriptionState::updateSubscriptionInterest(const absl::flat_hash_set& cur_added, - const absl::flat_hash_set& cur_removed) -{ +void DeltaSubscriptionState::updateSubscriptionInterest( + const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed) { if (auto* state = absl::get_if(state_); state != nullptr) { state->updateSubscriptionInterest(cur_added, cur_removed); return; @@ -67,7 +71,8 @@ void DeltaSubscriptionState::markStreamFresh() { state.markStreamFresh(); } -UpdateAck DeltaSubscriptionState::handleResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { +UpdateAck DeltaSubscriptionState::handleResponse( + const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { if (auto* state = absl::get_if(state_); state != nullptr) { return state->handleResponse(message); } @@ -84,7 +89,8 @@ void DeltaSubscriptionState::handleEstablishmentFailure() { state.handleEstablishmentFailure(); } -envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestAckless() { +envoy::service::discovery::v3::DeltaDiscoveryRequest +DeltaSubscriptionState::getNextRequestAckless() { if (auto* state = absl::get_if(state_); state != nullptr) { return state->getNextRequestAckless(); } @@ -92,7 +98,8 @@ envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::get return state.getNextRequestAckless(); } -envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { +envoy::service::discovery::v3::DeltaDiscoveryRequest +DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { if (auto* state = absl::get_if(state_); state != nullptr) { return state->getNextRequestWithAck(); } diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 6f13d5144add..4bdfbf5f278b 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -5,8 +5,8 @@ #include "envoy/service/discovery/v3/discovery.pb.h" #include "source/common/common/logger.h" -#include "source/common/config/old_delta_subscription_state.h" #include "source/common/config/new_delta_subscription_state.h" +#include "source/common/config/old_delta_subscription_state.h" #include "absl/container/flat_hash_set.h" #include "absl/types/variant.h" @@ -14,7 +14,8 @@ namespace Envoy { namespace Config { -using DeltaSubscriptionStateVariant = absl::variant; +using DeltaSubscriptionStateVariant = + absl::variant; class DeltaSubscriptionState : public Logger::Loggable { public: diff --git a/source/common/config/new_delta_subscription_state.cc b/source/common/config/new_delta_subscription_state.cc index 3c57cb433e98..010fae361fd9 100644 --- a/source/common/config/new_delta_subscription_state.cc +++ b/source/common/config/new_delta_subscription_state.cc @@ -1,4 +1,4 @@ -#include "source/common/config/delta_subscription_state.h" +#include "source/common/config/new_delta_subscription_state.h" #include "envoy/event/dispatcher.h" #include "envoy/service/discovery/v3/discovery.pb.h" diff --git a/source/common/config/old_delta_subscription_state.h b/source/common/config/old_delta_subscription_state.h index db8dbf6b2a5f..d4f3313d54ef 100644 --- a/source/common/config/old_delta_subscription_state.h +++ b/source/common/config/old_delta_subscription_state.h @@ -103,8 +103,8 @@ class OldDeltaSubscriptionState : public Logger::Loggable { const std::string type_url_; // Is the subscription is for a wildcard request. - bool wildcard_set_ {}; - bool wildcard_ {}; + bool wildcard_set_{}; + bool wildcard_{}; UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; Event::Dispatcher& dispatcher_; From 649af01080acad414a736e1ab9c8bc94267993ac Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 4 Oct 2021 08:03:33 +0200 Subject: [PATCH 41/49] Build fixes Signed-off-by: Krzesimir Nowak --- .../common/config/delta_subscription_state.cc | 39 ++++++++---------- .../common/config/delta_subscription_state.h | 3 +- .../config/new_delta_subscription_state.cc | 41 +++++++++---------- .../config/new_delta_subscription_state.h | 20 ++++----- .../config/old_delta_subscription_state.cc | 26 ++++++------ .../config/old_delta_subscription_state.h | 7 ++-- .../config/delta_subscription_state_test.cc | 15 +++---- 7 files changed, 70 insertions(+), 81 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 47adf46a111b..826f28ab9ba3 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -10,10 +10,12 @@ DeltaSubscriptionStateVariant get_state(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher) { - if (Runtime::runtimeFeatureEnabled()) { - return OldDeltaSubscriptionState(std::move(type_url), watch_map, local_info, dispatcher); + if (Runtime::runtimeFeatureEnabled("envoy.restart_features.explicit_wildcard_resource")) { + return DeltaSubscriptionStateVariant(absl::in_place_type, + std::move(type_url), watch_map, local_info, dispatcher); } else { - return NewDeltaSubscriptionState(std::move(type_url), watch_map, local_info, dispatcher); + return DeltaSubscriptionStateVariant(absl::in_place_type, + std::move(type_url), watch_map, local_info, dispatcher); } } @@ -28,25 +30,16 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { state->updateSubscriptionInterest(cur_added, cur_removed); return; } - auto& state = absl::get(state_); - state.updateSubscriptionInterest(cur_added, cur_removed); -} - -void DeltaSubscriptionState::addAliasesToResolve(const absl::flat_hash_set& aliases) { - if (auto* state = absl::get_if(state_); state != nullptr) { - state->addAliasesToResolve(aliases); - return; - } auto& state = absl::get(state_); - state.addAliasesToResolve(aliases); + state.updateSubscriptionInterest(cur_added, cur_removed); } void DeltaSubscriptionState::setMustSendDiscoveryRequest() { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { state->setMustSendDiscoveryRequest(); return; } @@ -55,7 +48,7 @@ void DeltaSubscriptionState::setMustSendDiscoveryRequest() { } bool DeltaSubscriptionState::subscriptionUpdatePending() const { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { return state->subscriptionUpdatePending(); } auto& state = absl::get(state_); @@ -63,7 +56,7 @@ bool DeltaSubscriptionState::subscriptionUpdatePending() const { } void DeltaSubscriptionState::markStreamFresh() { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { state->markStreamFresh(); return; } @@ -73,7 +66,7 @@ void DeltaSubscriptionState::markStreamFresh() { UpdateAck DeltaSubscriptionState::handleResponse( const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { return state->handleResponse(message); } auto& state = absl::get(state_); @@ -81,7 +74,7 @@ UpdateAck DeltaSubscriptionState::handleResponse( } void DeltaSubscriptionState::handleEstablishmentFailure() { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { state->handleEstablishmentFailure(); return; } @@ -91,7 +84,7 @@ void DeltaSubscriptionState::handleEstablishmentFailure() { envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestAckless() { - if (auto* state = absl::get_if(state_); state != nullptr) { + if (auto* state = absl::get_if(&state_); state != nullptr) { return state->getNextRequestAckless(); } auto& state = absl::get(state_); @@ -100,11 +93,11 @@ DeltaSubscriptionState::getNextRequestAckless() { envoy::service::discovery::v3::DeltaDiscoveryRequest DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { - if (auto* state = absl::get_if(state_); state != nullptr) { - return state->getNextRequestWithAck(); + if (auto* state = absl::get_if(&state_); state != nullptr) { + return state->getNextRequestWithAck(ack); } auto& state = absl::get(state_); - return state.getNextRequestWithAck(); + return state.getNextRequestWithAck(ack); } } // namespace Config diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 4bdfbf5f278b..6b613ade0b4f 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -24,7 +24,6 @@ class DeltaSubscriptionState : public Logger::Loggable { void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed); - void addAliasesToResolve(const absl::flat_hash_set& aliases); void setMustSendDiscoveryRequest(); bool subscriptionUpdatePending() const; void markStreamFresh(); @@ -37,7 +36,7 @@ class DeltaSubscriptionState : public Logger::Loggable { DeltaSubscriptionState& operator=(const DeltaSubscriptionState&) = delete; private: - absl::variant state_; + DeltaSubscriptionStateVariant state_; }; } // namespace Config diff --git a/source/common/config/new_delta_subscription_state.cc b/source/common/config/new_delta_subscription_state.cc index 010fae361fd9..94f25ac952eb 100644 --- a/source/common/config/new_delta_subscription_state.cc +++ b/source/common/config/new_delta_subscription_state.cc @@ -11,10 +11,10 @@ namespace Envoy { namespace Config { -DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, - UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, - Event::Dispatcher& dispatcher) +NewDeltaSubscriptionState::NewDeltaSubscriptionState(std::string type_url, + UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, + Event::Dispatcher& dispatcher) // TODO(snowp): Hard coding VHDS here is temporary until we can move it away from relying on // empty resources as updates. : supports_heartbeats_(type_url != "envoy.config.route.v3.VirtualHost"), @@ -36,10 +36,9 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, watch_map_.onConfigUpdate({}, removed_resources, ""); }, dispatcher, dispatcher.timeSource()), - type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), - dispatcher_(dispatcher) {} + type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info) {} -void DeltaSubscriptionState::updateSubscriptionInterest( +void NewDeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { @@ -110,7 +109,7 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // Not having sent any requests yet counts as an "update pending" since you're supposed to resend // the entirety of your interest at the start of a stream, even if nothing has changed. -bool DeltaSubscriptionState::subscriptionUpdatePending() const { +bool NewDeltaSubscriptionState::subscriptionUpdatePending() const { if (!names_added_.empty() || !names_removed_.empty()) { return true; } @@ -138,7 +137,7 @@ bool DeltaSubscriptionState::subscriptionUpdatePending() const { return must_send_discovery_request_; } -UpdateAck DeltaSubscriptionState::handleResponse( +UpdateAck NewDeltaSubscriptionState::handleResponse( const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { // We *always* copy the response's nonce into the next request, even if we're going to make that // request a NACK by setting error_detail. @@ -151,7 +150,7 @@ UpdateAck DeltaSubscriptionState::handleResponse( return ack; } -bool DeltaSubscriptionState::isHeartbeatResponse( +bool NewDeltaSubscriptionState::isHeartbeatResponse( const envoy::service::discovery::v3::Resource& resource) const { if (!supports_heartbeats_ && !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { @@ -182,7 +181,7 @@ bool DeltaSubscriptionState::isHeartbeatResponse( return false; } -void DeltaSubscriptionState::handleGoodResponse( +void NewDeltaSubscriptionState::handleGoodResponse( const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { absl::flat_hash_set names_added_removed; Protobuf::RepeatedPtrField non_heartbeat_resources; @@ -256,7 +255,7 @@ void DeltaSubscriptionState::handleGoodResponse( message.resources().size(), message.removed_resources().size()); } -void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { +void NewDeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { // Note that error_detail being set is what indicates that a DeltaDiscoveryRequest is a NACK. ack.error_detail_.set_code(Grpc::Status::WellKnownGrpcStatus::Internal); ack.error_detail_.set_message(Config::Utility::truncateGrpcStatusMessage(e.what())); @@ -264,13 +263,13 @@ void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAc watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, &e); } -void DeltaSubscriptionState::handleEstablishmentFailure() { +void NewDeltaSubscriptionState::handleEstablishmentFailure() { watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure, nullptr); } envoy::service::discovery::v3::DeltaDiscoveryRequest -DeltaSubscriptionState::getNextRequestAckless() { +NewDeltaSubscriptionState::getNextRequestAckless() { envoy::service::discovery::v3::DeltaDiscoveryRequest request; must_send_discovery_request_ = false; if (!any_request_sent_yet_in_current_stream_) { @@ -315,7 +314,7 @@ DeltaSubscriptionState::getNextRequestAckless() { return request; } -bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { +bool NewDeltaSubscriptionState::isInitialRequestForLegacyWildcard() { if (in_initial_legacy_wildcard_) { requested_resource_state_.insert_or_assign(Wildcard, ResourceState::waitingForServer()); ASSERT(requested_resource_state_.contains(Wildcard)); @@ -349,7 +348,7 @@ bool DeltaSubscriptionState::isInitialRequestForLegacyWildcard() { } envoy::service::discovery::v3::DeltaDiscoveryRequest -DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { +NewDeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { envoy::service::discovery::v3::DeltaDiscoveryRequest request = getNextRequestAckless(); request.set_response_nonce(ack.nonce_); if (ack.error_detail_.code() != Grpc::Status::WellKnownGrpcStatus::Ok) { @@ -359,7 +358,7 @@ DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { return request; } -void DeltaSubscriptionState::addResourceStateFromServer( +void NewDeltaSubscriptionState::addResourceStateFromServer( const envoy::service::discovery::v3::Resource& resource) { if (resource.has_ttl()) { ttl_.add(std::chrono::milliseconds(DurationUtil::durationToMilliseconds(resource.ttl())), @@ -387,8 +386,8 @@ void DeltaSubscriptionState::addResourceStateFromServer( } } -OptRef -DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) { +OptRef +NewDeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) { auto itr = requested_resource_state_.find(resource_name); if (itr == requested_resource_state_.end()) { return {}; @@ -396,8 +395,8 @@ DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_nam return {itr->second}; } -OptRef -DeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) const { +OptRef +NewDeltaSubscriptionState::getRequestedResourceState(absl::string_view resource_name) const { auto itr = requested_resource_state_.find(resource_name); if (itr == requested_resource_state_.end()) { return {}; diff --git a/source/common/config/new_delta_subscription_state.h b/source/common/config/new_delta_subscription_state.h index 1f663eddc39c..9ef841cffb22 100644 --- a/source/common/config/new_delta_subscription_state.h +++ b/source/common/config/new_delta_subscription_state.h @@ -19,11 +19,11 @@ namespace Envoy { namespace Config { // Tracks the xDS protocol state of an individual ongoing delta xDS session, i.e. a single type_url. -// There can be multiple DeltaSubscriptionStates active. They will always all be -// blissfully unaware of each other's existence, even when their messages are -// being multiplexed together by ADS. +// There can be multiple NewDeltaSubscriptionStates active. They will always all be blissfully +// unaware of each other's existence, even when their messages are being multiplexed together by +// ADS. // -// There are two scenarios which affect how DeltaSubscriptionState manages the resources. First +// There are two scenarios which affect how NewDeltaSubscriptionState manages the resources. First // scenario is when we are subscribed to a wildcard resource, and other scenario is when we are not. // // Delta subscription state also divides the resources it cached into three categories: requested, @@ -74,15 +74,14 @@ namespace Config { // subscribed to wildcard resource or not. Nothing special happens when we transition from "no // wildcard subscription" to "wildcard subscription" scenario, but when transitioning in the other // direction, we drop all the resources in "wildcard" and "ambiguous" categories. -class DeltaSubscriptionState : public Logger::Loggable { +class NewDeltaSubscriptionState : public Logger::Loggable { public: - DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); + NewDeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); // Update which resources we're interested in subscribing to. void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed); - void addAliasesToResolve(const absl::flat_hash_set& aliases); void setMustSendDiscoveryRequest() { must_send_discovery_request_ = true; } // Whether there was a change in our subscription interest we have yet to inform the server of. @@ -101,8 +100,8 @@ class DeltaSubscriptionState : public Logger::Loggable { // The WithAck version first calls the Ack-less version, then adds in the passed-in ack. envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestWithAck(const UpdateAck& ack); - DeltaSubscriptionState(const DeltaSubscriptionState&) = delete; - DeltaSubscriptionState& operator=(const DeltaSubscriptionState&) = delete; + NewDeltaSubscriptionState(const NewDeltaSubscriptionState&) = delete; + NewDeltaSubscriptionState& operator=(const NewDeltaSubscriptionState&) = delete; private: bool isHeartbeatResponse(const envoy::service::discovery::v3::Resource& resource) const; @@ -164,7 +163,6 @@ class DeltaSubscriptionState : public Logger::Loggable { const std::string type_url_; UntypedConfigUpdateCallbacks& watch_map_; const LocalInfo::LocalInfo& local_info_; - Event::Dispatcher& dispatcher_; bool in_initial_legacy_wildcard_{true}; bool any_request_sent_yet_in_current_stream_{}; diff --git a/source/common/config/old_delta_subscription_state.cc b/source/common/config/old_delta_subscription_state.cc index 7bf5e109f981..8a4b9272c30e 100644 --- a/source/common/config/old_delta_subscription_state.cc +++ b/source/common/config/old_delta_subscription_state.cc @@ -32,10 +32,10 @@ OldDeltaSubscriptionState::OldDeltaSubscriptionState(std::string type_url, type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), dispatcher_(dispatcher) {} -void DeltaSubscriptionState::updateSubscriptionInterest( +void OldDeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed) { - if (wildcard_set_) { + if (!wildcard_set_) { wildcard_set_ = true; wildcard_ = cur_added.empty() && cur_removed.empty(); } @@ -61,12 +61,12 @@ void DeltaSubscriptionState::updateSubscriptionInterest( // Not having sent any requests yet counts as an "update pending" since you're supposed to resend // the entirety of your interest at the start of a stream, even if nothing has changed. -bool DeltaSubscriptionState::subscriptionUpdatePending() const { +bool OldDeltaSubscriptionState::subscriptionUpdatePending() const { return !names_added_.empty() || !names_removed_.empty() || !any_request_sent_yet_in_current_stream_ || must_send_discovery_request_; } -UpdateAck DeltaSubscriptionState::handleResponse( +UpdateAck OldDeltaSubscriptionState::handleResponse( const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { // We *always* copy the response's nonce into the next request, even if we're going to make that // request a NACK by setting error_detail. @@ -79,7 +79,7 @@ UpdateAck DeltaSubscriptionState::handleResponse( return ack; } -bool DeltaSubscriptionState::isHeartbeatResponse( +bool OldDeltaSubscriptionState::isHeartbeatResponse( const envoy::service::discovery::v3::Resource& resource) const { if (!supports_heartbeats_ && !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.vhds_heartbeats")) { @@ -94,7 +94,7 @@ bool DeltaSubscriptionState::isHeartbeatResponse( resource.version() == itr->second.version(); } -void DeltaSubscriptionState::handleGoodResponse( +void OldDeltaSubscriptionState::handleGoodResponse( const envoy::service::discovery::v3::DeltaDiscoveryResponse& message) { absl::flat_hash_set names_added_removed; Protobuf::RepeatedPtrField non_heartbeat_resources; @@ -156,7 +156,7 @@ void DeltaSubscriptionState::handleGoodResponse( message.resources().size(), message.removed_resources().size()); } -void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { +void OldDeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAck& ack) { // Note that error_detail being set is what indicates that a DeltaDiscoveryRequest is a NACK. ack.error_detail_.set_code(Grpc::Status::WellKnownGrpcStatus::Internal); ack.error_detail_.set_message(Config::Utility::truncateGrpcStatusMessage(e.what())); @@ -164,13 +164,13 @@ void DeltaSubscriptionState::handleBadResponse(const EnvoyException& e, UpdateAc watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, &e); } -void DeltaSubscriptionState::handleEstablishmentFailure() { +void OldDeltaSubscriptionState::handleEstablishmentFailure() { watch_map_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::ConnectionFailure, nullptr); } envoy::service::discovery::v3::DeltaDiscoveryRequest -DeltaSubscriptionState::getNextRequestAckless() { +OldDeltaSubscriptionState::getNextRequestAckless() { envoy::service::discovery::v3::DeltaDiscoveryRequest request; must_send_discovery_request_ = false; if (!any_request_sent_yet_in_current_stream_) { @@ -211,7 +211,7 @@ DeltaSubscriptionState::getNextRequestAckless() { } envoy::service::discovery::v3::DeltaDiscoveryRequest -DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { +OldDeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { envoy::service::discovery::v3::DeltaDiscoveryRequest request = getNextRequestAckless(); request.set_response_nonce(ack.nonce_); if (ack.error_detail_.code() != Grpc::Status::WellKnownGrpcStatus::Ok) { @@ -221,7 +221,7 @@ DeltaSubscriptionState::getNextRequestWithAck(const UpdateAck& ack) { return request; } -void DeltaSubscriptionState::addResourceState( +void OldDeltaSubscriptionState::addResourceState( const envoy::service::discovery::v3::Resource& resource) { if (resource.has_ttl()) { ttl_.add(std::chrono::milliseconds(DurationUtil::durationToMilliseconds(resource.ttl())), @@ -234,12 +234,12 @@ void DeltaSubscriptionState::addResourceState( resource_names_.insert(resource.name()); } -void DeltaSubscriptionState::setResourceWaitingForServer(const std::string& resource_name) { +void OldDeltaSubscriptionState::setResourceWaitingForServer(const std::string& resource_name) { resource_state_[resource_name] = ResourceState(); resource_names_.insert(resource_name); } -void DeltaSubscriptionState::removeResourceState(const std::string& resource_name) { +void OldDeltaSubscriptionState::removeResourceState(const std::string& resource_name) { resource_state_.erase(resource_name); resource_names_.erase(resource_name); } diff --git a/source/common/config/old_delta_subscription_state.h b/source/common/config/old_delta_subscription_state.h index d4f3313d54ef..f8aef137f133 100644 --- a/source/common/config/old_delta_subscription_state.h +++ b/source/common/config/old_delta_subscription_state.h @@ -19,7 +19,7 @@ namespace Envoy { namespace Config { // Tracks the xDS protocol state of an individual ongoing delta xDS session, i.e. a single type_url. -// There can be multiple DeltaSubscriptionStates active. They will always all be +// There can be multiple OldDeltaSubscriptionStates active. They will always all be // blissfully unaware of each other's existence, even when their messages are // being multiplexed together by ADS. class OldDeltaSubscriptionState : public Logger::Loggable { @@ -30,7 +30,6 @@ class OldDeltaSubscriptionState : public Logger::Loggable { // Update which resources we're interested in subscribing to. void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, const absl::flat_hash_set& cur_removed); - void addAliasesToResolve(const absl::flat_hash_set& aliases); void setMustSendDiscoveryRequest() { must_send_discovery_request_ = true; } // Whether there was a change in our subscription interest we have yet to inform the server of. @@ -49,8 +48,8 @@ class OldDeltaSubscriptionState : public Logger::Loggable { // The WithAck version first calls the Ack-less version, then adds in the passed-in ack. envoy::service::discovery::v3::DeltaDiscoveryRequest getNextRequestWithAck(const UpdateAck& ack); - DeltaSubscriptionState(const DeltaSubscriptionState&) = delete; - DeltaSubscriptionState& operator=(const DeltaSubscriptionState&) = delete; + OldDeltaSubscriptionState(const OldDeltaSubscriptionState&) = delete; + OldDeltaSubscriptionState& operator=(const OldDeltaSubscriptionState&) = delete; private: bool isHeartbeatResponse(const envoy::service::discovery::v3::Resource& resource) const; diff --git a/test/common/config/delta_subscription_state_test.cc b/test/common/config/delta_subscription_state_test.cc index 86c0364bf4ee..e908cbd08047 100644 --- a/test/common/config/delta_subscription_state_test.cc +++ b/test/common/config/delta_subscription_state_test.cc @@ -991,19 +991,19 @@ TEST_P(WildcardDeltaSubscriptionStateTest, ResetToLegacyWildcardBehaviorOnStream // All resources from the server should be tracked. TEST_P(WildcardDeltaSubscriptionStateTest, AllResourcesFromServerAreTrackedInWildcardXDS) { - { // Add "name4", "name5", "name6" and remove "name1", "name2", "name3". - updateSubscriptionInterest({"name4", "name5", "name6"}, {"name1", "name2", "name3"}); + { // Add "name4", "name5", "name6" + updateSubscriptionInterest({"name4", "name5", "name6"}, {}); auto cur_request = getNextRequestAckless(); EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4", "name5", "name6")); - EXPECT_THAT(cur_request->resource_names_unsubscribe(), - UnorderedElementsAre("name1", "name2", "name3")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); } { - // On Reconnection, only "name4", "name5", "name6" are sent. + // On Reconnection, only "name4", "name5", "name6" and wildcard resource are sent. markStreamFresh(); auto cur_request = getNextRequestAckless(); - EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre(WildcardStr, "name4", "name5", "name6")); EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); EXPECT_TRUE(cur_request->initial_resource_versions().empty()); } @@ -1023,7 +1023,8 @@ TEST_P(WildcardDeltaSubscriptionStateTest, AllResourcesFromServerAreTrackedInWil { // Simulate a stream reconnection, just to see the current resource_state_. markStreamFresh(); auto cur_request = getNextRequestAckless(); - EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre(WildcardStr, "name4", "name5", "name6")); EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); ASSERT_EQ(cur_request->initial_resource_versions().size(), 4); EXPECT_EQ(cur_request->initial_resource_versions().at("name1"), "version1A"); From 6f4724b241ec9f167b262786993bcd01ca8beaec Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 4 Oct 2021 18:19:03 +0200 Subject: [PATCH 42/49] Test the old implementation too Signed-off-by: Krzesimir Nowak --- test/common/config/BUILD | 21 + .../delta_subscription_state_old_test.cc | 702 ++++++++++++++++++ 2 files changed, 723 insertions(+) create mode 100644 test/common/config/delta_subscription_state_old_test.cc diff --git a/test/common/config/BUILD b/test/common/config/BUILD index bc783732c18c..acbf0c6063c4 100644 --- a/test/common/config/BUILD +++ b/test/common/config/BUILD @@ -75,6 +75,27 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "delta_subscription_state_old_test", + srcs = ["delta_subscription_state_old_test.cc"], + deps = [ + "//source/common/config:delta_subscription_state_lib", + "//source/common/config:grpc_subscription_lib", + "//source/common/config:new_grpc_mux_lib", + "//source/common/stats:isolated_store_lib", + "//test/mocks:common_lib", + "//test/mocks/config:config_mocks", + "//test/mocks/event:event_mocks", + "//test/mocks/grpc:grpc_mocks", + "//test/mocks/local_info:local_info_mocks", + "//test/mocks/runtime:runtime_mocks", + "//test/test_common:logging_lib", + "//test/test_common:test_runtime_lib", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + "@envoy_api//envoy/service/discovery/v3:pkg_cc_proto", + ], +) + envoy_cc_test( name = "sotw_subscription_state_test", srcs = ["sotw_subscription_state_test.cc"], diff --git a/test/common/config/delta_subscription_state_old_test.cc b/test/common/config/delta_subscription_state_old_test.cc new file mode 100644 index 000000000000..6d7ceda982ca --- /dev/null +++ b/test/common/config/delta_subscription_state_old_test.cc @@ -0,0 +1,702 @@ +#include + +#include "envoy/config/cluster/v3/cluster.pb.h" +#include "envoy/service/discovery/v3/discovery.pb.h" + +#include "source/common/config/delta_subscription_state.h" +#include "source/common/config/utility.h" +#include "source/common/stats/isolated_store_impl.h" + +#include "test/mocks/config/mocks.h" +#include "test/mocks/event/mocks.h" +#include "test/mocks/local_info/mocks.h" +#include "test/test_common/simulated_time_system.h" +#include "test/test_common/test_runtime.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::IsSubstring; +using testing::NiceMock; +using testing::Throw; +using testing::UnorderedElementsAre; +using testing::UnorderedElementsAreArray; + +namespace Envoy { +namespace Config { +namespace { + +const char TypeUrl[] = "type.googleapis.com/envoy.config.cluster.v3.Cluster"; + +class OldDeltaSubscriptionStateTestBase : public testing::Test { +protected: + OldDeltaSubscriptionStateTestBase(const std::string& type_url, + const absl::flat_hash_set initial_resources = { + "name1", "name2", "name3"}) { + ttl_timer_ = new Event::MockTimer(&dispatcher_); + + // Disable the explicit wildcard resource feature, so OldDeltaSubscriptionState will be picked + // up. + { + TestScopedRuntime scoped_runtime_; + Runtime::LoaderSingleton::getExisting()->mergeValues({ + {"envoy.restart_features.explicit_wildcard_resource", "false"}, + }); + state_ = std::make_unique(type_url, callbacks_, + local_info_, dispatcher_); + } + updateSubscriptionInterest(initial_resources, {}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), + // UnorderedElementsAre("name1", "name2", "name3")); + UnorderedElementsAreArray(initial_resources.cbegin(), initial_resources.cend())); + } + + void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed) { + state_->updateSubscriptionInterest(cur_added, cur_removed); + } + + std::unique_ptr getNextRequestAckless() { + return std::make_unique( + state_->getNextRequestAckless()); + } + + UpdateAck + handleResponse(const envoy::service::discovery::v3::DeltaDiscoveryResponse& response_proto) { + return state_->handleResponse(response_proto); + } + + UpdateAck deliverDiscoveryResponse( + const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& version_info, absl::optional nonce = absl::nullopt, + bool expect_config_update_call = true, absl::optional updated_resources = {}) { + envoy::service::discovery::v3::DeltaDiscoveryResponse message; + *message.mutable_resources() = added_resources; + *message.mutable_removed_resources() = removed_resources; + message.set_system_version_info(version_info); + if (nonce.has_value()) { + message.set_nonce(nonce.value()); + } + EXPECT_CALL(callbacks_, onConfigUpdate(_, _, _)) + .Times(expect_config_update_call ? 1 : 0) + .WillRepeatedly(Invoke([updated_resources](const auto& added, const auto&, const auto&) { + if (updated_resources) { + EXPECT_EQ(added.size(), *updated_resources); + } + })); + return handleResponse(message); + } + + UpdateAck deliverBadDiscoveryResponse( + const Protobuf::RepeatedPtrField& added_resources, + const Protobuf::RepeatedPtrField& removed_resources, + const std::string& version_info, std::string nonce, std::string error_message) { + envoy::service::discovery::v3::DeltaDiscoveryResponse message; + *message.mutable_resources() = added_resources; + *message.mutable_removed_resources() = removed_resources; + message.set_system_version_info(version_info); + message.set_nonce(nonce); + EXPECT_CALL(callbacks_, onConfigUpdate(_, _, _)).WillOnce(Throw(EnvoyException(error_message))); + return handleResponse(message); + } + + void markStreamFresh() { state_->markStreamFresh(); } + + bool subscriptionUpdatePending() { return state_->subscriptionUpdatePending(); } + + NiceMock callbacks_; + NiceMock local_info_; + NiceMock dispatcher_; + Event::MockTimer* ttl_timer_; + // We start out interested in three resources: name1, name2, and name3. + std::unique_ptr state_; +}; + +Protobuf::RepeatedPtrField +populateRepeatedResource(std::vector> items) { + Protobuf::RepeatedPtrField add_to; + for (const auto& item : items) { + auto* resource = add_to.Add(); + resource->set_name(item.first); + resource->set_version(item.second); + } + return add_to; +} + +class OldDeltaSubscriptionStateTest : public OldDeltaSubscriptionStateTestBase { +public: + OldDeltaSubscriptionStateTest() : OldDeltaSubscriptionStateTestBase(TypeUrl) {} +}; + +// Delta subscription state of a wildcard subscription request. +class OldWildcardDeltaSubscriptionStateTest : public OldDeltaSubscriptionStateTestBase { +public: + OldWildcardDeltaSubscriptionStateTest() : OldDeltaSubscriptionStateTestBase(TypeUrl, {}) {} +}; + +// Basic gaining/losing interest in resources should lead to subscription updates. +TEST_F(OldDeltaSubscriptionStateTest, SubscribeAndUnsubscribe) { + { + updateSubscriptionInterest({"name4"}, {"name1"}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4")); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("name1")); + } + { + updateSubscriptionInterest({"name1"}, {"name3", "name4"}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name1")); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("name3", "name4")); + } +} + +// Resources has no subscriptions should not be tracked. +TEST_F(OldDeltaSubscriptionStateTest, NewPushDoesntAddUntrackedResources) { + { // Add "name4", "name5", "name6" and remove "name1", "name2", "name3". + updateSubscriptionInterest({"name4", "name5", "name6"}, {"name1", "name2", "name3"}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre("name4", "name5", "name6")); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), + UnorderedElementsAre("name1", "name2", "name3")); + } + { + // On Reconnection, only "name4", "name5", "name6" are sent. + markStreamFresh(); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre("name4", "name5", "name6")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + EXPECT_TRUE(cur_request->initial_resource_versions().empty()); + } + // The xDS server's first response includes removed items name1 and 2, and a + // completely unrelated resource "bluhbluh". + { + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource({{"name1", "version1A"}, + {"bluhbluh", "bluh"}, + {"name6", "version6A"}, + {"name2", "version2A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverDiscoveryResponse(added_resources, {}, "debug1", "nonce1"); + EXPECT_EQ("nonce1", ack.nonce_); + EXPECT_EQ(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + } + { // Simulate a stream reconnection, just to see the current resource_state_. + markStreamFresh(); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre("name4", "name5", "name6")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + ASSERT_EQ(cur_request->initial_resource_versions().size(), 1); + EXPECT_TRUE(cur_request->initial_resource_versions().contains("name6")); + EXPECT_EQ(cur_request->initial_resource_versions().at("name6"), "version6A"); + } +} + +// Delta xDS reliably queues up and sends all discovery requests, even in situations where it isn't +// strictly necessary. E.g.: if you subscribe but then unsubscribe to a given resource, all before a +// request was able to be sent, two requests will be sent. The following tests demonstrate this. +// +// If Envoy decided it wasn't interested in a resource and then (before a request was sent) decided +// it was again, for all we know, it dropped that resource in between and needs to retrieve it +// again. So, we *should* send a request "re-"subscribing. This means that the server needs to +// interpret the resource_names_subscribe field as "send these resources even if you think Envoy +// already has them". +TEST_F(OldDeltaSubscriptionStateTest, RemoveThenAdd) { + updateSubscriptionInterest({}, {"name3"}); + updateSubscriptionInterest({"name3"}, {}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name3")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); +} + +// Due to how our implementation provides the required behavior tested in RemoveThenAdd, the +// add-then-remove case *also* causes the resource to be referred to in the request (as an +// unsubscribe). +// Unlike the remove-then-add case, this one really is unnecessary, and ideally we would have +// the request simply not include any mention of the resource. Oh well. +// This test is just here to illustrate that this behavior exists, not to enforce that it +// should be like this. What *is* important: the server must happily and cleanly ignore +// "unsubscribe from [resource name I have never before referred to]" requests. +TEST_F(OldDeltaSubscriptionStateTest, AddThenRemove) { + updateSubscriptionInterest({"name4"}, {}); + updateSubscriptionInterest({}, {"name4"}); + auto cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("name4")); +} + +// add/remove/add == add. +TEST_F(OldDeltaSubscriptionStateTest, AddRemoveAdd) { + updateSubscriptionInterest({"name4"}, {}); + updateSubscriptionInterest({}, {"name4"}); + updateSubscriptionInterest({"name4"}, {}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); +} + +// remove/add/remove == remove. +TEST_F(OldDeltaSubscriptionStateTest, RemoveAddRemove) { + updateSubscriptionInterest({}, {"name3"}); + updateSubscriptionInterest({"name3"}, {}); + updateSubscriptionInterest({}, {"name3"}); + auto cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("name3")); +} + +// Starts with 1,2,3. 4 is added/removed/added. In those same updates, 1,2,3 are +// removed/added/removed. End result should be 4 added and 1,2,3 removed. +TEST_F(OldDeltaSubscriptionStateTest, BothAddAndRemove) { + updateSubscriptionInterest({"name4"}, {"name1", "name2", "name3"}); + updateSubscriptionInterest({"name1", "name2", "name3"}, {"name4"}); + updateSubscriptionInterest({"name4"}, {"name1", "name2", "name3"}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4")); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), + UnorderedElementsAre("name1", "name2", "name3")); +} + +TEST_F(OldDeltaSubscriptionStateTest, CumulativeUpdates) { + updateSubscriptionInterest({"name4"}, {}); + updateSubscriptionInterest({"name5"}, {}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4", "name5")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); +} + +// Verifies that a sequence of good and bad responses from the server all get the appropriate +// ACKs/NACKs from Envoy. +TEST_F(OldDeltaSubscriptionStateTest, AckGenerated) { + // The xDS server's first response includes items for name1 and 2, but not 3. + { + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverDiscoveryResponse(added_resources, {}, "debug1", "nonce1"); + EXPECT_EQ("nonce1", ack.nonce_); + EXPECT_EQ(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + } + // The next response updates 1 and 2, and adds 3. + { + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource( + {{"name1", "version1B"}, {"name2", "version2B"}, {"name3", "version3A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverDiscoveryResponse(added_resources, {}, "debug2", "nonce2"); + EXPECT_EQ("nonce2", ack.nonce_); + EXPECT_EQ(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + } + // The next response tries but fails to update all 3, and so should produce a NACK. + { + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource( + {{"name1", "version1C"}, {"name2", "version2C"}, {"name3", "version3B"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverBadDiscoveryResponse(added_resources, {}, "debug3", "nonce3", "oh no"); + EXPECT_EQ("nonce3", ack.nonce_); + EXPECT_NE(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + } + // The last response successfully updates all 3. + { + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource( + {{"name1", "version1D"}, {"name2", "version2D"}, {"name3", "version3C"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverDiscoveryResponse(added_resources, {}, "debug4", "nonce4"); + EXPECT_EQ("nonce4", ack.nonce_); + EXPECT_EQ(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + } + // Bad response error detail is truncated if it's too large. + { + const std::string very_large_error_message(1 << 20, 'A'); + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource( + {{"name1", "version1D"}, {"name2", "version2D"}, {"name3", "version3D"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverBadDiscoveryResponse(added_resources, {}, "debug5", "nonce5", + very_large_error_message); + EXPECT_EQ("nonce5", ack.nonce_); + EXPECT_NE(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + EXPECT_TRUE(absl::EndsWith(ack.error_detail_.message(), "AAAAAAA...(truncated)")); + EXPECT_LT(ack.error_detail_.message().length(), very_large_error_message.length()); + } +} + +// Tests population of the initial_resource_versions map in the first request of a new stream. +// Tests that +// 1) resources we have a version of are present in the map, +// 2) resources we are interested in but don't have are not present, and +// 3) resources we have lost interest in are not present. +TEST_F(OldDeltaSubscriptionStateTest, ResourceGoneLeadsToBlankInitialVersion) { + { + // The xDS server's first update includes items for name1 and 2, but not 3. + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + markStreamFresh(); // simulate a stream reconnection + auto cur_request = getNextRequestAckless(); + EXPECT_EQ("version1A", cur_request->initial_resource_versions().at("name1")); + EXPECT_EQ("version2A", cur_request->initial_resource_versions().at("name2")); + EXPECT_EQ(cur_request->initial_resource_versions().end(), + cur_request->initial_resource_versions().find("name3")); + } + + { + // The next update updates 1, removes 2, and adds 3. The map should then have 1 and 3. + Protobuf::RepeatedPtrField add1_3 = + populateRepeatedResource({{"name1", "version1B"}, {"name3", "version3A"}}); + Protobuf::RepeatedPtrField remove2; + *remove2.Add() = "name2"; + EXPECT_CALL(*ttl_timer_, disableTimer()).Times(2); + deliverDiscoveryResponse(add1_3, remove2, "debugversion2"); + markStreamFresh(); // simulate a stream reconnection + auto cur_request = getNextRequestAckless(); + EXPECT_EQ("version1B", cur_request->initial_resource_versions().at("name1")); + EXPECT_EQ(cur_request->initial_resource_versions().end(), + cur_request->initial_resource_versions().find("name2")); + EXPECT_EQ("version3A", cur_request->initial_resource_versions().at("name3")); + } + + { + // The next update removes 1 and 3. The map we send the server should be empty... + Protobuf::RepeatedPtrField remove1_3; + *remove1_3.Add() = "name1"; + *remove1_3.Add() = "name3"; + deliverDiscoveryResponse({}, remove1_3, "debugversion3"); + markStreamFresh(); // simulate a stream reconnection + auto cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->initial_resource_versions().empty()); + } + + { + // ...but our own map should remember our interest. In particular, losing interest in a + // resource should cause its name to appear in the next request's resource_names_unsubscribe. + updateSubscriptionInterest({"name4"}, {"name1", "name2"}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), UnorderedElementsAre("name4")); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), UnorderedElementsAre("name1", "name2")); + } +} + +// For non-wildcard subscription, upon a reconnection, the server is supposed to assume a +// blank slate for the Envoy's state (hence the need for initial_resource_versions). +// The resource_names_subscribe of the first message must therefore be every resource the +// Envoy is interested in. +// +// resource_names_unsubscribe, on the other hand, is always blank in the first request - even if, +// in between the last request of the last stream and the first request of the new stream, Envoy +// lost interest in a resource. The unsubscription implicitly takes effect by simply saying +// nothing about the resource in the newly reconnected stream. +TEST_F(OldDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + + updateSubscriptionInterest({"name4"}, {"name1"}); + markStreamFresh(); // simulate a stream reconnection + auto cur_request = getNextRequestAckless(); + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: yes do include: we are interested, its non-wildcard, and we have a version of it. + // name3: yes do include: even though we don't have a version of it, we are interested. + // name4: yes do include: we are newly interested. (If this wasn't a stream reconnect, only + // name4 would belong in this subscribe field). + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre("name2", "name3", "name4")); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); +} + +// For wildcard subscription, upon a reconnection, the server is supposed to assume a +// blank slate for the Envoy's state (hence the need for initial_resource_versions), and +// the resource_names_subscribe and resource_names_unsubscribe must be empty (as is expected +// of every wildcard first message). This is true even if in between the last request of the +// last stream and the first request of the new stream, Envoy gained or lost interest in a +// resource. The subscription & unsubscription implicitly takes effect by simply requesting a +// wildcard subscription in the newly reconnected stream. +TEST_F(OldWildcardDeltaSubscriptionStateTest, SubscribeAndUnsubscribeAfterReconnect) { + Protobuf::RepeatedPtrField add1_2 = + populateRepeatedResource({{"name1", "version1A"}, {"name2", "version2A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + deliverDiscoveryResponse(add1_2, {}, "debugversion1"); + + updateSubscriptionInterest({"name3"}, {"name1"}); + markStreamFresh(); // simulate a stream reconnection + auto cur_request = getNextRequestAckless(); + // Regarding the resource_names_subscribe field: + // name1: do not include: we lost interest. + // name2: do not include: we are interested, but for wildcard it shouldn't be provided. + // name4: do not include: although we are newly interested, an initial wildcard request + // must be with no resources. + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); +} + +// All resources from the server should be tracked. +TEST_F(OldWildcardDeltaSubscriptionStateTest, AllResourcesFromServerAreTrackedInWildcardXDS) { + { // Add "name4", "name5", "name6" and remove "name1", "name2", "name3". + updateSubscriptionInterest({"name4", "name5", "name6"}, {"name1", "name2", "name3"}); + auto cur_request = getNextRequestAckless(); + EXPECT_THAT(cur_request->resource_names_subscribe(), + UnorderedElementsAre("name4", "name5", "name6")); + EXPECT_THAT(cur_request->resource_names_unsubscribe(), + UnorderedElementsAre("name1", "name2", "name3")); + } + { + // On Reconnection, only "name4", "name5", "name6" are sent. + markStreamFresh(); + auto cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + EXPECT_TRUE(cur_request->initial_resource_versions().empty()); + } + // The xDS server's first response includes removed items name1 and 2, and a + // completely unrelated resource "bluhbluh". + { + Protobuf::RepeatedPtrField added_resources = + populateRepeatedResource({{"name1", "version1A"}, + {"bluhbluh", "bluh"}, + {"name6", "version6A"}, + {"name2", "version2A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + UpdateAck ack = deliverDiscoveryResponse(added_resources, {}, "debug1", "nonce1"); + EXPECT_EQ("nonce1", ack.nonce_); + EXPECT_EQ(Grpc::Status::WellKnownGrpcStatus::Ok, ack.error_detail_.code()); + } + { // Simulate a stream reconnection, just to see the current resource_state_. + markStreamFresh(); + auto cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->resource_names_subscribe().empty()); + EXPECT_TRUE(cur_request->resource_names_unsubscribe().empty()); + ASSERT_EQ(cur_request->initial_resource_versions().size(), 4); + EXPECT_EQ(cur_request->initial_resource_versions().at("name1"), "version1A"); + EXPECT_EQ(cur_request->initial_resource_versions().at("bluhbluh"), "bluh"); + EXPECT_EQ(cur_request->initial_resource_versions().at("name6"), "version6A"); + EXPECT_EQ(cur_request->initial_resource_versions().at("name2"), "version2A"); + } +} + +// initial_resource_versions should not be present on messages after the first in a stream. +TEST_F(OldDeltaSubscriptionStateTest, InitialVersionMapFirstMessageOnly) { + // First, verify that the first message of a new stream sends initial versions. + { + // The xDS server's first update gives us all three resources. + Protobuf::RepeatedPtrField add_all = + populateRepeatedResource( + {{"name1", "version1A"}, {"name2", "version2A"}, {"name3", "version3A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + deliverDiscoveryResponse(add_all, {}, "debugversion1"); + markStreamFresh(); // simulate a stream reconnection + auto cur_request = getNextRequestAckless(); + EXPECT_EQ("version1A", cur_request->initial_resource_versions().at("name1")); + EXPECT_EQ("version2A", cur_request->initial_resource_versions().at("name2")); + EXPECT_EQ("version3A", cur_request->initial_resource_versions().at("name3")); + } + // Then, after updating the resources but not reconnecting the stream, verify that initial + // versions are not sent. + { + updateSubscriptionInterest({"name4"}, {}); + // The xDS server updates our resources, and gives us our newly requested one too. + Protobuf::RepeatedPtrField add_all = + populateRepeatedResource({{"name1", "version1B"}, + {"name2", "version2B"}, + {"name3", "version3B"}, + {"name4", "version4A"}}); + EXPECT_CALL(*ttl_timer_, disableTimer()); + deliverDiscoveryResponse(add_all, {}, "debugversion2"); + auto cur_request = getNextRequestAckless(); + EXPECT_TRUE(cur_request->initial_resource_versions().empty()); + } +} + +TEST_F(OldDeltaSubscriptionStateTest, CheckUpdatePending) { + // Note that the test fixture ctor causes the first request to be "sent", so we start in the + // middle of a stream, with our initially interested resources having been requested already. + EXPECT_FALSE(subscriptionUpdatePending()); + updateSubscriptionInterest({}, {}); // no change + EXPECT_FALSE(subscriptionUpdatePending()); + markStreamFresh(); + EXPECT_TRUE(subscriptionUpdatePending()); // no change, BUT fresh stream + updateSubscriptionInterest({}, {"name3"}); // one removed + EXPECT_TRUE(subscriptionUpdatePending()); + updateSubscriptionInterest({"name3"}, {}); // one added + EXPECT_TRUE(subscriptionUpdatePending()); +} + +// The next three tests test that duplicate resource names (whether additions or removals) cause +// DeltaSubscriptionState to reject the update without even trying to hand it to the consuming +// API's onConfigUpdate(). +TEST_F(OldDeltaSubscriptionStateTest, DuplicatedAdd) { + Protobuf::RepeatedPtrField additions = + populateRepeatedResource({{"name1", "version1A"}, {"name1", "sdfsdfsdfds"}}); + UpdateAck ack = deliverDiscoveryResponse(additions, {}, "debugversion1", absl::nullopt, false); + EXPECT_EQ("duplicate name name1 found among added/updated resources", + ack.error_detail_.message()); +} + +TEST_F(OldDeltaSubscriptionStateTest, DuplicatedRemove) { + Protobuf::RepeatedPtrField removals; + *removals.Add() = "name1"; + *removals.Add() = "name1"; + UpdateAck ack = deliverDiscoveryResponse({}, removals, "debugversion1", absl::nullopt, false); + EXPECT_EQ("duplicate name name1 found in the union of added+removed resources", + ack.error_detail_.message()); +} + +TEST_F(OldDeltaSubscriptionStateTest, AddedAndRemoved) { + Protobuf::RepeatedPtrField additions = + populateRepeatedResource({{"name1", "version1A"}}); + Protobuf::RepeatedPtrField removals; + *removals.Add() = "name1"; + UpdateAck ack = + deliverDiscoveryResponse(additions, removals, "debugversion1", absl::nullopt, false); + EXPECT_EQ("duplicate name name1 found in the union of added+removed resources", + ack.error_detail_.message()); +} + +TEST_F(OldDeltaSubscriptionStateTest, ResourceTTL) { + Event::SimulatedTimeSystem time_system; + time_system.setSystemTime(std::chrono::milliseconds(0)); + + auto create_resource_with_ttl = [](absl::optional ttl_s, + bool include_resource) { + Protobuf::RepeatedPtrField added_resources; + auto* resource = added_resources.Add(); + resource->set_name("name1"); + resource->set_version("version1A"); + + if (include_resource) { + resource->mutable_resource(); + } + + if (ttl_s) { + ProtobufWkt::Duration ttl; + ttl.set_seconds(ttl_s->count()); + resource->mutable_ttl()->CopyFrom(ttl); + } + + return added_resources; + }; + + { + EXPECT_CALL(*ttl_timer_, enabled()); + EXPECT_CALL(*ttl_timer_, enableTimer(std::chrono::milliseconds(1000), _)); + deliverDiscoveryResponse(create_resource_with_ttl(std::chrono::seconds(1), true), {}, "debug1", + "nonce1"); + } + + { + // Increase the TTL. + EXPECT_CALL(*ttl_timer_, enabled()); + EXPECT_CALL(*ttl_timer_, enableTimer(std::chrono::milliseconds(2000), _)); + deliverDiscoveryResponse(create_resource_with_ttl(std::chrono::seconds(2), true), {}, "debug1", + "nonce1", true, 1); + } + + { + // Refresh the TTL with a heartbeat. The resource should not be passed to the update callbacks. + EXPECT_CALL(*ttl_timer_, enabled()); + deliverDiscoveryResponse(create_resource_with_ttl(std::chrono::seconds(2), false), {}, "debug1", + "nonce1", true, 0); + } + + // Remove the TTL. + EXPECT_CALL(*ttl_timer_, disableTimer()); + deliverDiscoveryResponse(create_resource_with_ttl(absl::nullopt, true), {}, "debug1", "nonce1", + true, 1); + + // Add back the TTL. + EXPECT_CALL(*ttl_timer_, enabled()); + EXPECT_CALL(*ttl_timer_, enableTimer(_, _)); + deliverDiscoveryResponse(create_resource_with_ttl(std::chrono::seconds(2), true), {}, "debug1", + "nonce1"); + + EXPECT_CALL(callbacks_, onConfigUpdate(_, _, _)); + EXPECT_CALL(*ttl_timer_, disableTimer()); + time_system.setSystemTime(std::chrono::seconds(2)); + + // Invoke the TTL. + ttl_timer_->invokeCallback(); +} + +TEST_F(OldDeltaSubscriptionStateTest, TypeUrlMismatch) { + envoy::service::discovery::v3::DeltaDiscoveryResponse message; + + Protobuf::RepeatedPtrField additions; + auto* resource = additions.Add(); + resource->set_name("name1"); + resource->set_version("version1"); + resource->mutable_resource()->set_type_url("foo"); + + *message.mutable_resources() = additions; + *message.mutable_removed_resources() = {}; + message.set_system_version_info("version1"); + message.set_nonce("nonce1"); + message.set_type_url("bar"); + + EXPECT_CALL(callbacks_, + onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::UpdateRejected, _)) + .WillOnce(Invoke([](Envoy::Config::ConfigUpdateFailureReason, const EnvoyException* e) { + EXPECT_TRUE(IsSubstring("", "", + "type URL foo embedded in an individual Any does not match the " + "message-wide type URL bar", + e->what())); + })); + handleResponse(message); +} + +class OldVhdsDeltaSubscriptionStateTest : public OldDeltaSubscriptionStateTestBase { +public: + OldVhdsDeltaSubscriptionStateTest() + : OldDeltaSubscriptionStateTestBase("envoy.config.route.v3.VirtualHost") {} +}; + +TEST_F(OldVhdsDeltaSubscriptionStateTest, ResourceTTL) { + Event::SimulatedTimeSystem time_system; + time_system.setSystemTime(std::chrono::milliseconds(0)); + + TestScopedRuntime scoped_runtime; + + auto create_resource_with_ttl = [](bool include_resource) { + Protobuf::RepeatedPtrField added_resources; + auto* resource = added_resources.Add(); + resource->set_name("name1"); + resource->set_version("version1A"); + + if (include_resource) { + resource->mutable_resource(); + } + + ProtobufWkt::Duration ttl; + ttl.set_seconds(1); + resource->mutable_ttl()->CopyFrom(ttl); + + return added_resources; + }; + + EXPECT_CALL(*ttl_timer_, enabled()); + EXPECT_CALL(*ttl_timer_, enableTimer(std::chrono::milliseconds(1000), _)); + deliverDiscoveryResponse(create_resource_with_ttl(true), {}, "debug1", "nonce1", true, 1); + + // Heartbeat update should not be propagated to the subscription callback. + EXPECT_CALL(*ttl_timer_, enabled()); + deliverDiscoveryResponse(create_resource_with_ttl(false), {}, "debug1", "nonce1", true, 0); + + // When runtime flag is disabled, maintain old behavior where we do propagate + // the update to the subscription callback. + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.vhds_heartbeats", "false"}}); + + EXPECT_CALL(*ttl_timer_, enabled()); + deliverDiscoveryResponse(create_resource_with_ttl(false), {}, "debug1", "nonce1", true, 1); +} + +} // namespace +} // namespace Config +} // namespace Envoy From 7d78a007ff7593d01eec508605726d1110aff27a Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Mon, 4 Oct 2021 18:19:38 +0200 Subject: [PATCH 43/49] docs: Add a note about xds changes Signed-off-by: Krzesimir Nowak --- docs/root/version_history/current.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index fa70ee694ab6..20be60733503 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -30,6 +30,7 @@ Incompatible Behavior Changes * extensions: deprecated extension names now default to triggering a configuration error. The previous warning-only behavior may be temporarily reverted by setting the runtime key ``envoy.deprecated_features.allow_deprecated_extension_names`` to true. +* xds: ``*`` became a reserved name for a wildcard resource that can be subscribed to and unsubscribed from at any time. This is a requirement for implementing the on-demand xDSes (like on-demand CDS) that can subscribe to specific resources next to their wildcard subscription. If such xDS is subscribed to both wildcard resource and to other specific resource, then in stream reconnection scenario, the xDS will not send an empty initial request, but a request containing ``*`` for wildcard subscription and the rest of the resources the xDS is subscribed to. If the xDS is only subscribed to wildcard resource, it will try to send a legacy wildcard request. This behavior implements the recent changes in :ref:`xDS protocol ` and can be temporarily reverted by setting the ``envoy.restart_features.explicit_wildcard_resource`` runtime guard to false. Minor Behavior Changes ---------------------- From 93ec1877ca282b289e8854c8a61e783a74b0a52d Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 5 Oct 2021 13:15:18 +0200 Subject: [PATCH 44/49] Fix clang tidy Signed-off-by: Krzesimir Nowak --- source/common/config/delta_subscription_state.cc | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 826f28ab9ba3..39429f88b4a5 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -6,10 +6,10 @@ namespace Envoy { namespace Config { namespace { -DeltaSubscriptionStateVariant get_state(std::string type_url, - UntypedConfigUpdateCallbacks& watch_map, - const LocalInfo::LocalInfo& local_info, - Event::Dispatcher& dispatcher) { +DeltaSubscriptionStateVariant getState(std::string type_url, + UntypedConfigUpdateCallbacks& watch_map, + const LocalInfo::LocalInfo& local_info, + Event::Dispatcher& dispatcher) { if (Runtime::runtimeFeatureEnabled("envoy.restart_features.explicit_wildcard_resource")) { return DeltaSubscriptionStateVariant(absl::in_place_type, std::move(type_url), watch_map, local_info, dispatcher); @@ -25,7 +25,7 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, UntypedConfigUpdateCallbacks& watch_map, const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher) - : state_(get_state(std::move(type_url), watch_map, local_info, dispatcher)) {} + : state_(getState(std::move(type_url), watch_map, local_info, dispatcher)) {} void DeltaSubscriptionState::updateSubscriptionInterest( const absl::flat_hash_set& cur_added, From 52a66eef03e073e4d2bc38da2e6b0cb447f41d4b Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 5 Oct 2021 15:08:06 +0200 Subject: [PATCH 45/49] Run ADS integration tests together with old and new DSS Signed-off-by: Krzesimir Nowak --- test/integration/ads_integration.cc | 2 ++ test/integration/ads_integration.h | 37 ++++++++++++++++++++- test/integration/ads_integration_test.cc | 41 ++++++++++++++---------- 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/test/integration/ads_integration.cc b/test/integration/ads_integration.cc index 53716bfa5e7d..2a693dbb86f6 100644 --- a/test/integration/ads_integration.cc +++ b/test/integration/ads_integration.cc @@ -101,6 +101,8 @@ void AdsIntegrationTest::makeSingleRequest() { void AdsIntegrationTest::initialize() { initializeAds(false); } void AdsIntegrationTest::initializeAds(const bool rate_limiting) { + config_helper_.addRuntimeOverride("envoy.restart_features.explicit_wildcard_resource", + oldDssOrNewDss() == OldDssOrNewDss::Old ? "false" : "true"); config_helper_.addConfigModifier([this, &rate_limiting]( envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* ads_config = bootstrap.mutable_dynamic_resources()->mutable_ads_config(); diff --git a/test/integration/ads_integration.h b/test/integration/ads_integration.h index adc0a66ffe02..221d463b4a21 100644 --- a/test/integration/ads_integration.h +++ b/test/integration/ads_integration.h @@ -15,7 +15,34 @@ namespace Envoy { -class AdsIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, public HttpIntegrationTest { +// Support parameterizing over old DSS vs new DSS. Can be dropped when old DSS goes away. +enum class OldDssOrNewDss { Old, New }; + +// Base class that supports parameterizing over old DSS vs new DSS. Can be replaced with +// Grpc::BaseGrpcClientIntegrationParamTest when old DSS is removed. +class AdsDeltaSotwIntegrationSubStateParamTest + : public Grpc::BaseGrpcClientIntegrationParamTest, + public testing::TestWithParam> { +public: + ~AdsDeltaSotwIntegrationSubStateParamTest() override = default; + static std::string protocolTestParamsToString( + const ::testing::TestParamInfo>& p) { + return fmt::format( + "{}_{}_{}_{}", std::get<0>(p.param) == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6", + std::get<1>(p.param) == Grpc::ClientType::GoogleGrpc ? "GoogleGrpc" : "EnvoyGrpc", + std::get<2>(p.param) == Grpc::SotwOrDelta::Delta ? "Delta" : "StateOfTheWorld", + std::get<3>(p.param) == OldDssOrNewDss::Old ? "OldDSS" : "NewDSS"); + } + Network::Address::IpVersion ipVersion() const override { return std::get<0>(GetParam()); } + Grpc::ClientType clientType() const override { return std::get<1>(GetParam()); } + Grpc::SotwOrDelta sotwOrDelta() const { return std::get<2>(GetParam()); } + OldDssOrNewDss oldDssOrNewDss() const { return std::get<3>(GetParam()); } +}; + +class AdsIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, + public HttpIntegrationTest { public: AdsIntegrationTest(); @@ -56,4 +83,12 @@ class AdsIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, public Ht envoy::admin::v3::RoutesConfigDump getRoutesConfigDump(); }; +// When old delta subscription state goes away, we could replace this macro back with +// DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS. +#define ADS_INTEGRATION_PARAMS \ + testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), \ + testing::ValuesIn(TestEnvironment::getsGrpcVersionsForTest()), \ + testing::Values(Grpc::SotwOrDelta::Sotw, Grpc::SotwOrDelta::Delta), \ + testing::Values(OldDssOrNewDss::Old, OldDssOrNewDss::New)) + } // namespace Envoy diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc index f2d0edad3ca8..7914d2cba66c 100644 --- a/test/integration/ads_integration_test.cc +++ b/test/integration/ads_integration_test.cc @@ -26,8 +26,8 @@ using testing::AssertionResult; namespace Envoy { -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, AdsIntegrationTest, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, AdsIntegrationTest, + ADS_INTEGRATION_PARAMS); // Validate basic config delivery and upgrade. TEST_P(AdsIntegrationTest, Basic) { @@ -1044,7 +1044,7 @@ TEST_P(AdsIntegrationTest, RdsAfterLdsInvalidated) { test_server_->waitForCounterGe("listener_manager.listener_create_success", 2); } -class AdsFailIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, +class AdsFailIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, public HttpIntegrationTest { public: AdsFailIntegrationTest() @@ -1059,6 +1059,8 @@ class AdsFailIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, void TearDown() override { cleanUpXdsConnection(); } void initialize() override { + config_helper_.addRuntimeOverride("envoy.restart_features.explicit_wildcard_resource", + oldDssOrNewDss() == OldDssOrNewDss::Old ? "false" : "true"); config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* grpc_service = bootstrap.mutable_dynamic_resources()->mutable_ads_config()->add_grpc_services(); @@ -1072,8 +1074,8 @@ class AdsFailIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, } }; -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, AdsFailIntegrationTest, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, AdsFailIntegrationTest, + ADS_INTEGRATION_PARAMS); // Validate that we don't crash on failed ADS stream. TEST_P(AdsFailIntegrationTest, ConnectDisconnect) { @@ -1084,7 +1086,7 @@ TEST_P(AdsFailIntegrationTest, ConnectDisconnect) { xds_stream_->finishGrpcStream(Grpc::Status::Internal); } -class AdsConfigIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, +class AdsConfigIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, public HttpIntegrationTest { public: AdsConfigIntegrationTest() @@ -1099,6 +1101,8 @@ class AdsConfigIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, void TearDown() override { cleanUpXdsConnection(); } void initialize() override { + config_helper_.addRuntimeOverride("envoy.restart_features.explicit_wildcard_resource", + oldDssOrNewDss() == OldDssOrNewDss::Old ? "false" : "true"); config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* grpc_service = bootstrap.mutable_dynamic_resources()->mutable_ads_config()->add_grpc_services(); @@ -1121,8 +1125,8 @@ class AdsConfigIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, } }; -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, AdsConfigIntegrationTest, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, AdsConfigIntegrationTest, + ADS_INTEGRATION_PARAMS); // This is s regression validating that we don't crash on EDS static Cluster that uses ADS. TEST_P(AdsConfigIntegrationTest, EdsClusterWithAdsConfigSource) { @@ -1269,7 +1273,7 @@ TEST_P(AdsIntegrationTest, SetNodeAlways) { }; // Check if EDS cluster defined in file is loaded before ADS request and used as xDS server -class AdsClusterFromFileIntegrationTest : public Grpc::DeltaSotwIntegrationParamTest, +class AdsClusterFromFileIntegrationTest : public AdsDeltaSotwIntegrationSubStateParamTest, public HttpIntegrationTest { public: AdsClusterFromFileIntegrationTest() @@ -1284,6 +1288,8 @@ class AdsClusterFromFileIntegrationTest : public Grpc::DeltaSotwIntegrationParam void TearDown() override { cleanUpXdsConnection(); } void initialize() override { + config_helper_.addRuntimeOverride("envoy.restart_features.explicit_wildcard_resource", + oldDssOrNewDss() == OldDssOrNewDss::Old ? "false" : "true"); config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { auto* grpc_service = bootstrap.mutable_dynamic_resources()->mutable_ads_config()->add_grpc_services(); @@ -1335,8 +1341,8 @@ class AdsClusterFromFileIntegrationTest : public Grpc::DeltaSotwIntegrationParam } }; -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, AdsClusterFromFileIntegrationTest, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, AdsClusterFromFileIntegrationTest, + ADS_INTEGRATION_PARAMS); // Validate if ADS cluster defined as EDS will be loaded from file and connection with ADS cluster // will be established. @@ -1398,8 +1404,8 @@ class AdsIntegrationTestWithRtds : public AdsIntegrationTest { } }; -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, AdsIntegrationTestWithRtds, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, AdsIntegrationTestWithRtds, + ADS_INTEGRATION_PARAMS); TEST_P(AdsIntegrationTestWithRtds, Basic) { initialize(); @@ -1452,8 +1458,8 @@ class AdsIntegrationTestWithRtdsAndSecondaryClusters : public AdsIntegrationTest } }; -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, AdsIntegrationTestWithRtdsAndSecondaryClusters, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, + AdsIntegrationTestWithRtdsAndSecondaryClusters, ADS_INTEGRATION_PARAMS); TEST_P(AdsIntegrationTestWithRtdsAndSecondaryClusters, Basic) { initialize(); @@ -1544,12 +1550,13 @@ class XdsTpAdsIntegrationTest : public AdsIntegrationTest { }; INSTANTIATE_TEST_SUITE_P( - IpVersionsClientTypeDelta, XdsTpAdsIntegrationTest, + IpVersionsClientTypeDeltaWildcard, XdsTpAdsIntegrationTest, testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), // There should be no variation across clients. testing::Values(Grpc::ClientType::EnvoyGrpc), // Only delta xDS is supported for XdsTp - testing::Values(Grpc::SotwOrDelta::Delta))); + testing::Values(Grpc::SotwOrDelta::Delta), + testing::Values(OldDssOrNewDss::Old, OldDssOrNewDss::New))); TEST_P(XdsTpAdsIntegrationTest, Basic) { initialize(); From 044bceb1c161750c0974b95b0cdcb7fd8861f88e Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 5 Oct 2021 15:19:19 +0200 Subject: [PATCH 46/49] Add DSS to dictionary Signed-off-by: Krzesimir Nowak --- tools/spelling/spelling_dictionary.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index f9d05beace12..ee7106285991 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -31,6 +31,7 @@ CDN CDS CEL DSR +DSS EBADF ENOTCONN EPIPE From 9a34a6b6f6fa1981edc6e87261cdaa35758505b2 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 5 Oct 2021 19:11:53 +0200 Subject: [PATCH 47/49] Fix version history after merge Signed-off-by: Krzesimir Nowak --- docs/root/version_history/current.rst | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 95fc9d99d9b9..eb0147e3bbb9 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -5,31 +5,6 @@ Incompatible Behavior Changes ----------------------------- *Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required* -* config: the ``--bootstrap-version`` CLI flag has been removed, Envoy has only been able to accept v3 - bootstrap configurations since 1.18.0. -* contrib: the :ref:`squash filter ` has been moved to - :ref:`contrib images `. -* contrib: the :ref:`kafka broker filter ` has been moved to - :ref:`contrib images `. -* contrib: the :ref:`RocketMQ proxy filter ` has been moved to - :ref:`contrib images `. -* contrib: the :ref:`Postgres proxy filter ` has been moved to - :ref:`contrib images `. -* contrib: the :ref:`MySQL proxy filter ` has been moved to - :ref:`contrib images `. -* dns_filter: :ref:`dns_filter ` - protobuf fields have been renumbered to restore compatibility with Envoy - 1.18, breaking compatibility with Envoy 1.19.0 and 1.19.1. The new field - numbering allows control planes supporting Envoy 1.18 to gracefully upgrade to - :ref:`dns_resolution_config `, - provided they skip over Envoy 1.19.0 and 1.19.1. - Control planes upgrading from Envoy 1.19.0 and 1.19.1 will need to - vendor the corresponding protobuf definitions to ensure that the - renumbered fields have the types expected by those releases. -* ext_authz: fixed skipping authentication when returning either a direct response or a redirect. This behavior can be temporarily reverted by setting the ``envoy.reloadable_features.http_ext_authz_do_not_skip_direct_response_and_redirect`` runtime guard to false. -* extensions: deprecated extension names now default to triggering a configuration error. - The previous warning-only behavior may be temporarily reverted by setting the runtime key - ``envoy.deprecated_features.allow_deprecated_extension_names`` to true. * xds: ``*`` became a reserved name for a wildcard resource that can be subscribed to and unsubscribed from at any time. This is a requirement for implementing the on-demand xDSes (like on-demand CDS) that can subscribe to specific resources next to their wildcard subscription. If such xDS is subscribed to both wildcard resource and to other specific resource, then in stream reconnection scenario, the xDS will not send an empty initial request, but a request containing ``*`` for wildcard subscription and the rest of the resources the xDS is subscribed to. If the xDS is only subscribed to wildcard resource, it will try to send a legacy wildcard request. This behavior implements the recent changes in :ref:`xDS protocol ` and can be temporarily reverted by setting the ``envoy.restart_features.explicit_wildcard_resource`` runtime guard to false. Minor Behavior Changes From 36b872b41d6b40d5e592a5f9b018232f7a60d22e Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Tue, 19 Oct 2021 10:53:37 +0200 Subject: [PATCH 48/49] test: Fix redis ADS integration test params This can be reverted too, when old DSS goes away. Signed-off-by: Krzesimir Nowak --- .../clusters/redis/redis_cluster_integration_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/extensions/clusters/redis/redis_cluster_integration_test.cc b/test/extensions/clusters/redis/redis_cluster_integration_test.cc index 16eff16c9a69..7a0128d09f00 100644 --- a/test/extensions/clusters/redis/redis_cluster_integration_test.cc +++ b/test/extensions/clusters/redis/redis_cluster_integration_test.cc @@ -670,8 +670,8 @@ TEST_P(RedisAdsIntegrationTest, RedisClusterRemoval) { test_server_->waitForCounterGe("cluster_manager.cluster_removed", 1); } -INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDelta, RedisAdsIntegrationTest, - DELTA_SOTW_GRPC_CLIENT_INTEGRATION_PARAMS); +INSTANTIATE_TEST_SUITE_P(IpVersionsClientTypeDeltaWildcard, RedisAdsIntegrationTest, + ADS_INTEGRATION_PARAMS); } // namespace } // namespace Envoy From 55dfdfacd98f039b40790dabc4d8876e4183f214 Mon Sep 17 00:00:00 2001 From: Krzesimir Nowak Date: Wed, 20 Oct 2021 15:43:32 +0200 Subject: [PATCH 49/49] Shard the redis integration test to avoid timeouts The integration test got twice as much test cases to run, because it runs the previous configuration with either new or old DSS. This resulted in breaking the timeout limit for the test. Signed-off-by: Krzesimir Nowak --- test/extensions/clusters/redis/BUILD | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/extensions/clusters/redis/BUILD b/test/extensions/clusters/redis/BUILD index 2c72cdfcdd99..5481a6ac3a6e 100644 --- a/test/extensions/clusters/redis/BUILD +++ b/test/extensions/clusters/redis/BUILD @@ -93,6 +93,9 @@ envoy_extension_cc_test( size = "small", srcs = ["redis_cluster_integration_test.cc"], extension_names = ["envoy.clusters.redis"], + # This test takes a while to run specially under tsan. + # Shard it to avoid test timeout. + shard_count = 2, deps = [ "//source/extensions/clusters/redis:redis_cluster", "//source/extensions/clusters/redis:redis_cluster_lb",