Skip to content

Commit 62be0c1

Browse files
authored
ALPN H2 Support for Netty Client (#5794)
* 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
1 parent 7f9dcdd commit 62be0c1

File tree

40 files changed

+1673
-276
lines changed

40 files changed

+1673
-276
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "feature",
3+
"category": "Netty NIO HTTP Client",
4+
"contributor": "",
5+
"description": "Adds ALPN H2 support for Netty client"
6+
}

codegen/src/main/java/software/amazon/awssdk/codegen/model/config/customization/CustomizationConfig.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,11 @@ public class CustomizationConfig {
258258
*/
259259
private boolean generateEndpointClientTests;
260260

261+
/**
262+
* Whether to use prior knowledge protocol negotiation for H2
263+
*/
264+
private boolean usePriorKnowledgeForH2;
265+
261266
/**
262267
* A mapping from the skipped test's description to the reason why it's being skipped.
263268
*/
@@ -746,6 +751,14 @@ public void setGenerateEndpointClientTests(boolean generateEndpointClientTests)
746751
this.generateEndpointClientTests = generateEndpointClientTests;
747752
}
748753

754+
public boolean isUsePriorKnowledgeForH2() {
755+
return usePriorKnowledgeForH2;
756+
}
757+
758+
public void setUsePriorKnowledgeForH2(boolean usePriorKnowledgeForH2) {
759+
this.usePriorKnowledgeForH2 = usePriorKnowledgeForH2;
760+
}
761+
749762
public boolean useGlobalEndpoint() {
750763
return useGlobalEndpoint;
751764
}

codegen/src/main/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClass.java

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
import software.amazon.awssdk.core.retry.RetryMode;
7575
import software.amazon.awssdk.core.signer.Signer;
7676
import software.amazon.awssdk.http.Protocol;
77+
import software.amazon.awssdk.http.ProtocolNegotiation;
7778
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
7879
import software.amazon.awssdk.http.auth.spi.scheme.AuthScheme;
7980
import software.amazon.awssdk.identity.spi.IdentityProvider;
@@ -718,22 +719,25 @@ private MethodSpec beanStyleSetServiceConfigurationMethod() {
718719
private void addServiceHttpConfigIfNeeded(TypeSpec.Builder builder, IntermediateModel model) {
719720
String serviceDefaultFqcn = model.getCustomizationConfig().getServiceSpecificHttpConfig();
720721
boolean supportsH2 = model.getMetadata().supportsH2();
722+
boolean usePriorKnowledgeForH2 = model.getCustomizationConfig().isUsePriorKnowledgeForH2();
721723

722724
if (serviceDefaultFqcn != null || supportsH2) {
723-
builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2));
725+
builder.addMethod(serviceSpecificHttpConfigMethod(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2));
724726
}
725727
}
726728

727-
private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2) {
729+
private MethodSpec serviceSpecificHttpConfigMethod(String serviceDefaultFqcn, boolean supportsH2,
730+
boolean usePriorKnowledgeForH2) {
728731
return MethodSpec.methodBuilder("serviceHttpConfig")
729732
.addAnnotation(Override.class)
730733
.addModifiers(PROTECTED, FINAL)
731734
.returns(AttributeMap.class)
732-
.addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2))
735+
.addCode(serviceSpecificHttpConfigMethodBody(serviceDefaultFqcn, supportsH2, usePriorKnowledgeForH2))
733736
.build();
734737
}
735738

