Skip to content

Commit

Permalink
ALPN H2 Support for Netty Client (#5794)
Browse files Browse the repository at this point in the history
* ALPN H2 support in Netty client

* Add codegen customizations to skip ALPN for existing H2 services

* Add changelog

* Add benchmarks

* Add tests

* Update test

* Update test

* Address comments

* Update test file name

* Revert to checking Java version for ALPN support

* Update Kinesis integ tests

* Propagate exception and close channel

* Add tests and update ALPN support check for OpenSsl

* Set max streams when completing protocol future

* Add test dependencies

* Do not use FATAL_ALERT if OpenSSL is used

* Remove completing future in ALPN handler

* Remove import

* Update ALPN support check to check for getApplicationProtocol method in SSLEngine

* Address comments

* Use Lazy for alpn support check
  • Loading branch information
davidh44 authored Jan 22, 2025
1 parent 7f9dcdd commit 62be0c1
Show file tree
Hide file tree
Showing 40 changed files with 1,673 additions and 276 deletions.
6 changes: 6 additions & 0 deletions .changes/next-release/feature-AWSSDKforJavav2-abb9c7e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "feature",
"category": "Netty NIO HTTP Client",
"contributor": "",
"description": "Adds ALPN H2 support for Netty client"
}
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ public class CustomizationConfig {
*/
private boolean generateEndpointClientTests;

/**
* Whether to use prior knowledge protocol negotiation for H2
*/
private boolean usePriorKnowledgeForH2;

/**
* A mapping from the skipped test's description to the reason why it's being skipped.
*/
Expand Down Expand Up @@ -746,6 +751,14 @@ public void setGenerateEndpointClientTests(boolean generateEndpointClientTests)
this.generateEndpointClientTests = generateEndpointClientTests;
}

public boolean isUsePriorKnowledgeForH2() {
return usePriorKnowledgeForH2;
}

public void setUsePriorKnowledgeForH2(boolean usePriorKnowledgeForH2) {
this.usePriorKnowledgeForH2 = usePriorKnowledgeForH2;
}

public boolean useGlobalEndpoint() {
return useGlobalEndpoint;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.signer.Signer;
import software.amazon.awssdk.http.Protocol;
import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
import software.amazon.awssdk.identity.spi.IdentityProvider;
Expand Down Expand Up @@ -718,22 +719,25 @@ private MethodSpec beanStyleSetServiceConfigurationMethod() {
private void addServiceHttpConfigIfNeeded(TypeSpec.Builder builder, IntermediateModel model) {
String serviceDefaultFqcn = model.getCustomizationConfig().getServiceSpecificHttpConfig();
boolean supportsH2 = model.getMetadata().supportsH2();
boolean usePriorKnowledgeForH2 = model.getCustomizationConfig().isUsePriorKnowledgeForH2();

if (serviceDefaultFqcn != null || supportsH2) {
builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2));
builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2));
}
}

private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2) {
private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2,
boolean usePriorKnowledgeForH2) {
return MethodSpec.methodBuilder("serviceHttpConfig")
.addAnnotation(Override.class)
.addModifiers(PROTECTED, FINAL)
.returns(AttributeMap.class)
.addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2))
.addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2))
.build();
}

private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2) {
private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2,
boolean usePriorKnowledgeForH2) {
CodeBlock.Builder builder = CodeBlock.builder();

if (serviceDefaultFqcn != null) {
Expand All @@ -745,10 +749,16 @@ private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn,
}

if (supportsH2) {
builder.addStatement("return result.merge(AttributeMap.builder()"
+ ".put($T.PROTOCOL, $T.HTTP2)"
+ ".build())",
SdkHttpConfigurationOption.class, Protocol.class);
builder.add("return result.merge(AttributeMap.builder()"
+ ".put($T.PROTOCOL, $T.HTTP2)",
SdkHttpConfigurationOption.class, Protocol.class);

if (!usePriorKnowledgeForH2) {
builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)",
SdkHttpConfigurationOption.class, ProtocolNegotiation.class);
}

builder.addStatement(".build())");
} else {
builder.addStatement("return result");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,36 @@ public static IntermediateModel serviceWithNoAuth() {
return new IntermediateModelBuilder(models).build();
}

public static IntermediateModel serviceWithH2() {
File serviceModel =
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2/service-2.json").getFile());
File customizationModel =
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2/customization.config")
.getFile());
C2jModels models = C2jModels
.builder()
.serviceModel(getServiceModel(serviceModel))
.customizationConfig(getCustomizationConfig(customizationModel))
.build();

return new IntermediateModelBuilder(models).build();
}

