-
Notifications
You must be signed in to change notification settings - Fork 5.5k
[fuzz] Added random load balancer fuzz #13400
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
Merged
Merged
Changes from 29 commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
b4bb682
Save progress
zasweq 8df52a8
Save progress
zasweq 7ee7b14
Save progress, can almost build
zasweq 9e8d678
Almost builds
zasweq b977914
Merge branch 'master' into load-balancer-fuzz
zasweq 1cfaa5d
Builds correctly
zasweq 73c24ed
Got rid of unused imports
zasweq 54d5b67
Save progress
zasweq d5f072f
Ready for PR, working with Asan-fuzzer
zasweq 8c00f14
Got rid of logs
zasweq 810eb49
Spelling
zasweq 413214c
First round of comments, saving progress
zasweq b5619fe
Responded to comments and redesigned based on design discussion
zasweq c4a53c0
Clean up
zasweq 6bcb394
Responded to Asra's comments
zasweq ea0264a
Save progress, responded to Alex's comments and some Harvey comments
zasweq e43bb2c
Responded to Harvey's comments
zasweq 6e3c420
Style
zasweq a7d7e88
Responded to Harvey's comments
zasweq 5c6bdaf
Spelling CI
zasweq 92666c1
Changed update to use generated bytes, added util class
zasweq 6ee35db
Responded to Asra's comments
zasweq a81eabe
Added logic for localities
zasweq 31a5668
Fixed slow iterations
zasweq 91e5738
Partially responded to comments
zasweq 794918b
Clang Tidy
zasweq c1c8e29
Adi's refactor comment
zasweq 741b3a0
Clang tidy and Harvey nit
zasweq 52cbbd5
Responded to Asra's comments
zasweq 4bec47d
Partially responded to comments, still have 0(N^2) and flags
zasweq 11a7f74
Removed O(N^2) and added health flags
zasweq 4675237
Responded to Asra's comments
zasweq d07db49
Spelling
zasweq ff8e22f
Responded to Harvey's comments
zasweq d71c5d1
Responded to comments
zasweq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package test.common.upstream; | ||
|
|
||
| import "validate/validate.proto"; | ||
| import "envoy/config/cluster/v3/cluster.proto"; | ||
| import "google/protobuf/empty.proto"; | ||
|
|
||
| message UpdateHealthFlags { | ||
| // The host priority determines what host set within the priority set which will get updated. | ||
| uint64 host_priority = 1; | ||
| // These will determine how many hosts will get placed into health hosts, degraded hosts, and | ||
| // excluded hosts from the full host list. | ||
| uint32 num_healthy_hosts = 2; | ||
| uint32 num_degraded_hosts = 3; | ||
| uint32 num_excluded_hosts = 4; | ||
| // This is used to determine which hosts get marked as healthy, degraded, and excluded. | ||
| bytes random_bytestring = 5 [(validate.rules).bytes = {min_len: 1, max_len: 256}]; | ||
|
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, an idea of making this run-length encoded just occurred to me, it would be space efficient, but not sure how that would interact with the fuzzing cross-over. |
||
| } | ||
|
|
||
| message LbAction { | ||
| oneof action_selector { | ||
| option (validate.required) = true; | ||
| // This updates the health flags of hosts at a certain priority level. The number of hosts in each priority level/in localities is static, | ||
| // as untrusted upstreams cannot change that, and can only change their health flags. | ||
| UpdateHealthFlags update_health_flags = 1; | ||
| // Prefetches a host using the encapsulated specific load balancer. | ||
| google.protobuf.Empty prefetch = 2; | ||
|
zasweq marked this conversation as resolved.
|
||
| // Chooses a host using the encapsulated specific load balancer. | ||
| google.protobuf.Empty choose_host = 3; | ||
| } | ||
| } | ||
|
|
||
| message SetupPriorityLevel { | ||
| uint32 num_hosts_in_priority_level = 1 [(validate.rules).uint32.lte = 500]; | ||
| // Doesn't need a cap on sum, as subset utility class handles "overflowing" subsets | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| uint32 num_hosts_locality_one = 2 [(validate.rules).uint32.lte = 500]; | ||
| uint32 num_hosts_locality_two = 3 [(validate.rules).uint32.lte = 500]; | ||
| // Hard cap at 3 localities for simplicity | ||
| uint32 num_hosts_locality_three = 4 [(validate.rules).uint32.lte = 500]; | ||
| // For choosing which hosts go in which locality | ||
| bytes random_bytestring = 5 [(validate.rules).bytes = {min_len: 1, max_len: 256}]; | ||
| } | ||
|
|
||
| // This message represents what LoadBalancerFuzzBase will interact with, performing setup of host sets and calling into load balancers. | ||
| // The logic that this message represents and the base class for load balancing fuzzing will be logic that maps to all types of load balancing | ||
| // and can be used in a modular way at the highest level for each load balancer. | ||
| message LoadBalancerTestCase { | ||
| envoy.config.cluster.v3.Cluster.CommonLbConfig common_lb_config = 1 | ||
| [(validate.rules).message.required = true]; | ||
| repeated LbAction actions = 2; | ||
|
|
||
| // Each generated integer will cause the fuzzer to initialize hosts at a certain priority level, each integer generated adding a priority | ||
| // level with integer generated hosts in that new priority level. Capped at 20 for simplicity. | ||
| repeated SetupPriorityLevel setup_priority_levels = 3 | ||
| [(validate.rules).repeated = {min_items: 1, max_items: 20}]; | ||
|
zasweq marked this conversation as resolved.
|
||
|
|
||
| // This number is used to instantiate the prng. The prng takes the place of random() calls, allowing a representative random distribution | ||
| // which is also deterministic. | ||
| uint64 seed_for_prng = 4 [(validate.rules).uint64.gt = 0]; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| #include "test/common/upstream/load_balancer_fuzz_base.h" | ||
|
|
||
| #include "test/common/upstream/utility.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Upstream { | ||
|
|
||
| namespace { | ||
|
|
||
| constexpr uint32_t MaxNumHostsPerPriorityLevel = 256; | ||
|
zasweq marked this conversation as resolved.
|
||
|
|
||
| } // namespace | ||
|
|
||
| void LoadBalancerFuzzBase::initializeASingleHostSet( | ||
| const test::common::upstream::SetupPriorityLevel& setup_priority_level, | ||
| const uint8_t priority_level) { | ||
| const uint32_t num_hosts_in_priority_level = setup_priority_level.num_hosts_in_priority_level(); | ||
| ENVOY_LOG_MISC(trace, "Will attempt to initialize host set {} with {} hosts.", priority_level, | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| num_hosts_in_priority_level); | ||
| MockHostSet& host_set = *priority_set_.getMockHostSet(priority_level); | ||
| uint32_t hosts_made = 0; | ||
| // Cap each host set at 256 hosts for efficiency - Leave port clause in for future changes | ||
| while (hosts_made < std::min(num_hosts_in_priority_level, MaxNumHostsPerPriorityLevel) && | ||
| port_ < 65535) { | ||
| host_set.hosts_.push_back(makeTestHost(info_, "tcp://127.0.0.1:" + std::to_string(port_))); | ||
| ++port_; | ||
| ++hosts_made; | ||
|
asraa marked this conversation as resolved.
|
||
| } | ||
|
|
||
| Fuzz::ProperSubsetSelector subset_selector(setup_priority_level.random_bytestring()); | ||
|
|
||
| const std::vector<std::vector<uint8_t>> localities = subset_selector.constructSubsets( | ||
| {setup_priority_level.num_hosts_locality_one(), setup_priority_level.num_hosts_locality_two(), | ||
| setup_priority_level.num_hosts_locality_three()}, | ||
| std::min(num_hosts_in_priority_level, MaxNumHostsPerPriorityLevel)); | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
|
|
||
| HostVector locality_one = {}; | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| HostVector locality_two = {}; | ||
| HostVector locality_three = {}; | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| // Used to index into correct locality in iteration through subsets | ||
| std::array<HostVector, 3> locality_indexes = {locality_one, locality_two, locality_three}; | ||
|
|
||
| for (uint8_t locality = 0; locality < locality_indexes.size(); locality++) { | ||
| for (uint8_t index : localities[locality]) { | ||
| locality_indexes[locality].push_back(host_set.hosts_[index]); | ||
| locality_indexes_[index] = locality; | ||
| } | ||
| } | ||
|
|
||
| ENVOY_LOG_MISC(trace, "Added these hosts to locality 1: ", absl::StrJoin(localities[0], " ")); | ||
| ENVOY_LOG_MISC(trace, "Added these hosts to locality 2: ", absl::StrJoin(localities[1], " ")); | ||
| ENVOY_LOG_MISC(trace, "Added these hosts to locality 3: ", absl::StrJoin(localities[2], " ")); | ||
|
|
||
| host_set.hosts_per_locality_ = makeHostsPerLocality({locality_one, locality_two, locality_three}); | ||
| } | ||
|
|
||
| // Initializes random and fixed host sets | ||
| void LoadBalancerFuzzBase::initializeLbComponents( | ||
| const test::common::upstream::LoadBalancerTestCase& input) { | ||
| random_.initializeSeed(input.seed_for_prng()); | ||
| for (uint8_t priority_of_host_set = 0; | ||
| priority_of_host_set < input.setup_priority_levels().size(); ++priority_of_host_set) { | ||
| initializeASingleHostSet(input.setup_priority_levels().at(priority_of_host_set), | ||
| priority_of_host_set); | ||
| } | ||
| num_priority_levels_ = input.setup_priority_levels().size(); | ||
| } | ||
|
|
||
| // Updating host sets is shared amongst all the load balancer tests. Since logically, we're just | ||
| // setting the mock priority set to have certain values, and all load balancers interface with host | ||
| // sets and their health statuses, this action maps to all load balancers. | ||
| void LoadBalancerFuzzBase::updateHealthFlagsForAHostSet(const uint64_t host_priority, | ||
| const uint32_t num_healthy_hosts, | ||
| const uint32_t num_degraded_hosts, | ||
| const uint32_t num_excluded_hosts, | ||
| const std::string random_bytestring) { | ||
| const uint8_t priority_of_host_set = host_priority % num_priority_levels_; | ||
| ENVOY_LOG_MISC(trace, "Updating health flags for host set at priority: {}", priority_of_host_set); | ||
| MockHostSet& host_set = *priority_set_.getMockHostSet(priority_of_host_set); | ||
| // This downcast will not overflow because size is capped by port numbers | ||
|
zasweq marked this conversation as resolved.
|
||
| const uint32_t host_set_size = host_set.hosts_.size(); | ||
| host_set.healthy_hosts_.clear(); | ||
| host_set.degraded_hosts_.clear(); | ||
| host_set.excluded_hosts_.clear(); | ||
|
|
||
| Fuzz::ProperSubsetSelector subset_selector(random_bytestring); | ||
|
|
||
| const std::vector<std::vector<uint8_t>> subsets = subset_selector.constructSubsets( | ||
| {num_healthy_hosts, num_degraded_hosts, num_excluded_hosts}, host_set_size); | ||
|
|
||
| // Healthy hosts are first subset | ||
| for (uint8_t index : subsets.at(0)) { | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| host_set.healthy_hosts_.push_back(host_set.hosts_[index]); | ||
| } | ||
| ENVOY_LOG_MISC(trace, "Hosts made healthy at priority level {}: {}", priority_of_host_set, | ||
| absl::StrJoin(subsets.at(0), " ")); | ||
|
|
||
| // Degraded hosts are second subset | ||
| for (uint8_t index : subsets.at(1)) { | ||
| host_set.degraded_hosts_.push_back(host_set.hosts_[index]); | ||
|
zasweq marked this conversation as resolved.
|
||
| } | ||
| ENVOY_LOG_MISC(trace, "Hosts made degraded at priority level {}: {}", priority_of_host_set, | ||
| absl::StrJoin(subsets.at(1), " ")); | ||
|
|
||
| // Excluded hosts are third subset | ||
| for (uint8_t index : subsets.at(2)) { | ||
| host_set.excluded_hosts_.push_back(host_set.hosts_[index]); | ||
| } | ||
| ENVOY_LOG_MISC(trace, "Hosts made excluded at priority level {}: {}", priority_of_host_set, | ||
| absl::StrJoin(subsets.at(2), " ")); | ||
|
|
||
| // Handle updating health flags for hosts_per_locality_ | ||
|
|
||
| // The index within the array of the vector represents the locality | ||
| std::array<HostVector, 3> healthy_hosts_per_locality; | ||
| std::array<HostVector, 3> degraded_hosts_per_locality; | ||
| std::array<HostVector, 3> excluded_hosts_per_locality; | ||
|
|
||
| // Wrap those three in an array here, where the index represents health flag of | ||
| // healthy/degraded/excluded, used for indexing during iteration through subsets | ||
| std::array<std::array<HostVector, 3>, 3> locality_health_flags = { | ||
| healthy_hosts_per_locality, degraded_hosts_per_locality, excluded_hosts_per_locality}; | ||
|
|
||
| // Iterate through subsets | ||
| for (uint8_t health_flag = 0; health_flag < locality_health_flags.size(); health_flag++) { | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| for (uint8_t index : subsets.at(health_flag)) { // Each subset logically represents a health | ||
| // flag | ||
| // If the host is in a locality, we have to update the corresponding health flag host vector | ||
| if (!(locality_indexes_.find(index) == locality_indexes_.end())) { | ||
| // First dimension of array represents health_flag, second represents locality, which is | ||
| // pulled from map | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| locality_health_flags[health_flag][locality_indexes_[index]].push_back( | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| host_set.hosts_[index]); | ||
| ENVOY_LOG_MISC(trace, "Added host at index {} in locality {} to health flag set {}", index, | ||
| locality_indexes_[index], health_flag + 1); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // This overrides what is currently present in the host set, thus not having to explicitly call | ||
| // vector.clear() | ||
| host_set.healthy_hosts_per_locality_ = | ||
| makeHostsPerLocality({healthy_hosts_per_locality[0], healthy_hosts_per_locality[1], | ||
| healthy_hosts_per_locality[2]}); | ||
| host_set.degraded_hosts_per_locality_ = | ||
| makeHostsPerLocality({degraded_hosts_per_locality[0], degraded_hosts_per_locality[1], | ||
| degraded_hosts_per_locality[2]}); | ||
| host_set.excluded_hosts_per_locality_ = | ||
| makeHostsPerLocality({excluded_hosts_per_locality[0], excluded_hosts_per_locality[1], | ||
| excluded_hosts_per_locality[2]}); | ||
|
|
||
| host_set.runCallbacks({}, {}); | ||
|
zasweq marked this conversation as resolved.
zasweq marked this conversation as resolved.
|
||
| } | ||
|
|
||
| void LoadBalancerFuzzBase::prefetch() { | ||
| // TODO: context, could generate it in proto action | ||
| lb_->peekAnotherHost(nullptr); | ||
| } | ||
|
|
||
| void LoadBalancerFuzzBase::chooseHost() { | ||
| // TODO: context, could generate it in proto action | ||
| lb_->chooseHost(nullptr); | ||
| } | ||
|
|
||
| void LoadBalancerFuzzBase::replay( | ||
| const Protobuf::RepeatedPtrField<test::common::upstream::LbAction>& actions) { | ||
| constexpr auto max_actions = 64; | ||
| for (int i = 0; i < std::min(max_actions, actions.size()); ++i) { | ||
| const auto& event = actions.at(i); | ||
| ENVOY_LOG_MISC(trace, "Action: {}", event.DebugString()); | ||
| switch (event.action_selector_case()) { | ||
| case test::common::upstream::LbAction::kUpdateHealthFlags: { | ||
| updateHealthFlagsForAHostSet(event.update_health_flags().host_priority(), | ||
| event.update_health_flags().num_healthy_hosts(), | ||
| event.update_health_flags().num_degraded_hosts(), | ||
| event.update_health_flags().num_excluded_hosts(), | ||
| event.update_health_flags().random_bytestring()); | ||
| break; | ||
| } | ||
| case test::common::upstream::LbAction::kPrefetch: | ||
| prefetch(); | ||
| break; | ||
| case test::common::upstream::LbAction::kChooseHost: | ||
| chooseHost(); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } // namespace Upstream | ||
| } // namespace Envoy | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| #include "envoy/config/cluster/v3/cluster.pb.h" | ||
|
|
||
| #include "common/upstream/load_balancer_impl.h" | ||
|
|
||
| #include "test/common/upstream/load_balancer_fuzz.pb.validate.h" | ||
| #include "test/fuzz/random.h" | ||
| #include "test/mocks/common.h" | ||
| #include "test/mocks/runtime/mocks.h" | ||
| #include "test/mocks/upstream/cluster_info.h" | ||
| #include "test/mocks/upstream/host_set.h" | ||
| #include "test/mocks/upstream/load_balancer_context.h" | ||
| #include "test/mocks/upstream/priority_set.h" | ||
|
|
||
| namespace Envoy { | ||
| namespace Upstream { | ||
|
|
||
| // This class implements replay logic, and also handles the initial setup of static host sets and | ||
| // the subsequent updates to those sets. | ||
| class LoadBalancerFuzzBase { | ||
| public: | ||
| LoadBalancerFuzzBase() : stats_(ClusterInfoImpl::generateStats(stats_store_)){}; | ||
|
|
||
| // Initializes load balancer components shared amongst every load balancer, random_, and | ||
| // priority_set_ | ||
| void initializeLbComponents(const test::common::upstream::LoadBalancerTestCase& input); | ||
| void updateHealthFlagsForAHostSet(const uint64_t host_priority, const uint32_t num_healthy_hosts, | ||
| const uint32_t num_degraded_hosts, | ||
| const uint32_t num_excluded_hosts, | ||
| const std::string random_bytestring); | ||
| // These two actions have a lot of logic attached to them. However, all the logic that the load | ||
| // balancer needs to run its algorithm is already encapsulated within the load balancer. Thus, | ||
| // once the load balancer is constructed, all this class has to do is call lb_->peekAnotherHost() | ||
| // and lb_->chooseHost(). | ||
| void prefetch(); | ||
| void chooseHost(); | ||
| ~LoadBalancerFuzzBase() = default; | ||
| void replay(const Protobuf::RepeatedPtrField<test::common::upstream::LbAction>& actions); | ||
|
|
||
| // These public objects shared amongst all types of load balancers will be used to construct load | ||
| // balancers in specific load balancer fuzz classes | ||
| Stats::IsolatedStoreImpl stats_store_; | ||
| ClusterStats stats_; | ||
| NiceMock<Runtime::MockLoader> runtime_; | ||
| Random::PsuedoRandomGenerator64 random_; | ||
| NiceMock<MockPrioritySet> priority_set_; | ||
| std::shared_ptr<MockClusterInfo> info_{new NiceMock<MockClusterInfo>()}; | ||
| std::unique_ptr<LoadBalancerBase> lb_; | ||
|
htuch marked this conversation as resolved.
|
||
|
|
||
| private: | ||
| // Untrusted upstreams don't have the ability to change the host set size, so keep it constant | ||
| // over the fuzz iteration. | ||
| void | ||
| initializeASingleHostSet(const test::common::upstream::SetupPriorityLevel& setup_priority_level, | ||
| const uint8_t priority_level); | ||
|
|
||
| // There are used to construct the priority set at the beginning of the fuzz iteration | ||
| uint16_t port_ = 80; | ||
|
zasweq marked this conversation as resolved.
Outdated
|
||
| uint8_t num_priority_levels_ = 0; | ||
|
|
||
| // This map used when updating health flags - making sure the health flags are updated hosts in | ||
| // localities Key - index of host within full host list, value - locality level host at index is | ||
| // in | ||
| absl::node_hash_map<uint8_t, uint8_t> locality_indexes_; | ||
|
zasweq marked this conversation as resolved.
|
||
| }; | ||
|
|
||
| } // namespace Upstream | ||
| } // namespace Envoy | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.