diff --git a/test/common/upstream/BUILD b/test/common/upstream/BUILD index b6b19139a7b5..216896626c45 100644 --- a/test/common/upstream/BUILD +++ b/test/common/upstream/BUILD @@ -242,6 +242,54 @@ envoy_cc_test( ], ) +envoy_cc_test_library( + name = "load_balancer_fuzz_lib", + srcs = ["load_balancer_fuzz_base.cc"], + hdrs = ["load_balancer_fuzz_base.h"], + deps = [ + ":load_balancer_fuzz_proto_cc_proto", + ":utility_lib", + "//source/common/upstream:load_balancer_lib", + "//test/fuzz:random_lib", + "//test/mocks:common_lib", + "//test/mocks/runtime:runtime_mocks", + "//test/mocks/upstream:cluster_info_mocks", + "//test/mocks/upstream:host_set_mocks", + "//test/mocks/upstream:load_balancer_context_mock", + "//test/mocks/upstream:priority_set_mocks", + "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", + ], +) + +envoy_proto_library( + name = "load_balancer_fuzz_proto", + srcs = ["load_balancer_fuzz.proto"], + deps = [ + "//test/fuzz:common_proto", + "@envoy_api//envoy/config/cluster/v3:pkg", + ], +) + +envoy_proto_library( + name = "random_load_balancer_fuzz_proto", + srcs = ["random_load_balancer_fuzz.proto"], + deps = [ + "//test/common/upstream:load_balancer_fuzz_proto", + ], +) + +envoy_cc_fuzz_test( + name = "random_load_balancer_fuzz_test", + srcs = ["random_load_balancer_fuzz_test.cc"], + corpus = "//test/common/upstream:random_load_balancer_corpus", + deps = [ + ":load_balancer_fuzz_lib", + ":load_balancer_fuzz_proto_cc_proto", + ":random_load_balancer_fuzz_proto_cc_proto", + ":utility_lib", + ], +) + envoy_cc_test( name = "load_balancer_simulation_test", srcs = ["load_balancer_simulation_test.cc"], diff --git a/test/common/upstream/load_balancer_fuzz.proto b/test/common/upstream/load_balancer_fuzz.proto new file mode 100644 index 000000000000..c4b1ead2c7d0 --- /dev/null +++ b/test/common/upstream/load_balancer_fuzz.proto @@ -0,0 +1,60 @@ +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}]; +} + +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; + // 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]; + uint32 num_hosts_locality_a = 2 [(validate.rules).uint32.lte = 500]; + uint32 num_hosts_locality_b = 3 [(validate.rules).uint32.lte = 500]; + // Hard cap at 3 localities for simplicity + uint32 num_hosts_locality_c = 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}]; + + // 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]; +} diff --git a/test/common/upstream/load_balancer_fuzz_base.cc b/test/common/upstream/load_balancer_fuzz_base.cc new file mode 100644 index 000000000000..6741f95f7581 --- /dev/null +++ b/test/common/upstream/load_balancer_fuzz_base.cc @@ -0,0 +1,229 @@ +#include "test/common/upstream/load_balancer_fuzz_base.h" + +#include "test/common/upstream/utility.h" + +namespace Envoy { +namespace Upstream { + +namespace { +// TODO(zasweq): This will be relaxed in the future in order to fully represent the state space +// possible within Load Balancing. In it's current state, it is too slow (particularly due to calls +// to makeTestHost()) to scale up hosts. Once this is made more efficient, this number will be +// increased. +constexpr uint32_t MaxNumHostsPerPriorityLevel = 256; + +} // namespace + +void LoadBalancerFuzzBase::initializeASingleHostSet( + const test::common::upstream::SetupPriorityLevel& setup_priority_level, + const uint8_t priority_level, uint16_t& port) { + 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 at priority level {} with {} hosts.", + priority_level, 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; + } + + Fuzz::ProperSubsetSelector subset_selector(setup_priority_level.random_bytestring()); + + const std::vector> localities = subset_selector.constructSubsets( + {setup_priority_level.num_hosts_locality_a(), setup_priority_level.num_hosts_locality_b(), + setup_priority_level.num_hosts_locality_c()}, + host_set.hosts_.size()); + + HostVector locality_a = {}; + HostVector locality_b = {}; + HostVector locality_c = {}; + // Used to index into correct locality in iteration through subsets + std::array locality_indexes = {locality_a, locality_b, locality_c}; + + 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 {}: {}", locality + 1, + absl::StrJoin(localities.at(locality), " ")); + } + + host_set.hosts_per_locality_ = makeHostsPerLocality({locality_a, locality_b, locality_c}); +} + +// Initializes random and fixed host sets +void LoadBalancerFuzzBase::initializeLbComponents( + const test::common::upstream::LoadBalancerTestCase& input) { + random_.initializeSeed(input.seed_for_prng()); + uint16_t port = 80; + 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, port); + } + 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); + // Remove health flags from hosts with health flags set - previous iterations of this function + // with hosts placed degraded and excluded buckets + for (auto& host : host_set.degraded_hosts_) { + host->healthFlagClear(Host::HealthFlag::DEGRADED_ACTIVE_HC); + } + for (auto& host : host_set.excluded_hosts_) { + host->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + } + // This downcast will not overflow because size is capped by port numbers + 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(); + + enum HealthStatus { + HEALTHY = 0, + DEGRADED = 1, + EXCLUDED = 2, + }; + + Fuzz::ProperSubsetSelector subset_selector(random_bytestring); + + const std::vector> 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(HealthStatus::HEALTHY)) { + host_set.healthy_hosts_.push_back(host_set.hosts_[index]); + // No health flags for healthy + } + ENVOY_LOG_MISC(trace, "Hosts made healthy at priority level {}: {}", priority_of_host_set, + absl::StrJoin(subsets.at(HealthStatus::HEALTHY), " ")); + + // Degraded hosts are second subset + for (uint8_t index : subsets.at(HealthStatus::DEGRADED)) { + host_set.degraded_hosts_.push_back(host_set.hosts_[index]); + // Health flags are not currently directly used by most load balancers, but + // they may be added and also are used by other components. + // There are two health flags that map to Host::Health::Degraded, DEGRADED_ACTIVE_HC and + // DEGRADED_EDS_HEALTH. Choose one hardcoded for simplicity. + host_set.hosts_[index]->healthFlagSet(Host::HealthFlag::DEGRADED_ACTIVE_HC); + } + ENVOY_LOG_MISC(trace, "Hosts made degraded at priority level {}: {}", priority_of_host_set, + absl::StrJoin(subsets.at(HealthStatus::DEGRADED), " ")); + + // Excluded hosts are third subset + for (uint8_t index : subsets.at(HealthStatus::EXCLUDED)) { + host_set.excluded_hosts_.push_back(host_set.hosts_[index]); + // Health flags are not currently directly used by most load balancers, but + // they may be added and also are used by other components. + // There are three health flags that map to Host::Health::Degraded, FAILED_ACTIVE_HC, + // FAILED_OUTLIER_CHECK, and FAILED_EDS_HEALTH. Choose one hardcoded for simplicity. + host_set.hosts_[index]->healthFlagSet(Host::HealthFlag::FAILED_ACTIVE_HC); + } + ENVOY_LOG_MISC(trace, "Hosts made excluded at priority level {}: {}", priority_of_host_set, + absl::StrJoin(subsets.at(HealthStatus::EXCLUDED), " ")); + + // Handle updating health flags for hosts_per_locality_ + enum Locality { + A = 0, + B = 1, + C = 2, + }; + + // The index within the array of the vector represents the locality + std::array healthy_hosts_per_locality; + std::array degraded_hosts_per_locality; + std::array excluded_hosts_per_locality; + + // Wrap those three in an array here, where the index represents health status of + // healthy/degraded/excluded, used for indexing during iteration through subsets + std::array, 3> locality_health_statuses = { + healthy_hosts_per_locality, degraded_hosts_per_locality, excluded_hosts_per_locality}; + + // Iterate through subsets + for (uint8_t health_status = 0; health_status < locality_health_statuses.size(); + health_status++) { + for (uint8_t index : subsets.at(health_status)) { // Each subset logically represents a health + // status + // If the host is in a locality, we have to update the corresponding health status host vector + if (!(locality_indexes_.find(index) == locality_indexes_.end())) { + // After computing the host index subsets, we want to propagate these changes to a host set + // by building and using these host vectors + uint8_t locality = locality_indexes_[index]; + locality_health_statuses[health_status][locality].push_back(host_set.hosts_[index]); + ENVOY_LOG_MISC(trace, "Added host at index {} in locality {} to health status {}", index, + locality_indexes_[index] + 1, health_status + 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[Locality::A], healthy_hosts_per_locality[Locality::B], + healthy_hosts_per_locality[Locality::C]}); + host_set.degraded_hosts_per_locality_ = makeHostsPerLocality( + {degraded_hosts_per_locality[Locality::A], degraded_hosts_per_locality[Locality::B], + degraded_hosts_per_locality[Locality::C]}); + host_set.excluded_hosts_per_locality_ = makeHostsPerLocality( + {excluded_hosts_per_locality[Locality::A], excluded_hosts_per_locality[Locality::B], + excluded_hosts_per_locality[Locality::C]}); + + // These callbacks update load balancing data structures (callbacks are piped into priority set + // in LoadBalancerBase constructor) This won't have any hosts added or removed, as untrusted + // upstreams cannot do that. + host_set.runCallbacks({}, {}); +} + +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& 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 diff --git a/test/common/upstream/load_balancer_fuzz_base.h b/test/common/upstream/load_balancer_fuzz_base.h new file mode 100644 index 000000000000..deeb4c82c216 --- /dev/null +++ b/test/common/upstream/load_balancer_fuzz_base.h @@ -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& 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_; + Random::PsuedoRandomGenerator64 random_; + NiceMock priority_set_; + std::shared_ptr info_{new NiceMock()}; + std::unique_ptr lb_; + +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, uint16_t& port); + + // This is used to choose a host set to update the flags in an update flags event by modding a + // random uint64 against this number. + 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 locality_indexes_; +}; + +} // namespace Upstream +} // namespace Envoy diff --git a/test/common/upstream/random_load_balancer_corpus/random_256_ports b/test/common/upstream/random_load_balancer_corpus/random_256_ports new file mode 100644 index 000000000000..1924462a2ee7 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_256_ports @@ -0,0 +1,41 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 256 + random_bytestring: "\x01\x02\x03\x04\x45\x80" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 256 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 256 + random_bytestring: "\x01\x02" +} +seed_for_prng: 4 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_NoHosts b/test/common/upstream/random_load_balancer_corpus/random_NoHosts new file mode 100644 index 000000000000..551225e908e3 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_NoHosts @@ -0,0 +1,24 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 0 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 0 + random_bytestring: "\x01\x02" +} +seed_for_prng: 2 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_Normal b/test/common/upstream/random_load_balancer_corpus/random_Normal new file mode 100644 index 000000000000..61bb66f8638c --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_Normal @@ -0,0 +1,41 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 2 + random_bytestring: "\x01\x02" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 2 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 0 + random_bytestring: "\x01\x02" +} +seed_for_prng: 1 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_crash-ba5efdfd9c412a8507087120783fe6529b1ac0cb b/test/common/upstream/random_load_balancer_corpus/random_crash-ba5efdfd9c412a8507087120783fe6529b1ac0cb new file mode 100644 index 000000000000..602a393132bf --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_crash-ba5efdfd9c412a8507087120783fe6529b1ac0cb @@ -0,0 +1,38 @@ +load_balancer_test_case { +common_lb_config { + healthy_panic_threshold { + value: 2.12199579096527e-314 + } + locality_weighted_lb_config { + } +} +actions { + choose_host { + } +} +actions { + prefetch { + } +} +actions { + prefetch { + } +} +actions { + choose_host { + } +} +actions { + choose_host { + } +} +setup_priority_levels { + num_hosts_in_priority_level: 2 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 9007199259945536 + random_bytestring: "\x01\x02" +} +seed_for_prng: 6 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_largest-port-value b/test/common/upstream/random_load_balancer_corpus/random_largest-port-value new file mode 100644 index 000000000000..2f95ce787ef1 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_largest-port-value @@ -0,0 +1,34 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + choose_host { + } +} +actions { + prefetch { + } +} +actions { + prefetch { + } +} +actions { + choose_host { + } +} +actions { + choose_host { + } +} +nsetup_priority_levels { + num_hosts_in_priority_level: 65455 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 65455 + random_bytestring: "\x01\x02" +} +seed_for_prng: 5 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_many_choose_hosts b/test/common/upstream/random_load_balancer_corpus/random_many_choose_hosts new file mode 100644 index 000000000000..b263d07ec40e --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_many_choose_hosts @@ -0,0 +1,61 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 2 + random_bytestring: "\x01\x02\x03\x04" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 2 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 0 + random_bytestring: "\x01\x02" +} +seed_for_prng: 1 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_max_ports b/test/common/upstream/random_load_balancer_corpus/random_max_ports new file mode 100644 index 000000000000..4a7406d8b765 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_max_ports @@ -0,0 +1,41 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 2 + random_bytestring: "\x01\x02\x03\x04" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 32726 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 32726 + random_bytestring: "\x01\x02" +} +seed_for_prng: 88 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_overflowing_ports b/test/common/upstream/random_load_balancer_corpus/random_overflowing_ports new file mode 100644 index 000000000000..4598c29dbe10 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_overflowing_ports @@ -0,0 +1,41 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 2 + random_bytestring: "\x01\x02" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 60000 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 60000 + random_bytestring: "\x01\x02" +} +seed_for_prng: 4 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_slow-unit-eed4596101efb3e737f736c8d5bcd4f0815a8728 b/test/common/upstream/random_load_balancer_corpus/random_slow-unit-eed4596101efb3e737f736c8d5bcd4f0815a8728 new file mode 100644 index 000000000000..7bebf1a2cf96 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_slow-unit-eed4596101efb3e737f736c8d5bcd4f0815a8728 @@ -0,0 +1,40 @@ +load_balancer_test_case { + common_lb_config { + update_merge_window { + nanos: 512 + } + } + actions { + update_health_flags { + num_healthy_hosts: 2 + random_bytestring: "\001\002\003\004" + } + } + actions { + prefetch { + } + } + actions { + prefetch { + } + } + actions { + choose_host { + } + } + actions { + update_health_flags { + num_healthy_hosts: 2 + random_bytestring: "\001\002\003\004" + } + } + setup_priority_levels { + num_hosts_in_priority_level: 536903638 + random_bytestring: "\001\002" + } + setup_priority_levels { + num_hosts_in_priority_level: 32726 + random_bytestring: "\001\002" + } + seed_for_prng: 88 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_slow-unit-test b/test/common/upstream/random_load_balancer_corpus/random_slow-unit-test new file mode 100644 index 000000000000..e1f2fcfdd303 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_slow-unit-test @@ -0,0 +1,46 @@ +load_balancer_test_case { + common_lb_config { + update_merge_window { + nanos: 512 + } + } + actions { + update_health_flags { + num_healthy_hosts: 2 + random_bytestring: "\001\002\003\004" + } + } + actions { + prefetch { + } + } + actions { + prefetch { + } + } + actions { + choose_host { + } + } + actions { + update_health_flags { + num_healthy_hosts: 2 + random_bytestring: "\001\002\003\004" + } + } + setup_priority_levels { + num_hosts_in_priority_level: 500 + num_hosts_locality_one: 50 + num_hosts_locality_two: 50 + num_hosts_locality_three: 50 + random_bytestring: "\001\002" + } + setup_priority_levels { + num_hosts_in_priority_level: 500 + num_hosts_locality_one: 50 + num_hosts_locality_two: 50 + num_hosts_locality_three: 50 + random_bytestring: "\001\002" + } + seed_for_prng: 88 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_test_something b/test/common/upstream/random_load_balancer_corpus/random_test_something new file mode 100644 index 000000000000..172e2cb1c051 --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_test_something @@ -0,0 +1,41 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 2 + random_bytestring: "\x01\x02" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 250 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 250 + random_bytestring: "\x01\x02" +} +seed_for_prng: 4 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_timeout-6b0d6b83136a4cf0b9ccd468f11207a792859d43 b/test/common/upstream/random_load_balancer_corpus/random_timeout-6b0d6b83136a4cf0b9ccd468f11207a792859d43 new file mode 100644 index 000000000000..96f3d0efd72b --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_timeout-6b0d6b83136a4cf0b9ccd468f11207a792859d43 @@ -0,0 +1,31 @@ +load_balancer_test_case { + common_lb_config { + healthy_panic_threshold { + value: 1.35807730621777e-312 + } + zone_aware_lb_config { + routing_enabled { + } + min_cluster_size { + value: 7380836839843720192 + } + fail_traffic_on_panic: true + } + consistent_hashing_lb_config { + hash_balance_factor { + value: 27656 + } + } + } + actions { + update_health_flags { + num_excluded_hosts: 268435456 + random_bytestring: "\x01\x02" + } + } + setup_priority_levels { + num_hosts_in_priority_level: 13534154135 + random_bytestring: "\x01\x02" + } + seed_for_prng: 32 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_timeout-9144cfbb40b5101ecc28b205b10e6c36a72aae83 b/test/common/upstream/random_load_balancer_corpus/random_timeout-9144cfbb40b5101ecc28b205b10e6c36a72aae83 new file mode 100644 index 000000000000..2fca35ed475d --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_timeout-9144cfbb40b5101ecc28b205b10e6c36a72aae83 @@ -0,0 +1,25 @@ +load_balancer_test_case { + common_lb_config { + healthy_panic_threshold { + value: 4.88907830238399e-311 + } + consistent_hashing_lb_config { + use_hostname_for_hashing: true + hash_balance_factor { + value: 1024 + } + } + } + actions { + update_health_flags { + host_priority: 270582939648 + num_degraded_hosts: 4194304 + random_bytestring: "\x01\x02\x03\x04" + } + } + setup_priority_levels { + num_hosts_in_priority_level: 1024 + random_bytestring: "\x01\x02" + } + seed_for_prng: 62208 +} diff --git a/test/common/upstream/random_load_balancer_corpus/random_with-locality b/test/common/upstream/random_load_balancer_corpus/random_with-locality new file mode 100644 index 000000000000..3f2f1281845f --- /dev/null +++ b/test/common/upstream/random_load_balancer_corpus/random_with-locality @@ -0,0 +1,49 @@ +load_balancer_test_case { +common_lb_config { + +} +actions { + update_health_flags { + host_priority: 0 + num_healthy_hosts: 2 + num_degraded_hosts: 3 + num_excluded_hosts: 4 + random_bytestring: "\x01\x02\x03\x04\x05\x06" + } +} +actions { + prefetch { + + } +} +actions { + prefetch { + + } +} +actions { + choose_host { + + } +} +actions { + choose_host { + + } +} +setup_priority_levels { + num_hosts_in_priority_level: 20 + num_hosts_locality_a: 3 + num_hosts_locality_b: 4 + num_hosts_locality_c: 5 + random_bytestring: "\x01\x02" +} +setup_priority_levels { + num_hosts_in_priority_level: 20 + num_hosts_locality_a: 3 + num_hosts_locality_b: 4 + num_hosts_locality_c: 5 + random_bytestring: "\x01\x02" +} +seed_for_prng: 1 +} diff --git a/test/common/upstream/random_load_balancer_fuzz.proto b/test/common/upstream/random_load_balancer_fuzz.proto new file mode 100644 index 000000000000..ba277976d0fe --- /dev/null +++ b/test/common/upstream/random_load_balancer_fuzz.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package test.common.upstream; + +import "validate/validate.proto"; +import "test/common/upstream/load_balancer_fuzz.proto"; + +//This has no specific logic needed for initialization +message RandomLoadBalancerTestCase { + LoadBalancerTestCase load_balancer_test_case = 1 [(validate.rules).message.required = true]; +} diff --git a/test/common/upstream/random_load_balancer_fuzz_test.cc b/test/common/upstream/random_load_balancer_fuzz_test.cc new file mode 100644 index 000000000000..6d5700654989 --- /dev/null +++ b/test/common/upstream/random_load_balancer_fuzz_test.cc @@ -0,0 +1,36 @@ +#include + +#include "test/common/upstream/load_balancer_fuzz_base.h" +#include "test/common/upstream/random_load_balancer_fuzz.pb.validate.h" +#include "test/fuzz/fuzz_runner.h" +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Upstream { + +DEFINE_PROTO_FUZZER(const test::common::upstream::RandomLoadBalancerTestCase& input) { + try { + TestUtility::validate(input); + } catch (const ProtoValidationException& e) { + ENVOY_LOG_MISC(debug, "ProtoValidationException: {}", e.what()); + return; + } + + LoadBalancerFuzzBase load_balancer_fuzz = LoadBalancerFuzzBase(); + load_balancer_fuzz.initializeLbComponents(input.load_balancer_test_case()); + + try { + load_balancer_fuzz.lb_ = std::make_unique( + load_balancer_fuzz.priority_set_, nullptr, load_balancer_fuzz.stats_, + load_balancer_fuzz.runtime_, load_balancer_fuzz.random_, + input.load_balancer_test_case().common_lb_config()); + } catch (EnvoyException& e) { + ENVOY_LOG_MISC(debug, "EnvoyException; {}", e.what()); + return; + } + + load_balancer_fuzz.replay(input.load_balancer_test_case().actions()); +} + +} // namespace Upstream +} // namespace Envoy diff --git a/test/fuzz/BUILD b/test/fuzz/BUILD index 35bd8e0ac197..153f6f0a94f4 100644 --- a/test/fuzz/BUILD +++ b/test/fuzz/BUILD @@ -1,5 +1,6 @@ load( "//bazel:envoy_build_system.bzl", + "envoy_cc_test", "envoy_cc_test_library", "envoy_package", "envoy_proto_library", @@ -68,3 +69,21 @@ envoy_cc_test_library( "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) + +envoy_cc_test_library( + name = "random_lib", + hdrs = ["random.h"], + deps = [ + "//include/envoy/common:random_generator_interface", + "//source/common/common:assert_lib", + "//source/common/common:minimal_logger_lib", + ], +) + +envoy_cc_test( + name = "random_test", + srcs = ["random_test.cc"], + deps = [ + "//test/fuzz:random_lib", + ], +) diff --git a/test/fuzz/random.h b/test/fuzz/random.h new file mode 100644 index 000000000000..01b8f5e83df5 --- /dev/null +++ b/test/fuzz/random.h @@ -0,0 +1,104 @@ +#include +#include +#include +#include + +#include "envoy/common/random_generator.h" + +#include "common/common/assert.h" +#include "common/common/logger.h" + +namespace Envoy { +namespace Random { + +class PsuedoRandomGenerator64 : public RandomGenerator { +public: + PsuedoRandomGenerator64() = default; + ~PsuedoRandomGenerator64() override = default; + + void initializeSeed(uint64_t seed) { prng_ = std::make_unique(seed); } + + // RandomGenerator + uint64_t random() override { + // Makes sure initializeSeed() was already called + ASSERT(prng_ != nullptr); + const uint64_t to_return = (*prng_)(); + ENVOY_LOG_MISC(trace, "random() returned: {}", to_return); + return to_return; + } + std::string uuid() override { return ""; } + std::unique_ptr prng_; +}; + +} // namespace Random + +namespace Fuzz { +class ProperSubsetSelector { +public: + ProperSubsetSelector(const std::string& random_bytestring) + : random_bytestring_(random_bytestring) {} + + /** + * This function does proper subset selection on a certain number of elements. It returns a vector + * of vectors of bytes. Each vector of bytes represents the indexes of a single subset. The + * "randomness" of the subset that the class will use is determined by a bytestring passed into + * the class. Example: call into function with a vector {3, 5} representing subset sizes, and 15 + * as number_of_elements. This function would return something such as {{3, 14, 7}, {2, 1, 13, 8, + * 6}}. If the sum of the number of elements in each elements in each subset > number of elements, + * this will stop constructing subsets once the number of elements has ran out and been already + * placed into subsets. So, if you had a vector {3, 5} representing subset sizes, and 2 as number + * of elements, the function would return something such as {{5, 3}}. + */ + + std::vector> + constructSubsets(const std::vector& number_of_elements_in_each_subset, + uint32_t number_of_elements) { + num_elements_left_ = number_of_elements; + std::vector index_vector; + index_vector.reserve(number_of_elements); + for (uint32_t i = 0; i < number_of_elements; i++) { + index_vector.push_back(i); + } + std::vector> subsets; + subsets.reserve(number_of_elements_in_each_subset.size()); + for (uint32_t i : number_of_elements_in_each_subset) { + subsets.push_back(constructSubset(i, index_vector)); + } + return subsets; + } + +private: + // Builds a single subset by pulling indexes off index_vector_ + std::vector constructSubset(uint32_t number_of_elements_in_subset, + std::vector& index_vector) { + std::vector subset; + + for (uint32_t i = 0; i < number_of_elements_in_subset && !(num_elements_left_ == 0); i++) { + // Index of bytestring will wrap around if it "overflows" past the random bytestring's length. + uint64_t index_of_index_vector = + random_bytestring_[index_of_random_bytestring_ % random_bytestring_.length()] % + num_elements_left_; + const uint64_t index = index_vector.at(index_of_index_vector); + subset.push_back(index); + // Move the index chosen to the end of the vector - will not be chosen again + std::swap(index_vector[index_of_index_vector], index_vector[num_elements_left_ - 1]); + --num_elements_left_; + + ++index_of_random_bytestring_; + } + + return subset; + } + + // This bytestring will be iterated through representing randomness in order to choose + // subsets + const std::string random_bytestring_; + uint32_t index_of_random_bytestring_ = 0; + + // Used to make subset construction linear time complexity with std::swap - chosen indexes will be + // swapped to end of vector, and won't be chosen again due to modding against this integer + uint32_t num_elements_left_; +}; + +} // namespace Fuzz +} // namespace Envoy diff --git a/test/fuzz/random_test.cc b/test/fuzz/random_test.cc new file mode 100644 index 000000000000..9e7fd1012260 --- /dev/null +++ b/test/fuzz/random_test.cc @@ -0,0 +1,27 @@ +#include "test/fuzz/random.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +using testing::ContainerEq; + +namespace Envoy { +namespace Fuzz { + +// Test the subset selection - since selection is based on a passed in random bytestring you can +// work the algorithm yourself Pass in 5 elements, expect first subset to be element 2 and element +// 4, second subset to be elements 1, 2, 3 +TEST(BasicSubsetSelection, RandomTest) { + // \x01 chooses the first element, which gets swapped with last element, 0x3 chooses the third + // index, which gets swapped with last element etc. + std::string random_bytestring = "\x01\x03\x09\x04\x33"; + ProperSubsetSelector subset_selector(random_bytestring); + const std::vector> subsets = subset_selector.constructSubsets({2, 3}, 5); + const std::vector expected_subset_one = {1, 3}; + const std::vector expected_subset_two = {0, 2, 4}; + EXPECT_THAT(subsets[0], ContainerEq(expected_subset_one)); + EXPECT_THAT(subsets[1], ContainerEq(expected_subset_two)); +} + +} // namespace Fuzz +} // namespace Envoy