public static IntermediateModel serviceWithH2UsePriorKnowledgeForH2() {
File serviceModel =
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2-usePriorKnowledgeForH2/service-2.json").getFile());
File customizationModel =
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2-usePriorKnowledgeForH2/customization.config")
.getFile());
C2jModels models = C2jModels
.builder()
.serviceModel(getServiceModel(serviceModel))
.customizationConfig(getCustomizationConfig(customizationModel))
.build();

return new IntermediateModelBuilder(models).build();
}

public static IntermediateModel serviceMiniS3() {
File serviceModel =
new File(ClientTestModels.class.getResource("client/c2j/mini-s3/service-2.json").getFile());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import static software.amazon.awssdk.codegen.poet.ClientTestModels.queryServiceModels;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.queryServiceModelsEndpointAuthParamsWithAllowList;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.restJsonServiceModels;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithH2;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithH2UsePriorKnowledgeForH2;
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithNoAuth;
import static software.amazon.awssdk.codegen.poet.builder.BuilderClassTestUtils.validateGeneration;

Expand Down Expand Up @@ -117,6 +119,16 @@ void syncComposedDefaultClientBuilderClass_sra() {
"test-composed-sync-default-client-builder.java", true);
}

@Test
void baseClientBuilderClassWithH2() {
validateBaseClientBuilderClassGeneration(serviceWithH2(), "test-h2-service-client-builder-class.java");
}

@Test
void baseClientBuilderClassWithH2_usePriorKnowledgeForH2() {
validateBaseClientBuilderClassGeneration(serviceWithH2UsePriorKnowledgeForH2(), "test-h2-usePriorKnowledgeForH2-service-client-builder-class.java");
}

private void validateBaseClientBuilderClassGeneration(IntermediateModel model, String expectedClassName) {
validateBaseClientBuilderClassGeneration(model, expectedClassName, false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package software.amazon.awssdk.services.h2;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import software.amazon.awssdk.annotations.Generated;
import software.amazon.awssdk.annotations.SdkInternalApi;
import software.amazon.awssdk.auth.signer.Aws4Signer;
import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder;
import software.amazon.awssdk.awscore.client.config.AwsClientOption;
import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider;
import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
import software.amazon.awssdk.core.SdkPlugin;
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
import software.amazon.awssdk.core.client.config.SdkClientOption;
import software.amazon.awssdk.core.interceptor.ClasspathInterceptorChainFactory;
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
import software.amazon.awssdk.core.retry.RetryMode;
import software.amazon.awssdk.core.signer.Signer;
import software.amazon.awssdk.http.Protocol;
import software.amazon.awssdk.http.ProtocolNegotiation;
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
import software.amazon.awssdk.identity.spi.IdentityProvider;
import software.amazon.awssdk.identity.spi.IdentityProviders;
import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption;
import software.amazon.awssdk.retries.api.RetryStrategy;
import software.amazon.awssdk.services.h2.endpoints.H2EndpointProvider;
import software.amazon.awssdk.services.h2.endpoints.internal.H2RequestSetEndpointInterceptor;
import software.amazon.awssdk.services.h2.endpoints.internal.H2ResolveEndpointInterceptor;
import software.amazon.awssdk.services.h2.internal.H2ServiceClientConfigurationBuilder;
import software.amazon.awssdk.utils.AttributeMap;
import software.amazon.awssdk.utils.CollectionUtils;
import software.amazon.awssdk.utils.Validate;

/**
* Internal base class for {@link DefaultH2ClientBuilder} and {@link DefaultH2AsyncClientBuilder}.
*/
@Generated("software.amazon.awssdk:codegen")
@SdkInternalApi
abstract class DefaultH2BaseClientBuilder<B extends H2BaseClientBuilder<B, C>, C> extends AwsDefaultClientBuilder<B, C> {
@Override
protected final String serviceEndpointPrefix() {
return "h2-service";
}

@Override
protected final String serviceName() {
return "H2";
}

@Override
protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) {
return config.merge(c -> c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider())
.option(SdkAdvancedClientOption.SIGNER, defaultSigner())
.option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false));
}

