Skip to content
Merged
1 change: 1 addition & 0 deletions source/common/runtime/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ envoy_cc_library(
# AVOID ADDING TO THESE DEPENDENCIES IF POSSIBLE
# Any code using runtime guards depends on this library, and the more dependencies there are,
# the harder it is to runtime-guard without dependency loops.
"@com_google_absl//absl/flags:commandlineflag",
"@com_google_absl//absl/flags:flag",
"//envoy/runtime:runtime_interface",
"//source/common/common:hash_lib",
Expand Down
187 changes: 70 additions & 117 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
#include "source/common/runtime/runtime_features.h"

#include "absl/flags/commandlineflag.h"
#include "absl/flags/flag.h"
#include "absl/strings/match.h"
#include "absl/strings/str_replace.h"

/* To add a runtime guard to Envoy, add 2 lines to this file.
*
* RUNTIME_GUARD(envoy_reloadable_features_flag_name)
*
* to the sorted macro block below and
*
* &FLAGS_envoy_reloadable_features_flag_name
*
* to the runtime features constexpr.
*
* The runtime guard to use in source and release notes will then be of the form
* "envoy.reloadable_features.flag_name" due to the prior naming scheme and swapPrefix.
**/

#define RUNTIME_GUARD(name) ABSL_FLAG(bool, name, true, ""); // NOLINT
#define FALSE_RUNTIME_GUARD(name) ABSL_FLAG(bool, name, false, ""); // NOLINT

// Add additional features here to enable the new code paths by default.
//
// Per documentation in CONTRIBUTING.md is expected that new high risk code paths be guarded
// by runtime feature guards. If you add a guard of the form
// RUNTIME_GUARD(envoy_reloadable_features_my_feature_name)
// here you can guard code checking against "envoy.reloadable_features.my_feature_name".
// Please note the swap of envoy_reloadable_features_ to envoy.reloadable_features.!
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What happens if people mess this up? Will it error out? If not, can we do something to avoid this mismatch?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it'll work just fine actually, it just won't have standard envoy guard naming.

//
// if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.my_feature_name")) {
// [new code path]
// else {
// [old_code_path]
// }
//
// Runtime features are true by default, so the new code path is exercised.
// To make a runtime feature false by default, use FALSE_RUNTIME_GUARD, and add
// a TODO to change it to true.
//
// If issues are found that require a runtime feature to be disabled, it should be reported
// ASAP by filing a bug on github. Overriding non-buggy code is strongly discouraged to avoid the
// problem of the bugs being found after the old code path has been removed.
RUNTIME_GUARD(envoy_reloadable_features_allow_upstream_inline_write);
RUNTIME_GUARD(envoy_reloadable_features_append_or_truncate);
RUNTIME_GUARD(envoy_reloadable_features_append_to_accept_content_encoding_only_once);
Expand Down Expand Up @@ -81,18 +89,61 @@ ABSL_FLAG(uint64_t, re2_max_program_size_warn_level, // NOLINT

namespace Envoy {
namespace Runtime {
namespace {

std::string swapPrefix(std::string name) {
return absl::StrReplaceAll(name, {{"envoy_", "envoy."}, {"features_", "features."}});
}

} // namespace

// This is a singleton class to map Envoy style flag names to absl flags
class RuntimeFeatures {
public:
RuntimeFeatures();

// Get the command line flag corresponding to the Envoy style feature name, or
// nullptr if it is not a registered flag.
absl::CommandLineFlag* getFlag(absl::string_view feature) const {
auto it = all_features_.find(feature);
if (it == all_features_.end()) {
return nullptr;
}
return it->second;
}

private:
absl::flat_hash_map<std::string, absl::CommandLineFlag*> all_features_;
};

using RuntimeFeaturesDefaults = ConstSingleton<RuntimeFeatures>;

RuntimeFeatures::RuntimeFeatures() {
absl::flat_hash_map<absl::string_view, absl::CommandLineFlag*> flags = absl::GetAllFlags();
for (auto& it : flags) {
absl::string_view name = it.second->Name();
if ((!absl::StartsWith(name, "envoy_reloadable_features_") &&
!absl::StartsWith(name, "envoy_restart_features_")) ||
Comment on lines +125 to +126
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just so I understand since I'm not that familiar with this code: why this filtering? How are flags accessed if not via this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this largely helps us avoid pulling in erroneous flags in google 3, where we may end up with non-envoy command line flags.

!it.second->TryGet<bool>().has_value()) {
continue;
}
std::string envoy_name = swapPrefix(std::string(name));
all_features_.emplace(envoy_name, it.second);
}
}

bool isRuntimeFeature(absl::string_view feature) {
return RuntimeFeaturesDefaults::get().getFlag(feature) != nullptr;
}

bool runtimeFeatureEnabled(absl::string_view feature) {
auto* flag = RuntimeFeaturesDefaults::get().getFlag(feature);
absl::CommandLineFlag* flag = RuntimeFeaturesDefaults::get().getFlag(feature);
if (flag == nullptr) {
IS_ENVOY_BUG(absl::StrCat("Unable to find runtime feature ", feature));
return false;
}
return absl::GetFlag(*flag);
// We validate in map creation that the flag is a boolean.
return flag->TryGet<bool>().value();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

OOC: What is the difference between TryGet().value() and GetFlag()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

GetFlag takes the absl flag and returns a commandlineflag. Here we don't have the absl flag we only have the string name, so tryget allows us to get a coommandlineflag from that.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AH! I see. That makes sense. I think I got confused because link 138 didn't change so I assumed that the type of flag didn't change. I think this is an example of how auto can actually make code a bit less readable, but it's a tradeoff, for sure. For bonus points consider changing auto to absl::CommandLineFlag on line 138. But totally at your discretion.

}

uint64_t getInteger(absl::string_view feature, uint64_t default_value) {
Expand All @@ -113,81 +164,14 @@ uint64_t getInteger(absl::string_view feature, uint64_t default_value) {
return default_value;
}

// Add additional features here to enable the new code paths by default.
//
// Per documentation in CONTRIBUTING.md is expected that new high risk code paths be guarded
// by runtime feature guards, i.e
//
// if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.my_feature_name")) {
// [new code path]
// else {
// [old_code_path]
// }
//
// Runtime features are false by default, so the old code path is exercised.
// To make a runtime feature true by default, add it to the array below.
// New features should be true-by-default for an Envoy release cycle before the
// old code path is removed.
//
// If issues are found that require a runtime feature to be disabled, it should be reported
// ASAP by filing a bug on github. Overriding non-buggy code is strongly discouraged to avoid the
// problem of the bugs being found after the old code path has been removed.
// clang-format off
constexpr absl::Flag<bool>* runtime_features[] = {
// Test flags
&FLAGS_envoy_reloadable_features_test_feature_false,
&FLAGS_envoy_reloadable_features_test_feature_true,
// Begin alphabetically sorted section_
&FLAGS_envoy_reloadable_features_allow_multiple_dns_addresses,
&FLAGS_envoy_reloadable_features_allow_upstream_inline_write,
&FLAGS_envoy_reloadable_features_append_or_truncate,
&FLAGS_envoy_reloadable_features_append_to_accept_content_encoding_only_once,
&FLAGS_envoy_reloadable_features_conn_pool_delete_when_idle,
&FLAGS_envoy_reloadable_features_conn_pool_new_stream_with_early_data_and_http3,
&FLAGS_envoy_reloadable_features_correct_scheme_and_xfp,
&FLAGS_envoy_reloadable_features_correctly_validate_alpn,
&FLAGS_envoy_reloadable_features_defer_processing_backedup_streams,
&FLAGS_envoy_reloadable_features_deprecate_global_ints,
&FLAGS_envoy_reloadable_features_disable_tls_inspector_injection,
&FLAGS_envoy_reloadable_features_do_not_await_headers_on_upstream_timeout_to_emit_stats,
&FLAGS_envoy_reloadable_features_enable_grpc_async_client_cache,
&FLAGS_envoy_reloadable_features_fix_added_trailers,
&FLAGS_envoy_reloadable_features_handle_stream_reset_during_hcm_encoding,
&FLAGS_envoy_reloadable_features_http1_lazy_read_disable,
&FLAGS_envoy_reloadable_features_http2_allow_capacity_increase_by_settings,
&FLAGS_envoy_reloadable_features_http2_new_codec_wrapper,
&FLAGS_envoy_reloadable_features_http_ext_authz_do_not_skip_direct_response_and_redirect,
&FLAGS_envoy_reloadable_features_http_reject_path_with_fragment,
&FLAGS_envoy_reloadable_features_http_strip_fragment_from_path_unsafe_if_disabled,
&FLAGS_envoy_reloadable_features_internal_address,
&FLAGS_envoy_reloadable_features_listener_wildcard_match_ip_family,
&FLAGS_envoy_reloadable_features_new_tcp_connection_pool,
&FLAGS_envoy_reloadable_features_postpone_h3_client_connect_to_next_loop,
&FLAGS_envoy_reloadable_features_proxy_102_103,
&FLAGS_envoy_reloadable_features_sanitize_http_header_referer,
&FLAGS_envoy_reloadable_features_skip_delay_close,
&FLAGS_envoy_reloadable_features_skip_dispatching_frames_for_closed_connection,
&FLAGS_envoy_reloadable_features_strict_check_on_ipv4_compat,
&FLAGS_envoy_reloadable_features_support_locality_update_on_eds_cluster_endpoints,
&FLAGS_envoy_reloadable_features_udp_listener_updates_filter_chain_in_place,
&FLAGS_envoy_reloadable_features_unified_mux,
&FLAGS_envoy_reloadable_features_update_expected_rq_timeout_on_retry,
&FLAGS_envoy_reloadable_features_update_grpc_response_error_tag,
&FLAGS_envoy_reloadable_features_use_dns_ttl,
&FLAGS_envoy_reloadable_features_validate_connect,
&FLAGS_envoy_restart_features_explicit_wildcard_resource,
&FLAGS_envoy_restart_features_use_apple_api_for_dns_lookups,
&FLAGS_envoy_restart_features_no_runtime_singleton,
};
// clang-format on

void maybeSetRuntimeGuard(absl::string_view name, bool value) {
auto* flag = RuntimeFeaturesDefaults::get().getFlag(name);
absl::CommandLineFlag* flag = RuntimeFeaturesDefaults::get().getFlag(name);
if (flag == nullptr) {
IS_ENVOY_BUG(absl::StrCat("Unable to find runtime feature ", name));
return;
}
absl::SetFlag(flag, value);
std::string err;
flag->ParseFrom(value ? "true" : "false", &err);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Interesting. The old code looked simpler, but I suspect it was deficient in some way. Can you elaborate?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

the code is cleaner using absl::Flags, but while you can get from string to CommandLineFlag there's no public registry from string to absl::Flags - the reflection only goes from string to CommandLineFlag, so I had to switch from the "I know my type" absl::Flag to "am I a boolean or not?" CommandLineFlag variant to get rid of the array.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Makes sense. Same point about auto confusing me. Same bonus points for changing auto to absl::CommandLineFlag :)

}

void maybeSetDeprecatedInts(absl::string_view name, uint32_t value) {
Expand All @@ -211,36 +195,5 @@ void maybeSetDeprecatedInts(absl::string_view name, uint32_t value) {
}
}

std::string swapPrefix(std::string name) {
return absl::StrReplaceAll(name, {{"envoy_", "envoy."}, {"features_", "features."}});
}

RuntimeFeatures::RuntimeFeatures() {
for (auto& feature : runtime_features) {
auto& reflection = absl::GetFlagReflectionHandle(*feature);
std::string envoy_name = swapPrefix(std::string(reflection.Name()));
all_features_.emplace(envoy_name, feature);
absl::optional<bool> value = reflection.TryGet<bool>();
ASSERT(value.has_value());
if (value.value()) {
enabled_features_.emplace(envoy_name, feature);
} else {
disabled_features_.emplace(envoy_name, feature);
}
}
}

void RuntimeFeatures::restoreDefaults() const {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this stuff no longer necessary?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, it's replaced by the absl flagsaver infrastructure

for (const auto& feature : enabled_features_) {
absl::SetFlag(feature.second, true);
}
for (const auto& feature : disabled_features_) {
absl::SetFlag(feature.second, false);
}
absl::SetFlag(&FLAGS_envoy_headermap_lazy_map_min_size, 3);
absl::SetFlag(&FLAGS_re2_max_program_size_error_level, 100);
absl::SetFlag(&FLAGS_re2_max_program_size_warn_level, std::numeric_limits<uint32_t>::max());
}

} // namespace Runtime
} // namespace Envoy
25 changes: 1 addition & 24 deletions source/common/runtime/runtime_features.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

#include "absl/container/flat_hash_set.h"
#include "absl/flags/flag.h"
#include "absl/flags/reflection.h"

namespace Envoy {
namespace Runtime {
Expand All @@ -24,29 +25,5 @@ constexpr absl::string_view conn_pool_new_stream_with_early_data_and_http3 =
constexpr absl::string_view defer_processing_backedup_streams =
"envoy.reloadable_features.defer_processing_backedup_streams";

class RuntimeFeatures {
public:
RuntimeFeatures();

absl::Flag<bool>* getFlag(absl::string_view feature) const {
auto it = all_features_.find(feature);
if (it == all_features_.end()) {
return nullptr;
}
return it->second;
}

void restoreDefaults() const;

private:
friend class RuntimeFeaturesPeer;

absl::flat_hash_map<std::string, absl::Flag<bool>*> enabled_features_;
absl::flat_hash_map<std::string, absl::Flag<bool>*> disabled_features_;
absl::flat_hash_map<std::string, absl::Flag<bool>*> all_features_;
};

using RuntimeFeaturesDefaults = ConstSingleton<RuntimeFeatures>;

} // namespace Runtime
} // namespace Envoy
3 changes: 0 additions & 3 deletions test/common/http/http1/codec_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3002,7 +3002,6 @@ TEST_F(Http1ServerConnectionImplTest, ManyLargeRequestHeadersAccepted) {

TEST_F(Http1ServerConnectionImplTest, RuntimeLazyReadDisableTest) {
TestScopedRuntime scoped_runtime;
Runtime::RuntimeFeaturesDefaults::get().restoreDefaults();

// No readDisable for normal non-piped HTTP request.
{
Expand Down Expand Up @@ -3077,7 +3076,6 @@ TEST_F(Http1ServerConnectionImplTest, RuntimeLazyReadDisableTest) {
// same time.
TEST_F(Http1ServerConnectionImplTest, PipedRequestWithSingleEvent) {
TestScopedRuntime scoped_runtime;
Runtime::RuntimeFeaturesDefaults::get().restoreDefaults();

initialize();

Expand Down Expand Up @@ -3114,7 +3112,6 @@ TEST_F(Http1ServerConnectionImplTest, PipedRequestWithSingleEvent) {
// before the end of the first request.
TEST_F(Http1ServerConnectionImplTest, PipedRequestWithMutipleEvent) {
TestScopedRuntime scoped_runtime;
Runtime::RuntimeFeaturesDefaults::get().restoreDefaults();

initialize();

Expand Down
6 changes: 1 addition & 5 deletions test/test_common/test_runtime.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,8 @@ class TestScopedRuntime {
loader().mergeValues(values);
}

~TestScopedRuntime() {
Runtime::RuntimeFeatures features;
features.restoreDefaults();
}

protected:
absl::FlagSaver saver_;
Event::MockDispatcher dispatcher_;
testing::NiceMock<ThreadLocal::MockInstance> tls_;
Stats::TestUtil::TestStore store_;
Expand Down
5 changes: 4 additions & 1 deletion test/test_listener.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ void TestListener::OnTestEnd(const ::testing::TestInfo& test_info) {
absl::StrCat("MainThreadLeak: [", test_info.test_suite_name(), ".",
test_info.name(), "] test exited before main thread shut down"));
}
Runtime::RuntimeFeaturesDefaults::get().restoreDefaults();
// We must do this in two phases; reset the old flags to get back to the clean state before
// constructing a new flag saver to latch the clean values.
saver_.reset();
saver_ = std::make_unique<absl::FlagSaver>();
}

} // namespace Envoy
6 changes: 5 additions & 1 deletion test/test_listener.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "absl/flags/reflection.h"
#include "gtest/gtest.h"

namespace Envoy {
Expand All @@ -21,10 +22,13 @@ namespace Envoy {
// be a tax paid by every test method in the codebase.
class TestListener : public ::testing::EmptyTestEventListener {
public:
TestListener(bool validate_singletons = true) : validate_singletons_(validate_singletons) {}
TestListener(bool validate_singletons = true)
: saver_(std::make_unique<absl::FlagSaver>()), validate_singletons_(validate_singletons) {}
void OnTestEnd(const ::testing::TestInfo& test_info) override;

private:
// Make sure runtime guards are restored to defaults on test completion.
std::unique_ptr<absl::FlagSaver> saver_;
bool validate_singletons_;
};

Expand Down