Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: Scoped RDS (PR 1/4): Introduce {static,dynamic} config provider framework #5243

Merged
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6c744cd
Refactor RouteConfigProvider{,Manager} into a generalized framework.
AndresGuedez Nov 9, 2018
1bccfaf
Add comments.
AndresGuedez Dec 7, 2018
66e4c45
Minor cleanup.
AndresGuedez Dec 7, 2018
02b5e97
More comments.
AndresGuedez Dec 7, 2018
09a0846
Comments.
AndresGuedez Dec 7, 2018
acf19cf
Minor const correctness fixes.
AndresGuedez Dec 13, 2018
c0366a7
Add test.
AndresGuedez Dec 13, 2018
4b1bef4
Comments, general cleanup and renaming.
AndresGuedez Dec 14, 2018
550723c
Minor cleanup.
AndresGuedez Dec 14, 2018
2c79e9c
Cleanup and comments based on reviewer feedback.
AndresGuedez Dec 17, 2018
854c2f2
Merge remote-tracking branch 'upstream/master' into scoped-rds-config…
AndresGuedez Dec 19, 2018
e67b5ee
Rename *Impl -> *ImplBase.
AndresGuedez Dec 19, 2018
821aab9
Cleanup based on review feedback.
AndresGuedez Dec 19, 2018
ca583a0
More comments and cleanup.
AndresGuedez Dec 19, 2018
15c9d19
Comments and cleanup based on reviewer comments.
AndresGuedez Dec 20, 2018
f69eb74
Header include order fix.
AndresGuedez Dec 20, 2018
ba44ad9
Remove superfluous constructor deletes.
AndresGuedez Dec 20, 2018
142d9c1
Rename ({Static,Dynamic} -> {Immutable,Mutable})ConfigProviderImplBase.
AndresGuedez Dec 20, 2018
5c71955
Merge remote-tracking branch 'upstream/master' into scoped-rds-config…
AndresGuedez Dec 21, 2018
7441143
Resolve build issue with GCC 5.X.
AndresGuedez Dec 21, 2018
a36a3e8
clang-tidy cleanup.
AndresGuedez Dec 21, 2018
bb34fdd
Remove unused function.
AndresGuedez Dec 21, 2018
ccc244a
Merge remote-tracking branch 'upstream/master' into scoped-rds-config…
AndresGuedez Dec 21, 2018
6d7cba8
Add comments.
AndresGuedez Dec 27, 2018
9cf7f27
clang-tidy cleanup.
AndresGuedez Dec 27, 2018
58ac876
Comments.
AndresGuedez Dec 28, 2018
bb990fa
Cleanup.
AndresGuedez Dec 31, 2018
b49127f
Minor cleanup.
AndresGuedez Jan 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions include/envoy/config/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,23 @@ envoy_cc_library(
"//source/common/protobuf",
],
)

envoy_cc_library(
name = "config_provider_interface",
hdrs = ["config_provider.h"],
external_deps = ["abseil_optional"],
deps = [
"//include/envoy/common:time_interface",
"//source/common/protobuf",
],
)

envoy_cc_library(
name = "config_provider_manager_interface",
hdrs = ["config_provider_manager.h"],
deps = [
":config_provider_interface",
"//include/envoy/server:filter_config_interface",
"//source/common/protobuf",
],
)
103 changes: 103 additions & 0 deletions include/envoy/config/config_provider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#pragma once

#include <memory>

#include "envoy/common/time.h"

#include "common/protobuf/protobuf.h"

#include "absl/types/optional.h"

