mirror of https://github.com/grpc/grpc.git
stateful session affinity: add end2end tests (#32009)
Co-authored-by: Mark D. Roth <roth@google.com>pull/32248/head
parent
b6cc7f0dba
commit
b168e9cc9d
8 changed files with 515 additions and 7 deletions
@ -0,0 +1,196 @@ |
||||
// Copyright 2023 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 <chrono> |
||||
#include <string> |
||||
#include <thread> |
||||
#include <vector> |
||||
|
||||
#include <gmock/gmock.h> |
||||
#include <gtest/gtest.h> |
||||
|
||||
#include "absl/strings/str_format.h" |
||||
#include "absl/strings/str_join.h" |
||||
#include "absl/strings/str_split.h" |
||||
|
||||
#include "src/core/ext/filters/client_channel/backup_poller.h" |
||||
#include "src/core/lib/gprpp/match.h" |
||||
#include "src/proto/grpc/testing/xds/v3/cluster.grpc.pb.h" |
||||
#include "src/proto/grpc/testing/xds/v3/outlier_detection.grpc.pb.h" |
||||
#include "src/proto/grpc/testing/xds/v3/router.grpc.pb.h" |
||||
#include "src/proto/grpc/testing/xds/v3/stateful_session.grpc.pb.h" |
||||
#include "src/proto/grpc/testing/xds/v3/stateful_session_cookie.pb.h" |
||||
#include "test/core/util/scoped_env_var.h" |
||||
#include "test/cpp/end2end/xds/xds_end2end_test_lib.h" |
||||
|
||||
namespace grpc { |
||||
namespace testing { |
||||
namespace { |
||||
using ::envoy::extensions::filters::http::stateful_session::v3::StatefulSession; |
||||
using ::envoy::extensions::filters::network::http_connection_manager::v3:: |
||||
HttpFilter; |
||||
using ::envoy::extensions::http::stateful_session::cookie::v3 :: |
||||
CookieBasedSessionState; |
||||
|
||||
constexpr absl::string_view kCookieName = "grpc_session_cookie"; |
||||
|
||||
class OverrideHostTest : public XdsEnd2endTest { |
||||
protected: |
||||
struct Cookie { |
||||
std::string value; |
||||
std::set<std::string> attributes; |
||||
std::string raw; |
||||
}; |
||||
|
||||
std::multimap<std::string, std::string> SendRpcGetServerMetadata( |
||||
const grpc_core::DebugLocation& debug_location, |
||||
const RpcOptions& rpc_options) { |
||||
std::multimap<std::string, std::string> server_initial_metadata; |
||||
EchoResponse response; |
||||
grpc::Status status = |
||||
SendRpc(rpc_options, &response, &server_initial_metadata); |
||||
EXPECT_TRUE(status.ok()) |
||||
<< "code=" << status.error_code() |
||||
<< ", message=" << status.error_message() << "\n" |
||||
<< debug_location.file() << ":" << debug_location.line(); |
||||
EXPECT_EQ(response.message(), kRequestMessage) |
||||
<< debug_location.file() << ":" << debug_location.line(); |
||||
return server_initial_metadata; |
||||
} |
||||
|
||||
static absl::optional<Cookie> ParseCookie(absl::string_view header, |
||||
absl::string_view cookie_name) { |
||||
std::pair<absl::string_view, absl::string_view> name_value = |
||||
absl::StrSplit(header, absl::MaxSplits('=', 1)); |
||||
if (name_value.first.empty() || name_value.first != cookie_name) { |
||||
return absl::nullopt; |
||||
} |
||||
std::pair<absl::string_view, absl::string_view> value_attrs = |
||||
absl::StrSplit(name_value.second, absl::MaxSplits(';', 1)); |
||||
std::set<std::string> attributes; |
||||
for (absl::string_view segment : absl::StrSplit(name_value.second, ';')) { |
||||
attributes.emplace(absl::StripAsciiWhitespace(segment)); |
||||
} |
||||
return Cookie({std::string(value_attrs.first), std::move(attributes), |
||||
std::string(name_value.second)}); |
||||
} |
||||
|
||||
static std::vector<std::string> GetStatefulSessionCookie( |
||||
const std::multimap<std::string, std::string>& server_initial_metadata, |
||||
absl::string_view cookie_name = kCookieName) { |
||||
std::vector<std::string> values; |
||||
auto pair = server_initial_metadata.equal_range("set-cookie"); |
||||
for (auto it = pair.first; it != pair.second; ++it) { |
||||
auto cookie = ParseCookie(it->second, cookie_name); |
||||
if (!cookie.has_value()) { |
||||
continue; |
||||
} |
||||
EXPECT_FALSE(cookie->value.empty()); |
||||
EXPECT_THAT(cookie->attributes, ::testing::Contains("HttpOnly")); |
||||
values.emplace_back(cookie->value); |
||||
} |
||||
return values; |
||||
} |
||||
|
||||
// Builds a Listener with Fault Injection filter config. If the http_fault
|
||||
// is nullptr, then assign an empty filter config. This filter config is
|
||||
// required to enable the fault injection features.
|
||||
static Listener BuildListenerWithStatefulSessionFilter() { |
||||
CookieBasedSessionState cookie_state; |
||||
cookie_state.mutable_cookie()->set_name(std::string(kCookieName)); |
||||
StatefulSession stateful_session; |
||||
stateful_session.mutable_session_state()->mutable_typed_config()->PackFrom( |
||||
cookie_state); |
||||
HttpConnectionManager http_connection_manager; |
||||
Listener listener; |
||||
listener.set_name(kServerName); |
||||
HttpFilter* session_filter = http_connection_manager.add_http_filters(); |
||||
session_filter->set_name("envoy.stateful_session"); |
||||
session_filter->mutable_typed_config()->PackFrom(stateful_session); |
||||
HttpFilter* router_filter = http_connection_manager.add_http_filters(); |
||||
router_filter->set_name("router"); |
||||
router_filter->mutable_typed_config()->PackFrom( |
||||
envoy::extensions::filters::http::router::v3::Router()); |
||||
listener.mutable_api_listener()->mutable_api_listener()->PackFrom( |
||||
http_connection_manager); |
||||
return listener; |
||||
} |
||||
}; |
||||
|
||||
INSTANTIATE_TEST_SUITE_P(XdsTest, OverrideHostTest, |
||||
::testing::Values(XdsTestType()), &XdsTestType::Name); |
||||
|
||||
TEST_P(OverrideHostTest, HappyPath) { |
||||
CreateAndStartBackends(2); |
||||
SetListenerAndRouteConfiguration(balancer_.get(), |
||||
BuildListenerWithStatefulSessionFilter(), |
||||
default_route_config_); |
||||
EdsResourceArgs args({{"locality0", CreateEndpointsForBackends()}}); |
||||
balancer_->ads_service()->SetEdsResource(BuildEdsResource(args)); |
||||
WaitForAllBackends(DEBUG_LOCATION); |
||||
// First call gets the cookie. RR policy picks the backend we will use.
|
||||
auto server_initial_metadata = |
||||
SendRpcGetServerMetadata(DEBUG_LOCATION, RpcOptions()); |
||||
std::vector<std::string> stateful_session_cookie = |
||||
GetStatefulSessionCookie(server_initial_metadata); |
||||
ASSERT_EQ(stateful_session_cookie.size(), 1); |
||||
size_t backend_idx = -1; |
||||
for (size_t i = 0; i < backends_.size(); ++i) { |
||||
if (backends_[i]->backend_service()->request_count() == 1) { |
||||
backend_idx = i; |
||||
break; |
||||
} |
||||
} |
||||
ASSERT_NE(-1, backend_idx); |
||||
ResetBackendCounters(); |
||||
std::vector<std::pair<std::string, std::string>> session_cookie = { |
||||
{"cookie", |
||||
absl::StrFormat("%s=%s", kCookieName, stateful_session_cookie[0])}}; |
||||
// All requests go to the backend we specified
|
||||
CheckRpcSendOk(DEBUG_LOCATION, 5, RpcOptions().set_metadata(session_cookie)); |
||||
EXPECT_EQ(backends_[backend_idx]->backend_service()->request_count(), 5); |
||||
// Round-robin spreads the load
|
||||
ResetBackendCounters(); |
||||
CheckRpcSendOk(DEBUG_LOCATION, backends_.size() * 2); |
||||
EXPECT_EQ(2, backends_[0]->backend_service()->request_count()); |
||||
EXPECT_EQ(2, backends_[1]->backend_service()->request_count()); |
||||
// Call a different service with the same cookie
|
||||
ResetBackendCounters(); |
||||
CheckRpcSendOk(DEBUG_LOCATION, 5, |
||||
RpcOptions() |
||||
.set_metadata(session_cookie) |
||||
.set_rpc_service(RpcService::SERVICE_ECHO2)); |
||||
EXPECT_EQ(backends_[backend_idx]->backend_service2()->request_count(), 5); |
||||
} |
||||
} // namespace
|
||||
} // namespace testing
|
||||
} // namespace grpc
|
||||
|
||||
int main(int argc, char** argv) { |
||||
grpc_core::testing::ScopedExperimentalEnvVar env_var( |
||||
"GRPC_EXPERIMENTAL_XDS_ENABLE_OVERRIDE_HOST"); |
||||
grpc::testing::TestEnvironment env(&argc, argv); |
||||
::testing::InitGoogleTest(&argc, argv); |
||||
// Make the backup poller poll very frequently in order to pick up
|
||||
// updates from all the subchannels's FDs.
|
||||
GPR_GLOBAL_CONFIG_SET(grpc_client_channel_backup_poll_interval_ms, 1); |
||||
#if TARGET_OS_IPHONE |
||||
// Workaround Apple CFStream bug
|
||||
grpc_core::SetEnv("grpc_cfstream", "0"); |
||||
#endif |
||||
grpc_init(); |
||||
const auto result = RUN_ALL_TESTS(); |
||||
grpc_shutdown(); |
||||
return result; |
||||
} |
Loading…
Reference in new issue