// Copyright 2022 gRPC authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include "src/core/lib/experiments/config.h" #include #include #include #include #include #include #include "absl/functional/any_invocable.h" #include "absl/strings/str_join.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "absl/strings/strip.h" #include #include "src/core/lib/config/config_vars.h" #include "src/core/lib/experiments/experiments.h" #include "src/core/lib/gprpp/crash.h" // IWYU pragma: keep #include "src/core/lib/gprpp/no_destruct.h" #ifndef GRPC_EXPERIMENTS_ARE_FINAL namespace grpc_core { namespace { struct Experiments { bool enabled[kNumExperiments]; }; struct ForcedExperiment { bool forced = false; bool value; }; ForcedExperiment* ForcedExperiments() { static NoDestruct forced_experiments[kNumExperiments]; return &**forced_experiments; } std::atomic* Loaded() { static NoDestruct> loaded(false); return &*loaded; } absl::AnyInvocable* g_check_constraints_cb = nullptr; class TestExperiments { public: TestExperiments(const ExperimentMetadata* experiment_metadata, size_t num_experiments) { enabled_ = new bool[num_experiments]; for (size_t i = 0; i < num_experiments; i++) { if (g_check_constraints_cb != nullptr) { enabled_[i] = (*g_check_constraints_cb)(experiment_metadata[i]); } else { enabled_[i] = experiment_metadata[i].default_value; } } // For each comma-separated experiment in the global config: for (auto experiment : absl::StrSplit(ConfigVars::Get().Experiments(), ',', absl::SkipWhitespace())) { // Enable unless prefixed with '-' (=> disable). bool enable = !absl::ConsumePrefix(&experiment, "-"); // See if we can find the experiment in the list in this binary. for (size_t i = 0; i < num_experiments; i++) { if (experiment == experiment_metadata[i].name) { enabled_[i] = enable; break; } } } } // Overloading [] operator to access elements in array style bool operator[](int index) { return enabled_[index]; } ~TestExperiments() { delete enabled_; } private: bool* enabled_; }; TestExperiments* g_test_experiments = nullptr; GPR_ATTRIBUTE_NOINLINE Experiments LoadExperimentsFromConfigVariableInner() { // Set defaults from metadata. Experiments experiments; for (size_t i = 0; i < kNumExperiments; i++) { if (!ForcedExperiments()[i].forced) { if (g_check_constraints_cb != nullptr) { experiments.enabled[i] = (*g_check_constraints_cb)(g_experiment_metadata[i]); } else { experiments.enabled[i] = g_experiment_metadata[i].default_value; } } else { experiments.enabled[i] = ForcedExperiments()[i].value; } } // For each comma-separated experiment in the global config: for (auto experiment : absl::StrSplit(ConfigVars::Get().Experiments(), ',', absl::SkipWhitespace())) { // Enable unless prefixed with '-' (=> disable). bool enable = true; if (experiment[0] == '-') { enable = false; experiment.remove_prefix(1); } // See if we can find the experiment in the list in this binary. bool found = false; for (size_t i = 0; i < kNumExperiments; i++) { if (experiment == g_experiment_metadata[i].name) { experiments.enabled[i] = enable; found = true; break; } } // If not found log an error, but don't take any other action. // Allows us an easy path to disabling experiments. if (!found) { gpr_log(GPR_ERROR, "Unknown experiment: %s", std::string(experiment).c_str()); } } for (size_t i = 0; i < kNumExperiments; i++) { // If required experiments are not enabled, disable this one too. for (size_t j = 0; j < g_experiment_metadata[i].num_required_experiments; j++) { // Require that we can check dependent requirements with a linear sweep // (implies the experiments generator must DAG sort the experiments) GPR_ASSERT(g_experiment_metadata[i].required_experiments[j] < i); if (!experiments .enabled[g_experiment_metadata[i].required_experiments[j]]) { experiments.enabled[i] = false; } } } return experiments; } Experiments LoadExperimentsFromConfigVariable() { Loaded()->store(true, std::memory_order_relaxed); return LoadExperimentsFromConfigVariableInner(); } Experiments& ExperimentsSingleton() { // One time initialization: static NoDestruct experiments{ LoadExperimentsFromConfigVariable()}; return *experiments; } } // namespace void TestOnlyReloadExperimentsFromConfigVariables() { ExperimentsSingleton() = LoadExperimentsFromConfigVariable(); PrintExperimentsList(); } void LoadTestOnlyExperimentsFromMetadata( const ExperimentMetadata* experiment_metadata, size_t num_experiments) { g_test_experiments = new TestExperiments(experiment_metadata, num_experiments); } bool IsExperimentEnabled(size_t experiment_id) { return ExperimentsSingleton().enabled[experiment_id]; } bool IsExperimentEnabledInConfiguration(size_t experiment_id) { return LoadExperimentsFromConfigVariableInner().enabled[experiment_id]; } bool IsTestExperimentEnabled(size_t experiment_id) { return (*g_test_experiments)[experiment_id]; } void PrintExperimentsList() { std::map experiment_status; std::set defaulted_on_experiments; for (size_t i = 0; i < kNumExperiments; i++) { const char* name = g_experiment_metadata[i].name; const bool enabled = IsExperimentEnabled(i); const bool default_enabled = g_experiment_metadata[i].default_value; const bool forced = ForcedExperiments()[i].forced; if (!default_enabled && !enabled) continue; if (default_enabled && enabled) { defaulted_on_experiments.insert(name); continue; } if (enabled) { if (g_check_constraints_cb != nullptr && (*g_check_constraints_cb)(g_experiment_metadata[i])) { experiment_status[name] = "on:constraints"; continue; } if (forced && ForcedExperiments()[i].value) { experiment_status[name] = "on:forced"; continue; } experiment_status[name] = "on"; } else { if (forced && !ForcedExperiments()[i].value) { experiment_status[name] = "off:forced"; continue; } experiment_status[name] = "off"; } } if (experiment_status.empty()) { if (!defaulted_on_experiments.empty()) { gpr_log(GPR_INFO, "gRPC experiments enabled: %s", absl::StrJoin(defaulted_on_experiments, ", ").c_str()); } } else { if (defaulted_on_experiments.empty()) { gpr_log(GPR_INFO, "gRPC experiments: %s", absl::StrJoin(experiment_status, ", ", absl::PairFormatter(":")) .c_str()); } else { gpr_log(GPR_INFO, "gRPC experiments: %s; default-enabled: %s", absl::StrJoin(experiment_status, ", ", absl::PairFormatter(":")) .c_str(), absl::StrJoin(defaulted_on_experiments, ", ").c_str()); } } } void ForceEnableExperiment(absl::string_view experiment, bool enable) { GPR_ASSERT(Loaded()->load(std::memory_order_relaxed) == false); for (size_t i = 0; i < kNumExperiments; i++) { if (g_experiment_metadata[i].name != experiment) continue; if (ForcedExperiments()[i].forced) { GPR_ASSERT(ForcedExperiments()[i].value == enable); } else { ForcedExperiments()[i].forced = true; ForcedExperiments()[i].value = enable; } return; } gpr_log(GPR_INFO, "gRPC EXPERIMENT %s not found to force %s", std::string(experiment).c_str(), enable ? "enable" : "disable"); } void RegisterExperimentConstraintsValidator( absl::AnyInvocable check_constraints_cb) { g_check_constraints_cb = new absl::AnyInvocable( std::move(check_constraints_cb)); } } // namespace grpc_core #else namespace grpc_core { void PrintExperimentsList() {} void ForceEnableExperiment(absl::string_view experiment_name, bool) { Crash(absl::StrCat("ForceEnableExperiment(\"", experiment_name, "\") called in final build")); } void RegisterExperimentConstraintsValidator( absl::AnyInvocable< bool(struct ExperimentMetadata)> /*check_constraints_cb*/) {} } // namespace grpc_core #endif