736-
private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2) {
739+
private CodeBlock serviceSpecificHttpConfigMethodBody(String serviceDefaultFqcn, boolean supportsH2,
740+
boolean usePriorKnowledgeForH2) {
737741
CodeBlock.Builder builder = CodeBlock.builder();
738742

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

747751
if (supportsH2) {
748-
builder.addStatement("return result.merge(AttributeMap.builder()"
749-
+ ".put($T.PROTOCOL, $T.HTTP2)"
750-
+ ".build())",
751-
SdkHttpConfigurationOption.class, Protocol.class);
752+
builder.add("return result.merge(AttributeMap.builder()"
753+
+ ".put($T.PROTOCOL, $T.HTTP2)",
754+
SdkHttpConfigurationOption.class, Protocol.class);
755+
756+
if (!usePriorKnowledgeForH2) {
757+
builder.add(".put($T.PROTOCOL_NEGOTIATION, $T.ALPN)",
758+
SdkHttpConfigurationOption.class, ProtocolNegotiation.class);
759+
}
760+
761+
builder.addStatement(".build())");
752762
} else {
753763
builder.addStatement("return result");
754764
}

codegen/src/test/java/software/amazon/awssdk/codegen/poet/ClientTestModels.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,36 @@ public static IntermediateModel serviceWithNoAuth() {
320320
return new IntermediateModelBuilder(models).build();
321321
}
322322

323+
public static IntermediateModel serviceWithH2() {
324+
File serviceModel =
325+
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2/service-2.json").getFile());
326+
File customizationModel =
327+
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2/customization.config")
328+
.getFile());
329+
C2jModels models = C2jModels
330+
.builder()
331+
.serviceModel(getServiceModel(serviceModel))
332+
.customizationConfig(getCustomizationConfig(customizationModel))
333+
.build();
334+
335+
return new IntermediateModelBuilder(models).build();
336+
}
337+
338+
public static IntermediateModel serviceWithH2UsePriorKnowledgeForH2() {
339+
File serviceModel =
340+
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2-usePriorKnowledgeForH2/service-2.json").getFile());
341+
File customizationModel =
342+
new File(ClientTestModels.class.getResource("client/c2j/service-with-h2-usePriorKnowledgeForH2/customization.config")
343+
.getFile());
344+
C2jModels models = C2jModels
345+
.builder()
346+
.serviceModel(getServiceModel(serviceModel))
347+
.customizationConfig(getCustomizationConfig(customizationModel))
348+
.build();
349+
350+
return new IntermediateModelBuilder(models).build();
351+
}
352+
323353
public static IntermediateModel serviceMiniS3() {
324354
File serviceModel =
325355
new File(ClientTestModels.class.getResource("client/c2j/mini-s3/service-2.json").getFile());

codegen/src/test/java/software/amazon/awssdk/codegen/poet/builder/BaseClientBuilderClassTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import static software.amazon.awssdk.codegen.poet.ClientTestModels.queryServiceModels;
2323
import static software.amazon.awssdk.codegen.poet.ClientTestModels.queryServiceModelsEndpointAuthParamsWithAllowList;
2424
import static software.amazon.awssdk.codegen.poet.ClientTestModels.restJsonServiceModels;
25+
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithH2;
26+
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithH2UsePriorKnowledgeForH2;
2527
import static software.amazon.awssdk.codegen.poet.ClientTestModels.serviceWithNoAuth;
2628
import static software.amazon.awssdk.codegen.poet.builder.BuilderClassTestUtils.validateGeneration;
2729

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

122+
@Test
123+
void baseClientBuilderClassWithH2() {
124+
validateBaseClientBuilderClassGeneration(serviceWithH2(), "test-h2-service-client-builder-class.java");
125+
}
126+
127+
@Test
128+
void baseClientBuilderClassWithH2_usePriorKnowledgeForH2() {
129+
validateBaseClientBuilderClassGeneration(serviceWithH2UsePriorKnowledgeForH2(), "test-h2-usePriorKnowledgeForH2-service-client-builder-class.java");
130+
}
131+
120132
private void validateBaseClientBuilderClassGeneration(IntermediateModel model, String expectedClassName) {
121133
validateBaseClientBuilderClassGeneration(model, expectedClassName, false);
122134
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package software.amazon.awssdk.services.h2;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.function.Consumer;
7+
import software.amazon.awssdk.annotations.Generated;
8+
import software.amazon.awssdk.annotations.SdkInternalApi;
9+
import software.amazon.awssdk.auth.signer.Aws4Signer;
10+
import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder;
11+
import software.amazon.awssdk.awscore.client.config.AwsClientOption;
12+
import software.amazon.awssdk.awscore.endpoint.AwsClientEndpointProvider;
13+
import software.amazon.awssdk.awscore.retry.AwsRetryStrategy;
14+
import software.amazon.awssdk.core.SdkPlugin;
15+
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration;
16+
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption;
17+
import software.amazon.awssdk.core.client.config.SdkClientConfiguration;
18+
import software.amazon.awssdk.core.client.config.SdkClientOption;
19+
import software.amazon.awssdk.core.interceptor.ClasspathInterceptorChainFactory;
20+
import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
21+
import software.amazon.awssdk.core.retry.RetryMode;
22+
import software.amazon.awssdk.core.signer.Signer;
23+
import software.amazon.awssdk.http.Protocol;
24+
import software.amazon.awssdk.http.ProtocolNegotiation;
25+
import software.amazon.awssdk.http.SdkHttpConfigurationOption;
26+
import software.amazon.awssdk.identity.spi.IdentityProvider;
27+
import software.amazon.awssdk.identity.spi.IdentityProviders;
28+
import software.amazon.awssdk.regions.ServiceMetadataAdvancedOption;
29+
import software.amazon.awssdk.retries.api.RetryStrategy;
30+
import software.amazon.awssdk.services.h2.endpoints.H2EndpointProvider;
31+
import software.amazon.awssdk.services.h2.endpoints.internal.H2RequestSetEndpointInterceptor;
32+
import software.amazon.awssdk.services.h2.endpoints.internal.H2ResolveEndpointInterceptor;
33+
import software.amazon.awssdk.services.h2.internal.H2ServiceClientConfigurationBuilder;
34+
import software.amazon.awssdk.utils.AttributeMap;
35+
import software.amazon.awssdk.utils.CollectionUtils;
36+
import software.amazon.awssdk.utils.Validate;
37+
38+
/**
39+
* Internal base class for {@link DefaultH2ClientBuilder} and {@link DefaultH2AsyncClientBuilder}.
40+
*/
41+
@Generated("software.amazon.awssdk:codegen")
42+
@SdkInternalApi
43+
abstract class DefaultH2BaseClientBuilder<B extends H2BaseClientBuilder<B, C>, C> extends AwsDefaultClientBuilder<B, C> {
44+
@Override
45+
protected final String serviceEndpointPrefix() {
46+
return "h2-service";
47+
}
48+
49+
@Override
50+
protected final String serviceName() {
51+
return "H2";
52+
}
53+
54+
@Override
55+
protected final SdkClientConfiguration mergeServiceDefaults(SdkClientConfiguration config) {
56+
return config.merge(c -> c.option(SdkClientOption.ENDPOINT_PROVIDER, defaultEndpointProvider())
57+
.option(SdkAdvancedClientOption.SIGNER, defaultSigner())
58+
.option(SdkClientOption.CRC32_FROM_COMPRESSED_DATA_ENABLED, false));
59+
}
60+
61+
@Override
62+
protected final SdkClientConfiguration finalizeServiceConfiguration(SdkClientConfiguration config) {
63+
List<ExecutionInterceptor> endpointInterceptors = new ArrayList<>();
64+
endpointInterceptors.add(new H2ResolveEndpointInterceptor());
65+
endpointInterceptors.add(new H2RequestSetEndpointInterceptor());
66+
ClasspathInterceptorChainFactory interceptorFactory = new ClasspathInterceptorChainFactory();
67+
List<ExecutionInterceptor> interceptors = interceptorFactory
68+
.getInterceptors("software/amazon/awssdk/services/h2/execution.interceptors");
69+
List<ExecutionInterceptor> additionalInterceptors = new ArrayList<>();
70+
interceptors = CollectionUtils.mergeLists(endpointInterceptors, interceptors);
71+
interceptors = CollectionUtils.mergeLists(interceptors, additionalInterceptors);
72+
interceptors = CollectionUtils.mergeLists(interceptors, config.option(SdkClientOption.EXECUTION_INTERCEPTORS));
73+
SdkClientConfiguration.Builder builder = config.toBuilder();
74+
builder.lazyOption(SdkClientOption.IDENTITY_PROVIDERS, c -> {
75+
IdentityProviders.Builder result = IdentityProviders.builder();
76+
IdentityProvider<?> credentialsIdentityProvider = c.get(AwsClientOption.CREDENTIALS_IDENTITY_PROVIDER);
77+
if (credentialsIdentityProvider != null) {
78+
result.putIdentityProvider(credentialsIdentityProvider);
79+
}
80+
return result.build();
81+
});
82+
builder.option(SdkClientOption.EXECUTION_INTERCEPTORS, interceptors);
83+
builder.lazyOptionIfAbsent(
84+
SdkClientOption.CLIENT_ENDPOINT_PROVIDER,
85+
c -> AwsClientEndpointProvider
86+
.builder()
87+
.serviceEndpointOverrideEnvironmentVariable("AWS_ENDPOINT_URL_H2_SERVICE")
88+
.serviceEndpointOverrideSystemProperty("aws.endpointUrlH2")
89+
.serviceProfileProperty("h2_service")
90+
.serviceEndpointPrefix(serviceEndpointPrefix())
91+
.defaultProtocol("https")
92+
.region(c.get(AwsClientOption.AWS_REGION))
93+
.profileFile(c.get(SdkClientOption.PROFILE_FILE_SUPPLIER))
94+
.profileName(c.get(SdkClientOption.PROFILE_NAME))
95+
.putAdvancedOption(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT,
96+
c.get(ServiceMetadataAdvancedOption.DEFAULT_S3_US_EAST_1_REGIONAL_ENDPOINT))
97+
.dualstackEnabled(c.get(AwsClientOption.DUALSTACK_ENDPOINT_ENABLED))
98+
.fipsEnabled(c.get(AwsClientOption.FIPS_ENDPOINT_ENABLED)).build());
99+
return builder.build();
100+
}
101+
102+
private Signer defaultSigner() {
103+
return Aws4Signer.create();
104+
}
105+
106+
@Override
107+
protected final String signingName() {
108+
return "h2-service";
109+
}
110+
111+
private H2EndpointProvider defaultEndpointProvider() {
112+
return H2EndpointProvider.defaultProvider();
113+
}
114+
115+
@Override
116+
protected final AttributeMap serviceHttpConfig() {
117+
AttributeMap result = AttributeMap.empty();
118+
return result.merge(AttributeMap.builder().put(SdkHttpConfigurationOption.PROTOCOL, Protocol.HTTP2)
119+
.put(SdkHttpConfigurationOption.PROTOCOL_NEGOTIATION, ProtocolNegotiation.ALPN).build());
120+
}
121+
122+
@Override
123+
protected SdkClientConfiguration invokePlugins(SdkClientConfiguration config) {
124+
List<SdkPlugin> internalPlugins = internalPlugins(config);
125+
List<SdkPlugin> externalPlugins = plugins();
126+
if (internalPlugins.isEmpty() && externalPlugins.isEmpty()) {
127+
return config;
128+
}
129+
List<SdkPlugin> plugins = CollectionUtils.mergeLists(internalPlugins, externalPlugins);
130+
SdkClientConfiguration.Builder configuration = config.toBuilder();
131+
H2ServiceClientConfigurationBuilder serviceConfigBuilder = new H2ServiceClientConfigurationBuilder(configuration);
132+
for (SdkPlugin plugin : plugins) {
133+
plugin.configureClient(serviceConfigBuilder);
134+
}
135+
updateRetryStrategyClientConfiguration(configuration);
136+
return configuration.build();
137+
}
138+
139+
private void updateRetryStrategyClientConfiguration(SdkClientConfiguration.Builder configuration) {
140+
ClientOverrideConfiguration.Builder builder = configuration.asOverrideConfigurationBuilder();
141+
RetryMode retryMode = builder.retryMode();
142+
if (retryMode != null) {
143+
configuration.option(SdkClientOption.RETRY_STRATEGY, AwsRetryStrategy.forRetryMode(retryMode));
144+
} else {
145+
Consumer<RetryStrategy.Builder<?, ?>> configurator = builder.retryStrategyConfigurator();
146+
if (configurator != null) {
147+
RetryStrategy.Builder<?, ?> defaultBuilder = AwsRetryStrategy.defaultRetryStrategy().toBuilder();
148+
configurator.accept(defaultBuilder);
149+
configuration.option(SdkClientOption.RETRY_STRATEGY, defaultBuilder.build());
150+
} else {
151+
RetryStrategy retryStrategy = builder.retryStrategy();
152+
if (retryStrategy != null) {
153+
configuration.option(SdkClientOption.RETRY_STRATEGY, retryStrategy);
154+
}
155+
}
156+
}
157+
configuration.option(SdkClientOption.CONFIGURED_RETRY_MODE, null);
158+
configuration.option(SdkClientOption.CONFIGURED_RETRY_STRATEGY, null);
159+
configuration.option(SdkClientOption.CONFIGURED_RETRY_CONFIGURATOR, null);
160+
}
161+
162+
private List<SdkPlugin> internalPlugins(SdkClientConfiguration config) {
163+
return Collections.emptyList();
164+
}
165+
166+
protected static void validateClientOptions(SdkClientConfiguration c) {
167+
Validate.notNull(c.option(SdkAdvancedClientOption.SIGNER),
168+
"The 'overrideConfiguration.advancedOption[SIGNER]' must be configured in the client builder.");
169+
}
170+
}

0 commit comments

Comments
 (0)