| // 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 "src/core/lib/experiments/config.h" |
| |
| #include <string.h> |
| |
| #include <algorithm> |
| #include <atomic> |
| #include <map> |
| #include <string> |
| #include <utility> |
| |
| #include "absl/functional/any_invocable.h" |
| #include "absl/log/check.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 <grpc/support/log.h> |
| #include <grpc/support/port_platform.h> |
| |
| #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<ForcedExperiment> forced_experiments[kNumExperiments]; |
| return &**forced_experiments; |
| } |
| |
| std::atomic<bool>* Loaded() { |
| static NoDestruct<std::atomic<bool>> loaded(false); |
| return &*loaded; |
| } |
| |
| absl::AnyInvocable<bool(struct ExperimentMetadata)>* 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) |
| CHECK(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> 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<std::string, std::string> experiment_status; |
| std::set<std::string> 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) { |
| CHECK(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) { |
| CHECK(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<bool(struct ExperimentMetadata)> check_constraints_cb) { |
| g_check_constraints_cb = |
| new absl::AnyInvocable<bool(struct ExperimentMetadata)>( |
| 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 |