namespace Envoy {
namespace Config {

/**
* A provider for configuration obtained ether statically or dynamically via xDS APIs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One high level question to kick this off with. Is the plan to also replace the static/dynamic treatment of SDS resoureces (e.g. see https://github.com/envoyproxy/envoy/blob/master/source/common/ssl/context_config_impl.cc#L57) with this class framework? It would be helpful to have in mind the concrete types that this will be instantiated with and places used when reading.

Copy link
Contributor Author

@AndresGuedez AndresGuedez Dec 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, SDS looks like a good candidate as well, although I was not planning to transition it to the new API as part of this PR chain so there are likely a few gaps that will need to be addressed.

One of those gaps is that the ConfigProvider currently only exposes a std::shared_ptr<const Config> config() const function, but the SDS config impls require allowing the user/consumer of the API to set a callback (see ContextConfigImpl::setSecretUpdateCallback). From an API standpoint, I think the solution is trivial by exposing a std::shared_ptr<Config> config() overload, and having the config impl enforce thread safety itself by leveraging any synchronization mechanisms required.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be ideal from a tech debt perspective if we could at least file an issue for this if not do the conversion at the end of the PR chain. There is a lot of commonality here between RDS and SDS, having multiple methods of managing static/dynamic resources seems redundant. Anyway, I'll do a deep dive now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I'll file an issue to keep track of SDS. I plan to take a deeper look at it once I get through these PRs and will likely take on the work myself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue filed: #5329.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I might be being dense here. However, this discussion and @lizan's comment here leave me with a few questions:

  1. The advantage of this framework is that it simplifies managing config providers that provide configuration that have shared semantics. In the PR comment this is clear, but I think it would be clarifying to spell that out in the interface here.
  2. Given the fact that listeners, and clusters have similar (between them), but different (w.r.t SDS/RDS) ownership models, I wonder if it is worth it to spell out the fact that this framework, as is, is not really intended for config objects that are not shared. Or is the plan long term to use this framework to manage "singly owned" config providers?
  3. About @lizan's comment. Should we put some effort in unifying what we mean when we say static vs inline? And how is the framework going to deal with the third origin model in SDS (what they call static)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, more emphasis on the shared aspect in this comment would make sense; a short sentence or two explaining where this fits into the taxonomy of configuration management in Envoy would be rad.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answering @junr03 's questions below:

  1. The advantage of this framework is that it simplifies managing config providers that provide configuration that have shared semantics. In the PR comment this is clear, but I think it would be clarifying to spell that out in the interface here.

Done.

  1. Given the fact that listeners, and clusters have similar (between them), but different (w.r.t SDS/RDS) ownership models, I wonder if it is worth it to spell out the fact that this framework, as is, is not really intended for config objects that are not shared. Or is the plan long term to use this framework to manage "singly owned" config providers?

This framework can also be leveraged for use cases where there is no expected sharing of dynamic resources, since that use case is a simplification of the target use case and the publicly exposed ConfigProvider API should suffice as well. However, I have avoided introducing this framework as the solution for config management, since it's likely that some of these "singly owned" use cases could make due with simpler implementations than what is imposed by this framework.

  1. About @lizan's comment. Should we put some effort in unifying what we mean when we say static vs inline? And how is the framework going to deal with the third origin model in SDS (what they call static)?

I have changed the naming of the *ImplBase classes and added comments to clarify how this framework can be leveraged for SDS as well and the static/inline distinction that it makes. Essentially, the ImmutableConfigProviderImplBase can satisfy both config types, and I've introduced a ConfigProviderInstanceType parameter to the constructor to distinguish between them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, more emphasis on the shared aspect in this comment would make sense; a short sentence or two explaining where this fits into the taxonomy of configuration management in Envoy would be rad.

Done.

*
* Use config() to obtain a shared_ptr to the implementation of the config, and configProtoInfo() to
* obtain a reference to the underlying config proto and version (applicable only to dynamic config
* providers).
*/
class ConfigProvider {
public:
/**
* The "implementation" of the configuration.
* Use config() to obtain a typed object that corresponds to the specific configuration
* represented by this abstract type.
*/
class Config {
public:
virtual ~Config() = default;
};
using ConfigConstSharedPtr = std::shared_ptr<const Config>;

/**
* Stores the config proto as well as the associated version.
*/
template <typename P> struct ConfigProtoInfo {
const P& config_proto_;

// Only populated by dynamic config providers.
std::string version_;
};

virtual ~ConfigProvider() = default;

/**
* Returns a ConfigProtoInfo associated with the provider.
* @return absl::optional<ConfigProtoInfo<P>> an optional ConfigProtoInfo; the value is set when a
* config is available.
*/
template <typename P> absl::optional<ConfigProtoInfo<P>> configProtoInfo() const {
static_assert(std::is_base_of<Protobuf::Message, P>::value,
"Proto type must derive from Protobuf::Message");

const auto* config_proto = dynamic_cast<const P*>(getConfigProto());
if (config_proto == nullptr) {
return {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: absl::nullopt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
return ConfigProtoInfo<P>{*config_proto, getConfigVersion()};
}

/**
* Returns the Config corresponding to the provider.
* @return std::shared_ptr<const C> a shared pointer to the Config.
*/
template <typename C> std::shared_ptr<const C> config() const {
static_assert(std::is_base_of<Config, C>::value,
"Config type must derive from ConfigProvider::Config");

return std::dynamic_pointer_cast<const C>(getConfig());
}

/**
* Returns the timestamp associated with the last update to the Config.
* @return SystemTime the timestamp corresponding to the last config update.
*/
virtual SystemTime lastUpdated() const PURE;

protected:
/**
* Returns the config proto associated with the provider.
* @return Protobuf::Message* the config proto corresponding to the Config instantiated by the
* provider.
*/
virtual const Protobuf::Message* getConfigProto() const PURE;

/**
* Returns the config version associated with the provider.
* @return std::string the config version.
*/
virtual std::string getConfigVersion() const PURE;

/**
* Returns the config implementation associated with the provider.
* @return ConfigConstSharedPtr the config as the base type.
*/
virtual ConfigConstSharedPtr getConfig() const PURE;
};

using ConfigProviderPtr = std::unique_ptr<ConfigProvider>;

} // namespace Config
} // namespace Envoy
58 changes: 58 additions & 0 deletions include/envoy/config/config_provider_manager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#pragma once

#include <string>

#include "envoy/config/config_provider.h"
#include "envoy/server/filter_config.h"

#include "common/protobuf/protobuf.h"

namespace Envoy {
namespace Config {

/**
* A ConfigProvider manager which instantiates static and dynamic (xDS) providers.
*
* ConfigProvider objects are owned by the caller of the
* createXdsConfigProvider()/createStaticConfigProvider() functions. The ConfigProviderManager holds
* raw pointers to those objects.
*
* Configuration implementations returned by ConfigProvider::config() are immutable, which allows
* them to share the underlying objects such as config protos and subscriptions (for dynamic
* providers) without synchronization related performance penalties. This enables linear memory
* growth based on the size of the configuration set, regardless of the number of threads/objects
* that must hold a reference/pointer to them.
*/
class ConfigProviderManager {
public:
virtual ~ConfigProviderManager() = default;

/**
* Returns a dynamic ConfigProvider which receives configuration via an xDS API.
* A shared ownership model is used, such that the underlying subscription, config proto
* and Config are shared amongst all providers relying on the same config source.
* @param config_source_proto supplies the proto containing the xDS API configuration.
* @param factory_context is the context to use for the provider.
* @param stat_prefix supplies the prefix to use for statistics.
* @return ConfigProviderPtr a newly allocated dynamic config provider which shares underlying
* data structures with other dynamic providers configured with the same
* API source.
*/
virtual ConfigProviderPtr
createXdsConfigProvider(const Protobuf::Message& config_source_proto,
Server::Configuration::FactoryContext& factory_context,
const std::string& stat_prefix) PURE;

/**
* Returns a ConfigProvider associated with a statically specified configuration.
* @param config_proto supplies the configuration proto.
* @param factory_context is the context to use for the provider.
* @return ConfigProviderPtr a newly allocated static config provider.
*/
virtual ConfigProviderPtr
createStaticConfigProvider(const Protobuf::Message& config_proto,
Server::Configuration::FactoryContext& factory_context) PURE;
};

} // namespace Config
} // namespace Envoy
22 changes: 22 additions & 0 deletions source/common/common/assert.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ namespace Envoy {
} while (false)
#endif

#ifndef NDEBUG
#if __clang__
#define __CLANG_PRAGMA(X) _Pragma(X)
#else
#define __CLANG_PRAGMA(X)
#endif

// clang-format off

#define ASSERT_IGNORE_POTENTIALLY_EVALUATED(COND) \
do { \
__CLANG_PRAGMA("clang diagnostic push") \
__CLANG_PRAGMA("clang diagnostic ignored \"-Wpotentially-evaluated-expression\"") \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Searching around for what this does, it seems to involve side-effects, which I don't see at the sites where it is used. Can you add some comments on when you need to use this?

Alternatively, can we just fold these pragmas into ASSERT? Fewer knobs here seems better, we could potentially have an unbounded number of these ASSERTs, one for each clang warning :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment in https://github.com/envoyproxy/envoy/pull/5243/files#diff-0bbc1b7ef8d5cfd41d41b2296caf7f1fR65 attempts to address the reason for this assert: the typeid(**mutable_config_providers_.begin()) statement triggers the potentially evaluated warning in clang.

This seems like a useful enough warning that I would rather not hide it by default (by folding it into the ASSERT() macro), but if others feel strongly about this I am certainly open to doing so.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I guess I'm still not a huge fan of adding compiler specific ASSERTs into the implementation here. @envoyproxy/maintainers WDYT?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, if we do assert like this (evaluate the expression outside of typeid) then we don't need the pragma at all, since it is no longer potentially evaluated expression, do you think this is cleaner?

  ASSERT([&]() {
    if (!mutable_config_providers_.empty()) {
      const auto& first_provider = **mutable_config_providers_.begin();
      return typeid(*provider) == typeid(first_provider);
    }
    return true;
  }());

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good solution; I definitely think it's cleaner overall even though it makes the ASSERT() slightly harder to parse. I just pushed this change and removed my changes to assert.h. Thanks!

ASSERT(COND); \
__CLANG_PRAGMA("clang diagnostic pop") \
} while (0)
#else // NDEBUG
#define ASSERT_IGNORE_POTENTIALLY_EVALUATED(COND)
#endif

// clang-format on

/**
* Indicate a panic situation and exit.
*/
Expand Down
11 changes: 11 additions & 0 deletions source/common/common/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,17 @@ struct StringViewHash {
std::size_t operator()(const absl::string_view& k) const { return HashUtil::xxHash64(k); }
};

/**
* Hashing functor for use with enum class types.
* This is needed for GCC 5.X; newer versions of GCC, as well as clang7, provide native hashing
* specializations.
*/
struct EnumClassHash {
template <typename T> std::size_t operator()(T t) const {
return std::hash<std::size_t>()(static_cast<std::size_t>(t));
}
};

/**
* Computes running standard-deviation using Welford's algorithm:
* https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Online_algorithm
Expand Down
17 changes: 17 additions & 0 deletions source/common/config/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,20 @@ envoy_cc_library(
"//source/common/singleton:const_singleton",
],
)

envoy_cc_library(
name = "config_provider_lib",
srcs = ["config_provider_impl.cc"],
hdrs = ["config_provider_impl.h"],
deps = [
":utility_lib",
"//include/envoy/config:config_provider_interface",
"//include/envoy/config:config_provider_manager_interface",
"//include/envoy/init:init_interface",
"//include/envoy/server:admin_interface",
"//include/envoy/server:config_tracker_interface",
"//include/envoy/singleton:instance_interface",
"//include/envoy/thread_local:thread_local_interface",
"//source/common/protobuf",
],
)
116 changes: 116 additions & 0 deletions source/common/config/config_provider_impl.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#include "common/config/config_provider_impl.h"

namespace Envoy {
namespace Config {

ImmutableConfigProviderImplBase::ImmutableConfigProviderImplBase(
Server::Configuration::FactoryContext& factory_context,
ConfigProviderManagerImplBase& config_provider_manager, ConfigProviderInstanceType type)
: last_updated_(factory_context.timeSource().systemTime()),
config_provider_manager_(config_provider_manager), type_(type) {
config_provider_manager_.bindImmutableConfigProvider(this);
}

ImmutableConfigProviderImplBase::~ImmutableConfigProviderImplBase() {
config_provider_manager_.unbindImmutableConfigProvider(this);
}

ConfigSubscriptionInstanceBase::~ConfigSubscriptionInstanceBase() {
runInitializeCallbackIfAny();
config_provider_manager_.unbindSubscription(manager_identifier_);
}

void ConfigSubscriptionInstanceBase::runInitializeCallbackIfAny() {
if (initialize_callback_) {
initialize_callback_();
initialize_callback_ = nullptr;
}
}

bool ConfigSubscriptionInstanceBase::checkAndApplyConfig(const Protobuf::Message& config_proto,
const std::string& config_name,
const std::string& version_info) {
const uint64_t new_hash = MessageUtil::hash(config_proto);
if (config_info_ && config_info_.value().last_config_hash_ == new_hash) {
return false;
}

config_info_ = {new_hash, version_info};
ENVOY_LOG(debug, "{}: loading new configuration: config_name={} hash={}", name_, config_name,
new_hash);

ASSERT(!mutable_config_providers_.empty());
ConfigProvider::ConfigConstSharedPtr new_config;
for (auto* provider : mutable_config_providers_) {
// All bound mutable config providers must be of the same type (see the ASSERT... in
// bindConfigProvider()).
// This makes it safe to call any of the provider's onConfigProtoUpdate() to get a new config
// impl, which can then be passed to all providers.
if (new_config == nullptr) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loop looks wrong based on a quick glance; the new_config nullness gets shared across dynamic providers... unless onConfigProtoUpdate is identical across them all? If so, please add comments and maybe ASSERTs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dynamic providers are all instances of the same class, and they share the underlying ConfigSubscriptionInstance on which this function is taking effect.

I will add the comments and assert()s.

Copy link
Contributor Author

@AndresGuedez AndresGuedez Dec 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments and an ASSERT to bindConfigProvider(). The assertion is tricky due to the lack of concrete type information in these functions, so PTAL and let me know your thoughts on my approach using RTTI.

if ((new_config = provider->onConfigProtoUpdate(config_proto)) == nullptr) {
return false;
}
}
provider->onConfigUpdate(new_config);
}

return true;
}

void ConfigSubscriptionInstanceBase::bindConfigProvider(MutableConfigProviderImplBase* provider) {
// All config providers bound to a ConfigSubscriptionInstanceBase must be of the same concrete
// type; this is assumed by checkAndApplyConfig() and is verified by the assertion below. NOTE: a
// regular ASSERT() triggers a potentially evaluated expression warning from clang due to the args
// passed to the second typeid() call.
ASSERT_IGNORE_POTENTIALLY_EVALUATED(mutable_config_providers_.empty() ||
typeid(*provider) ==
typeid(**mutable_config_providers_.begin()));
mutable_config_providers_.insert(provider);
}

ConfigProviderManagerImplBase::ConfigProviderManagerImplBase(Server::Admin& admin,
const std::string& config_name) {
config_tracker_entry_ =
admin.getConfigTracker().add(config_name, [this] { return dumpConfigs(); });
// ConfigTracker keys must be unique. We are asserting that no one has stolen the key
// from us, since the returned entry will be nullptr if the key already exists.
RELEASE_ASSERT(config_tracker_entry_, "");
}

const ConfigProviderManagerImplBase::ConfigProviderSet&
ConfigProviderManagerImplBase::immutableConfigProviders(ConfigProviderInstanceType type) const {
static ConfigProviderSet empty_set;
ConfigProviderMap::const_iterator it;
if ((it = immutable_config_providers_map_.find(type)) == immutable_config_providers_map_.end()) {
return empty_set;
}

return *it->second;
}

void ConfigProviderManagerImplBase::bindImmutableConfigProvider(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, the new naming scheme is also a bit confusing, since the config object that is provided is always immutable, but the fact that a new config can be swapped in is being assigned the mutability characteristic. IDK if it's worth bike shedding further on this, but just raising this as a potential reader confusion.

Copy link
Contributor Author

@AndresGuedez AndresGuedez Dec 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added to the file level comment more details regarding the mutability property and how it applies in this context.

I am open to considering other names but this is the best I have come up with given the constraints pointed out by @lizan related to naming conventions already in place for the concrete classes.

ImmutableConfigProviderImplBase* provider) {
ASSERT(provider->type() == ConfigProviderInstanceType::Static ||
provider->type() == ConfigProviderInstanceType::Inline);
ConfigProviderMap::iterator it;
if ((it = immutable_config_providers_map_.find(provider->type())) ==
immutable_config_providers_map_.end()) {
immutable_config_providers_map_.insert(std::make_pair(
provider->type(),
std::make_unique<ConfigProviderSet>(std::initializer_list<ConfigProvider*>({provider}))));
} else {
it->second->insert(provider);
}
}

void ConfigProviderManagerImplBase::unbindImmutableConfigProvider(
ImmutableConfigProviderImplBase* provider) {
ASSERT(provider->type() == ConfigProviderInstanceType::Static ||
provider->type() == ConfigProviderInstanceType::Inline);
auto it = immutable_config_providers_map_.find(provider->type());
ASSERT(it != immutable_config_providers_map_.end());
it->second->erase(provider);
}

} // namespace Config
} // namespace Envoy
Loading