-
Notifications
You must be signed in to change notification settings - Fork 5.5k
runtime: codifying runtime guarded features #6134
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
Changes from 1 commit
5f368a3
0147b76
7f3fc25
3ac22ff
33bbf5e
ad5d657
5b838e8
26bdbd6
1cc4738
b5cf76c
e063208
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -137,6 +137,56 @@ maximize the chances of your PR being merged. | |
| [envoy-filter-example](https://github.com/envoyproxy/envoy-filter-example) (for example making a new | ||
| branch so that CI can pass) it is your responsibility to follow through with merging those | ||
| changes back to master once the CI dance is done. | ||
| * If your PR is a high risk change, the reviewer may ask that you runtime guard | ||
| it. See the section on runtime guarding below. | ||
|
|
||
|
|
||
| # Runtime guarding | ||
|
|
||
| Some high risk changes in Envoy are deemed worthy of runtime guarding. Instead of just replacing | ||
| old code with new code, both code paths are supported for between one Envoy release (if it is | ||
| guarded due to performance concerns) and a full deprecation cycle (if it is a high risk behavioral | ||
| change). | ||
|
|
||
| The canonican way to runtime guard a feature is | ||
| ``` | ||
| if (Runtime::LoaderSingleton::getExisting() && | ||
| Runtime::LoaderSingleton::getExisting()->snapshot()->runtimeFeatureEnabled(" | ||
| envoy.reloadable_features.my_feature_name")) { | ||
| [new code path] | ||
| } else { | ||
| [old_code_path] | ||
| } | ||
| ``` | ||
| Runtime guarded features named with the "envoy.reloadable_features." prefix must be safe to flip | ||
| true or false on running Envoy instances. In some situations, for example the buffer rewrite in | ||
| [#5441](https://github.com/envoyproxy/envoy/pull/5441), it may make more sense to | ||
| latch the value in a member variable on class creation, for example: | ||
|
|
||
| ``` | ||
| bool use_new_code_path_ = Runtime::LoaderSingleton::getExisting() && | ||
| Runtime::LoaderSingleton::getExisting()->snapshot()->runtimeFeatureEnabled( | ||
| "envoy.reloadable_features.my_feature_name")); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth trying to make this a bit less verbose at the sites where it is used? There's a lot of boilerplate to get to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah we could likely have a wrapper that internally loads the singleton and does the right thing if it doesn't exist.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Definitely the right way to go. Less clear, should we use this in tests? I was a bit bummed at the heavyweight work in TestEnvironment to be able to flag override - setting up all those fake components. With the helper if we want to have a friend test-only class in ConstSingleton we can skip all the static state and just muck with the const singleton directly. I think that might also work for integration tests, which would be pretty awesome. That said, it's making our const-singleton non-const and I know how much the larger Envoy community loves hacks like that ;-)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about an approach like #6139 with some test class mixin that changes the singleton behavior? That seems like a really cleanly structured way to have a "mostly const" singleton that alters behavior only within a particular test. |
||
| ``` | ||
|
|
||
| Runtime guarded features may either set true (running the new code by default) in the initial PR, | ||
| after a testing interval, or during the next release cycle, at the PR author's and reviewing | ||
| maintainer's discretion. Generally all runtime guarded features will be set true when a | ||
| release is cut, and the old code path will be deprecated at that time. Runtime features | ||
| are set true by default by inclusion in | ||
| [source/common/runtime/runtime_features.h](https://github.com/envoyproxy/envoy/blob/master/source/common/runtime/runtime_features.h) | ||
|
|
||
| There are three options for testing new runtime features: | ||
|
|
||
| 1. Create a per-test Runtime::LoaderSingleton as done in [DeprecatedFieldsTest.IndividualFieldDisallowedWithRuntimeOverride](https://github.com/envoyproxy/envoy/blob/master/test/common/protobuf/utility_test.cc) | ||
| 2. Set up integration tests with custom runtime defaults as documented in the | ||
| [integration test README](https://github.com/envoyproxy/envoy/blob/master/test/integration/README.md) | ||
| 3. Run a given unit test with the new runtime value explicitly set true as done | ||
| for [runtime_flag_override_test](https://github.com/envoyproxy/envoy/blob/master/test/common/runtime/BUILD) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it also worth specifically calling out parameterized tests as a useful tool here? I find that particularly useful when the logical behavior is supposed to be the same regardless of flag value (ie, goal is pure refactoring or performance improvement). |
||
|
|
||
| Runtime code is held to the same standard as regular Envoy code, so both the old | ||
| path and the new should have 100% coverage both with the feature defaulting true | ||
| and false. | ||
|
|
||
| # PR review policy for maintainers | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,6 +83,13 @@ class Snapshot { | |
| // configuration of "false" in runtime config. | ||
| virtual bool deprecatedFeatureEnabled(const std::string& key) const PURE; | ||
|
|
||
| // Returns true if a runtime feature is enabled. | ||
| // | ||
| // Runtime features are used to easily allow switching between old and new code paths for high | ||
| // risk changes. The intent is for the old code path to be short lived - the old code path is | ||
| // deprecated as the feature is defaulted true, and removed with the following Envoy release. | ||
| virtual bool runtimeFeatureEnabled(const std::string& key) const PURE; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As written I think someone could If they do the inefficient If I switch to string_view I think the map lookup will convert the string piece to the string on every single call, and there's no way to avoid it, so I think that's strictly less performant? I could update the instructions to encourage static string on the canonical non-latch path if we think that makes more sense
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As long as it is an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TIL! Ok, got that to work for the flat hash, but unfortunately to string_view all the way up the API we need to also change Runtime::EntryMap in include/envoy/runtime/runtime.h to have a comparator. I'd prefer to land this as-is and do a separate PR converting to string piece, especially as none of the call sites will need to change. What do you think of a TODO and a follow-up?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah fine to leave as TODO.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds fine to me. |
||
|
|
||
| /** | ||
| * Test if a feature is enabled using the built in random generator. This is done by generating | ||
| * a random number in the range 0-99 and seeing if this number is < the value stored in the | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -152,7 +152,7 @@ bool SnapshotImpl::deprecatedFeatureEnabled(const std::string& key) const { | |
| bool stored = getBoolean(key, allowed); | ||
| // If not, the default value is based on disallowedByDefault. | ||
| if (!stored) { | ||
| allowed = !DisallowedFeaturesDefaults::get().disallowedByDefault(key); | ||
| allowed = !RuntimeFeaturesDefaults::get().disallowedByDefault(key); | ||
| } | ||
|
|
||
| if (!allowed) { | ||
|
|
@@ -165,6 +165,18 @@ bool SnapshotImpl::deprecatedFeatureEnabled(const std::string& key) const { | |
| return true; | ||
| } | ||
|
|
||
| bool SnapshotImpl::runtimeFeatureEnabled(const std::string& key) const { | ||
| bool enabled = false; | ||
| // See if this value is explicitly set as a runtime boolean. | ||
| bool stored = getBoolean(key, enabled); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this can just be inlined in the if statement below? |
||
| // If not, the default value is based on runtime_features. | ||
| if (!stored) { | ||
| enabled = RuntimeFeaturesDefaults::get().enabledByDefault(key); | ||
| } | ||
|
|
||
| return enabled; | ||
| } | ||
|
|
||
| bool SnapshotImpl::featureEnabled(const std::string& key, uint64_t default_value, | ||
| uint64_t random_value, uint64_t num_buckets) const { | ||
| return random_value % num_buckets < std::min(getInteger(key, default_value), num_buckets); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,18 @@ envoy_cc_test( | |
| ], | ||
| ) | ||
|
|
||
| envoy_cc_test( | ||
| name = "runtime_flag_override_test", | ||
| srcs = ["runtime_flag_override_test.cc"], | ||
| args = [ | ||
| "--runtime-feature-override-for-tests=envoy.reloadable_features.test_feature_false", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, this seems like a generally useful feature for runtime configuration where people don't want to use the filesystem (CLI/gflags implementation like we discussed). Is it worth adding a TODO or opening a help wanted issue on that?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, if possible can you rename
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think other folks would use this? I thought from prior discussions you thought google was flag happy but most folks wanted to use the existing runtime. Not sure if removing test_feature_false helps - I've clarified the language in both tests to hopefully clarify things.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do think that Google is an outlier in using flags for this type of stuff, but I was just suggesting that this code might be generally useful to someone, so probably worth tracking somewhere in case it comes up again. I think it's totally fine to leave as test only code for now. Thanks for all the comments, it's much easier to understand for me now. |
||
| ], | ||
| coverage = False, | ||
| deps = [ | ||
| "//source/common/runtime:runtime_lib", | ||
| ], | ||
| ) | ||
|
|
||
| envoy_cc_test( | ||
| name = "uuid_util_test", | ||
| srcs = ["uuid_util_test.cc"], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| #include "common/runtime/runtime_impl.h" | ||
|
|
||
| #include "gmock/gmock.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Runtime { | ||
|
|
||
| // In the envoy_cc_test declaration, the flag is set | ||
| // "--runtime-feature-override-for-tests=envoy.reloadable_features.test_feature_false" | ||
| TEST(RuntimeFlagOverrideTest, OverridesWork) { | ||
| Snapshot& snapshot = Runtime::LoaderSingleton::getExisting()->snapshot(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| EXPECT_EQ(true, snapshot.runtimeFeatureEnabled("envoy.reloadable_features.test_feature_false")); | ||
| } | ||
|
|
||
| } // namespace Runtime | ||
| } // namespace Envoy | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,19 +20,71 @@ | |
| #include "common/common/compiler_requirements.h" | ||
| #include "common/common/logger.h" | ||
| #include "common/common/macros.h" | ||
| #include "common/runtime/runtime_impl.h" | ||
| #include "common/common/utility.h" | ||
|
|
||
| #include "server/options_impl.h" | ||
|
|
||
| #include "test/mocks/server/mocks.h" | ||
| #include "test/test_common/network_utility.h" | ||
|
|
||
| #include "absl/strings/match.h" | ||
| #include "test/test_common/test_base.h" | ||
| #include "spdlog/spdlog.h" | ||
|
|
||
| #include "tclap/CmdLine.h" | ||
|
|
||
| #include "gtest/gtest.h" | ||
| namespace Envoy { | ||
| namespace { | ||
|
|
||
| std::string findAndRemove(const std::regex& pattern, int& argc, char**& argv) { | ||
| std::smatch matched; | ||
| std::string return_value; | ||
| for (int i = 0; i < argc; ++i) { | ||
| if (return_value.empty()) { | ||
| std::string argument = std::string(argv[i]); | ||
| if (regex_search(argument, matched, pattern)) { | ||
| return_value = matched[1]; | ||
| argc--; | ||
| } | ||
| } | ||
| if (!return_value.empty() && i < argc) { | ||
| argv[i] = argv[i + 1]; | ||
| } | ||
| } | ||
| return return_value; | ||
| } | ||
|
|
||
| // This class is created iff a test is run with the special runtime override flag. | ||
| class RuntimeManagingListener : public ::testing::EmptyTestEventListener { | ||
| public: | ||
| RuntimeManagingListener(std::string& runtime_override) : runtime_override_(runtime_override) {} | ||
|
|
||
| // On each test start, create and register a runtime instance, with this specific feature set to | ||
| // true. | ||
| void OnTestStart(const ::testing::TestInfo&) override { | ||
| if (!runtime_override_.empty()) { | ||
| runtime_state_ = std::make_unique<RuntimeState>(); | ||
| Runtime::LoaderSingleton::getExisting()->mergeValues({{runtime_override_, "true"}}); | ||
| } | ||
| } | ||
|
|
||
| // As each test ends, clean up the singleton state. | ||
| void OnTestEnd(const ::testing::TestInfo&) override { runtime_state_.reset(); } | ||
|
|
||
| struct RuntimeState { | ||
| NiceMock<ThreadLocal::MockInstance> tls; | ||
| Stats::IsolatedStoreImpl store; | ||
| Runtime::MockRandomGenerator rand; | ||
| Runtime::ScopedLoaderSingleton loader{ | ||
| Runtime::LoaderPtr{new Runtime::LoaderImpl(rand, store, tls)}}; | ||
| }; | ||
|
|
||
| std::unique_ptr<RuntimeState> runtime_state_; | ||
| std::string runtime_override_; | ||
| }; | ||
|
|
||
| std::string makeTempDir(char* name_template) { | ||
| #ifdef WIN32 | ||
| char* dirname = ::_mktemp(name_template); | ||
|
|
@@ -129,6 +181,20 @@ std::string TestEnvironment::getCheckedEnvVar(const std::string& var) { | |
| } | ||
|
|
||
| void TestEnvironment::initializeOptions(int argc, char** argv) { | ||
| // Before latching argv and argc, remove any runtime override flag. | ||
| // This allows doing test overrides of Envoy runtime features without adding | ||
| // test flags to the Envoy production command line. | ||
| const std::regex PATTERN{"--runtime-feature-override-for-tests=(.*)", std::regex::optimize}; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a probably stupid question: Is there any reason we couldn't just hit this directly from a test given that we already have static support and also have some work that @jmarantz did to clean things up at the end of tests? I'm just wondering if we could avoid some of the regex/listener/etc. magic here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, now that I did the const-singleton refactor this is much less tied to environment.cc than it was. I'd be inclined to forklift all this code over to TestRunner::RunTests - we still need the regex magic and I'd prefer a separate listener just to avoid the set up and teardown checks for the vast majority of tests which don't need them. I'm going to hold off until we've decided what to do with command line flags - if we want to make it general purpose I'd be inclined to leave it here with the TODO, and the move it into the main server TCLAP in the follow-up.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think my feeling is to not making it general purpose for now, and just track somewhere in a TODO/issue/etc. that we might want to think about this in the future. So in that case, I guess I would opt for making the test code as simple as possible so maybe do the move you suggested? Though if we do a move, why do we need all the regex stuff? Couldn't we just hit a static method with a string to alter runtime state for a test?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, we can do this statically from test, but in-house when introducing a high risk flag we often just dup a full test (or tests) with no edits, so we have one variant running with true and the other running with the flag false. We've found it pretty handy especially for tests which are already parameterized so hard to muck with. It's not a lot of code so I'd be inclined to check it in and we can always remove it if no one uses it and it turns out to be overkill. WDYT?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have a strong opinion on this one way or the other, but couldn't we accomplish roughly the same thing with a TEST_P and some type of param helper that just toggles a specific string? I think @dnoe mentioned this? I'm mainly just wondering if the same behavior can be achieved with less code?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can, but if a test is already a TEST_P it can mean updating all the test params from GetParam() to GetParam(0), adding GetParam(1) for every test fixture in the whole class, creating or editing the constructur etc, then tearing it all back down in a few months. Given many of our files have 3-5 fixtures this can end up being a lot of throwaway work per test you want to dual-dun, which is why we bother with the flag based solution internally.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK SGTM if you think this is the best option.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again it's really handy to have, but TDB if folks use it. Any other comments you're waiting on? |
||
| std::string runtime_override = findAndRemove(PATTERN, argc, argv); | ||
| if (!runtime_override.empty()) { | ||
| ENVOY_LOG_TO_LOGGER(Logger::Registry::getLog(Logger::Id::testing), info, | ||
| "Running with runtime feature override {}", runtime_override); | ||
| // Set up a listener which will create a global runtime and set the feature | ||
| // to true for the duration of each test instance. | ||
| ::testing::TestEventListeners& listeners = ::testing::UnitTest::GetInstance()->listeners(); | ||
| listeners.Append(new RuntimeManagingListener(runtime_override)); | ||
| } | ||
|
|
||
| argc_ = argc; | ||
| argv_ = argv; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless canonicans are some sci-fi aliens that always speak canonically, I assume this is typo :)