@Override
protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) {
List<ExecutionInterceptor> endpointInterceptors = new ArrayList<>();
endpointInterceptors.add(new H2ResolveEndpointInterceptor());
endpointInterceptors.add(new H2RequestSetEndpointInterceptor());
ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory();
List<ExecutionInterceptor> interceptors = interceptorFactory
.getInterceptors("software/amazon/awssdk/services/h2/execution.interceptors");
List<ExecutionInterceptor> additionalInterceptors = new ArrayList<>();
interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors);
interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors);
interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS));
SdkClientConfiguration.Builder builder = config.toBuilder();
builder.lazyOption(SdkClientOption.IDENTITY_PROVIDERS, c -> {
IdentityProviders.Builder result = IdentityProviders.builder();
IdentityProvider<?> credentialsIdentityProvider = c.get(AwsClientOption.CREDENTIALS_IDENTITY_PROVIDER);
if (credentialsIdentityProvider != null) {
result.putIdentityProvider(credentialsIdentityProvider);
}
return result.build();
});
builder.option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors);
builder.lazyOptionIfAbsent(
SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
c -> AwsClientEndpointProvider
.builder()
.serviceEndpointOverrideEnvironmentVariable("AWS_ENDPOINT_URL_H2_SERVICE")
.serviceEndpointOverrideSystemProperty("aws.endpointUrlH2")
.serviceProfileProperty("h2_service")
.serviceEndpointPrefix(serviceEndpointPrefix())
.defaultProtocol("https")
.region(c.get(AwsClientOption.AWS_REGION))
.profileFile(c.get(SdkClientOption.PROFILE_FILE_SUPPLIER))
.profileName(c.get(SdkClientOption.PROFILE_NAME))
.putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT,
c.get(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT))
.dualstackEnabled(c.get(AwsClientOption.DUALSTACK_ENDPOINT_ENABLED))
.fipsEnabled(c.get(AwsClientOption.FIPS_ENDPOINT_ENABLED)).build());
return builder.build();
}

private Signer defaultSigner() {
return Aws4Signer.create();
}

@Override
protected final String signingName() {
return "h2-service";
}

private H2EndpointProvider defaultEndpointProvider() {
return H2EndpointProvider.defaultProvider();
}

@Override
protected final AttributeMap serviceHttpConfig() {
AttributeMap result = AttributeMap.empty();
return result.merge(AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2)
.put(SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION, ProtocolNegotiation.ALPN).build());
}

@Override
protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) {
List<SdkPlugin> internalPlugins = internalPlugins(config);
List<SdkPlugin> externalPlugins = plugins();
if (internalPlugins.isEmpty() && externalPlugins.isEmpty()) {
return config;
}
List<SdkPlugin> plugins = CollectionUtils.mergeLists(internalPlugins, externalPlugins);
SdkClientConfiguration.Builder configuration = config.toBuilder();
H2ServiceClientConfigurationBuilder serviceConfigBuilder = new H2ServiceClientConfigurationBuilder(configuration);
for (SdkPlugin plugin : plugins) {
plugin.configureClient(serviceConfigBuilder);
}
updateRetryStrategyClientConfiguration(configuration);
return configuration.build();
}

private void updateRetryStrategyClientConfiguration(SdkClientConfiguration.Builder configuration) {
ClientOverrideConfiguration.Builder builder = configuration.asOverrideConfigurationBuilder();
RetryMode retryMode = builder.retryMode();
if (retryMode != null) {
configuration.option(SdkClientOption.RETRY_STRATEGY, AwsRetryStrategy.forRetryMode(retryMode));
} else {
Consumer<RetryStrategy.Builder<?, ?>> configurator = builder.retryStrategyConfigurator();
if (configurator != null) {
RetryStrategy.Builder<?, ?> defaultBuilder = AwsRetryStrategy.defaultRetryStrategy().toBuilder();
configurator.accept(defaultBuilder);
configuration.option(SdkClientOption.RETRY_STRATEGY, defaultBuilder.build());
} else {
RetryStrategy retryStrategy = builder.retryStrategy();
if (retryStrategy != null) {
configuration.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
}
}
}
configuration.option(SdkClientOption.CONFIGURED_RETRY_MODE, null);
configuration.option(SdkClientOption.CONFIGURED_RETRY_STRATEGY, null);
configuration.option(SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR, null);
}

private List<SdkPlugin> internalPlugins(SdkClientConfiguration config) {
return Collections.emptyList();
}

protected static void validateClientOptions(SdkClientConfiguration c) {
Validate.notNull(c.option(SdkAdvancedClientOption.SIGNER),
"The 'overrideConfiguration.advancedOption[SIGNER]' must be configured in the client builder.");
}
}
Loading

0 comments on commit 62be0c1

Please sign in to comment.