Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ if(ALTERNATOR_CLIENT_CPP_BUILD_TESTS)

set(ALTERNATOR_CLIENT_CPP_CORE_TEST_SOURCES
tests/attribute_value_test.cpp
tests/header_optimization_test.cpp
tests/http_client_test.cpp
tests/key_route_affinity_test.cpp
tests/live_nodes_integration_test.cpp
Expand Down
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,60 @@ Alternator node discovery or load balancing.

The AWS adapter uses the SDK's per-client `DynamoDBEndpointProviderBase` hook by default. AWS SDK for C++ resolves that endpoint once for a retried operation, so `DynamoDBHelper::ApplyToSDKOptions()` can also install a process-wide HTTP client factory before `Aws::InitAPI()`. That factory delegates to the SDK HTTP client and rotates only requests already aimed at the helper's Alternator endpoints.

The HTTP client factory runs after request signing. This is the closest public AWS SDK for C++ hook for retry-aware endpoint rewriting, but it means applications that depend on strict SigV4 host validation should prefer endpoint-provider routing or implement their own SDK wrapper. Automatic middleware features such as transparent header optimization and request-content-based endpoint rewriting are not available in the same form. The core library does provide deterministic key-route affinity primitives, and `DynamoDBHelper` exposes them for applications that integrate request-specific routing in their own AWS SDK wrapper.
The HTTP client factory runs after request signing. This is the closest public AWS SDK for C++ hook for retry-aware endpoint rewriting and header optimization, but it means applications that depend on strict SigV4 host validation should prefer endpoint-provider routing or implement their own SDK wrapper. Automatic request-content-based endpoint rewriting is not available in the same form. The core library does provide deterministic key-route affinity primitives, and `DynamoDBHelper` exposes them for applications that integrate request-specific routing in their own AWS SDK wrapper.

### Headers Optimization

Header optimization is disabled by default because `Config::header_optimization`
is null. Set it to a `HeaderOptimization` implementation to make
`DynamoDBHelper::ApplyToSDKOptions()` install an HTTP client factory that
removes DynamoDB request headers not used by Alternator before the request is
sent. The default allowlist keeps `Host`, `X-Amz-Target`, `Content-Length`,
`Accept-Encoding`, and `Content-Encoding`. When `Config::credentials` is set,
it also keeps `Authorization` and `X-Amz-Date`. When `Config::user_agent` is
set, it keeps `User-Agent`.

```cpp
scylladb::alternator::Config cfg;
cfg.header_optimization =
std::make_shared<scylladb::alternator::DefaultHeaderOptimization>();
```

Use `HeaderAllowlistOptimization` to replace the default allowlist:

```cpp
cfg.header_optimization =
std::make_shared<scylladb::alternator::HeaderAllowlistOptimization>(
std::vector<std::string>{
"Host",
"X-Amz-Target",
"Content-Length",
});
```

Header names are matched case-insensitively. The override is exact; if it is
set, the helper does not add credential or user-agent headers automatically.
Applications can provide their own implementation by deriving from
`HeaderOptimization`:

```cpp
class MyHeaderOptimization final : public scylladb::alternator::HeaderOptimization {
public:
std::vector<std::string> AllowedHeaders(
const scylladb::alternator::HeaderOptimizationContext& context) const override {
std::vector<std::string> headers = {"Host", "X-Amz-Target", "Content-Length"};
if (context.credentials_configured) {
headers.push_back("Authorization");
headers.push_back("X-Amz-Date");
}
return headers;
}
};
```

Header optimization applies only to requests routed to known Alternator
endpoints through the factory installed by `DynamoDBHelper::ApplyToSDKOptions()`
before `Aws::InitAPI()`.

### HTTP Response Compression

Expand Down
30 changes: 30 additions & 0 deletions include/scylladb/alternator/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,35 @@ class ZlibContentEncodingDecoder final : public HttpContentEncodingDecoder {
std::vector<std::string> accepted_response_encodings_;
};

struct HeaderOptimizationContext {
bool credentials_configured = false;
bool user_agent_configured = false;
};

class HeaderOptimization {
public:
virtual ~HeaderOptimization() = default;

[[nodiscard]] virtual std::vector<std::string> AllowedHeaders(
const HeaderOptimizationContext& context) const = 0;
};

class DefaultHeaderOptimization final : public HeaderOptimization {
public:
[[nodiscard]] std::vector<std::string> AllowedHeaders(
const HeaderOptimizationContext& context) const override;
};

class HeaderAllowlistOptimization final : public HeaderOptimization {
public:
explicit HeaderAllowlistOptimization(std::vector<std::string> allowed_headers);

[[nodiscard]] std::vector<std::string> AllowedHeaders(
const HeaderOptimizationContext& context) const override;
private:
std::vector<std::string> allowed_headers_;
};

struct Config {
std::uint16_t port = 8080;
std::string scheme = "http";
Expand All @@ -60,6 +89,7 @@ struct Config {
bool reuse_discovery_connections = true;
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders;
std::string user_agent = "scylladb-alternator-client-cpp/devel";
std::shared_ptr<HeaderOptimization> header_optimization;

NodeHealthStoreConfig node_health;
KeyRouteAffinityConfig key_route_affinity;
Expand Down
73 changes: 66 additions & 7 deletions src/aws_dynamodb_helper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ namespace {
constexpr char kAllocationTag[] = "ScyllaDBAlternatorAwsAdapter";
constexpr char kEndpointOverrideHost[] = "dynamodb.fake.alternator.cluster.node";

struct HeaderOptimizationPolicy {
bool enabled = false;
std::set<std::string> allowed_headers;
};

Aws::Http::Scheme AwsScheme(const std::string& scheme) {
return scheme == "https" ? Aws::Http::Scheme::HTTPS : Aws::Http::Scheme::HTTP;
}
Expand Down Expand Up @@ -184,6 +189,46 @@ bool IsFirstSdkAttempt(const Aws::Http::HttpRequest& request) {
return value_pos < header.size() && header[value_pos] == '1';
}

std::set<std::string> NormalizeHeaderAllowlist(const std::vector<std::string>& headers) {
std::set<std::string> allowlist;
for (const auto& header : headers) {
allowlist.insert(detail::ToLowerAscii(header));
}
return allowlist;
}

HeaderOptimizationContext HeaderOptimizationContextFromConfig(const Config& config) {
return {
!config.credentials.access_key_id.empty() || !config.credentials.secret_access_key.empty(),
!config.user_agent.empty(),
};
}

HeaderOptimizationPolicy BuildHeaderOptimizationPolicy(const Config& config) {
if (!config.header_optimization) {
return {};
}

const auto headers = config.header_optimization->AllowedHeaders(
HeaderOptimizationContextFromConfig(config));
return {true, NormalizeHeaderAllowlist(headers)};
}

void OptimizeRequestHeaders(const std::shared_ptr<Aws::Http::HttpRequest>& request,
const HeaderOptimizationPolicy& policy) {
if (!request || !policy.enabled) {
return;
}

const auto headers = request->GetHeaders();
for (const auto& header : headers) {
const auto header_name = detail::ToLowerAscii(std::string(header.first.c_str()));
if (policy.allowed_headers.find(header_name) == policy.allowed_headers.end()) {
request->DeleteHeader(header.first.c_str());
}
}
}

Url SelectAttemptNode(const std::shared_ptr<Aws::Http::HttpRequest>& request,
const std::shared_ptr<AlternatorLiveNodes>& nodes,
std::uint16_t endpoint_override_port) {
Expand All @@ -208,11 +253,13 @@ class AlternatorHttpClient final : public Aws::Http::HttpClient {
AlternatorHttpClient(std::shared_ptr<AlternatorLiveNodes> nodes,
std::uint16_t endpoint_override_port,
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders,
HeaderOptimizationPolicy header_optimization,
std::shared_ptr<Aws::Http::HttpClient> delegate)
: nodes_(std::move(nodes))
, endpoint_override_port_(endpoint_override_port)
, content_encoding_decoders_(std::move(content_encoding_decoders))
, accept_encoding_value_(detail::BuildAcceptEncodingValue(content_encoding_decoders_))
, header_optimization_(std::move(header_optimization))
, delegate_(std::move(delegate)) {
if (!nodes_) {
throw std::invalid_argument("nodes must not be null");
Expand All @@ -230,6 +277,9 @@ class AlternatorHttpClient final : public Aws::Http::HttpClient {
if (!node.Empty() && !accept_encoding_value_.empty()) {
request->SetAcceptEncoding(Aws::String(accept_encoding_value_.c_str()));
}
if (!node.Empty()) {
OptimizeRequestHeaders(request, header_optimization_);
}

auto response = delegate_->MakeRequest(request, read_limiter, write_limiter);
if (!node.Empty()) {
Expand All @@ -253,17 +303,20 @@ class AlternatorHttpClient final : public Aws::Http::HttpClient {
std::uint16_t endpoint_override_port_ = 0;
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders_;
std::string accept_encoding_value_;
HeaderOptimizationPolicy header_optimization_;
std::shared_ptr<Aws::Http::HttpClient> delegate_;
};

class AlternatorHttpClientFactory final : public Aws::Http::HttpClientFactory {
public:
AlternatorHttpClientFactory(std::shared_ptr<AlternatorLiveNodes> nodes,
std::uint16_t endpoint_override_port,
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders)
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders,
HeaderOptimizationPolicy header_optimization)
: nodes_(std::move(nodes))
, endpoint_override_port_(endpoint_override_port)
, content_encoding_decoders_(std::move(content_encoding_decoders)) {
, content_encoding_decoders_(std::move(content_encoding_decoders))
, header_optimization_(std::move(header_optimization)) {
if (!nodes_) {
throw std::invalid_argument("nodes must not be null");
}
Expand All @@ -281,6 +334,7 @@ class AlternatorHttpClientFactory final : public Aws::Http::HttpClientFactory {
nodes_,
endpoint_override_port_,
content_encoding_decoders_,
header_optimization_,
std::move(delegate));
}

Expand Down Expand Up @@ -315,6 +369,7 @@ class AlternatorHttpClientFactory final : public Aws::Http::HttpClientFactory {
std::shared_ptr<AlternatorLiveNodes> nodes_;
std::uint16_t endpoint_override_port_ = 0;
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders_;
HeaderOptimizationPolicy header_optimization_;
};

std::string EndpointOverride(std::uint16_t port, const std::string& scheme) {
Expand All @@ -324,12 +379,14 @@ std::string EndpointOverride(std::uint16_t port, const std::string& scheme) {
std::shared_ptr<Aws::Http::HttpClientFactory> NewAlternatorHttpClientFactory(
std::shared_ptr<AlternatorLiveNodes> nodes,
std::uint16_t endpoint_override_port,
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders) {
std::vector<std::shared_ptr<HttpContentEncodingDecoder>> content_encoding_decoders,
HeaderOptimizationPolicy header_optimization) {
return Aws::MakeShared<AlternatorHttpClientFactory>(
kAllocationTag,
std::move(nodes),
endpoint_override_port,
std::move(content_encoding_decoders));
std::move(content_encoding_decoders),
std::move(header_optimization));
}

class FixedEndpointProvider final : public Aws::DynamoDB::Endpoint::DynamoDBEndpointProviderBase {
Expand Down Expand Up @@ -741,7 +798,8 @@ std::shared_ptr<Aws::Http::HttpClientFactory> DynamoDBHelper::NewHttpClientFacto
return NewAlternatorHttpClientFactory(
nodes_,
config_.port,
config_.content_encoding_decoders);
config_.content_encoding_decoders,
BuildHeaderOptimizationPolicy(config_));
}

Aws::DynamoDB::DynamoDBClientConfiguration DynamoDBHelper::NewClientConfiguration() const {
Expand Down Expand Up @@ -772,8 +830,9 @@ void DynamoDBHelper::ApplyToSDKOptions(Aws::SDKOptions& options) const {
auto nodes = nodes_;
const auto port = config_.port;
const auto content_encoding_decoders = config_.content_encoding_decoders;
options.httpOptions.httpClientFactory_create_fn = [nodes, port, content_encoding_decoders] {
return NewAlternatorHttpClientFactory(nodes, port, content_encoding_decoders);
const auto header_optimization = BuildHeaderOptimizationPolicy(config_);
options.httpOptions.httpClientFactory_create_fn = [nodes, port, content_encoding_decoders, header_optimization] {
return NewAlternatorHttpClientFactory(nodes, port, content_encoding_decoders, header_optimization);
};
}

Expand Down
28 changes: 28 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,37 @@
#include "http_compression.h"

#include <stdexcept>
#include <utility>

namespace scylladb::alternator {

std::vector<std::string> DefaultHeaderOptimization::AllowedHeaders(
const HeaderOptimizationContext& context) const {
std::vector<std::string> headers = {
"Host",
"X-Amz-Target",
"Content-Length",
"Accept-Encoding",
"Content-Encoding",
};
if (context.credentials_configured) {
headers.push_back("Authorization");
headers.push_back("X-Amz-Date");
}
if (context.user_agent_configured) {
headers.push_back("User-Agent");
}
return headers;
}

HeaderAllowlistOptimization::HeaderAllowlistOptimization(std::vector<std::string> allowed_headers)
: allowed_headers_(std::move(allowed_headers)) {}

std::vector<std::string> HeaderAllowlistOptimization::AllowedHeaders(
const HeaderOptimizationContext&) const {
return allowed_headers_;
}

void ValidateConfig(const Config& config) {
if (config.scheme != "http" && config.scheme != "https") {
throw std::invalid_argument("scheme must be http or https");
Expand Down
Loading
Loading