-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement a HttpUrlConnection and Apache HC4 based channels (#335)
- Loading branch information
1 parent
4dc3e9b
commit 0593d6b
Showing
25 changed files
with
943 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type: feature | ||
feature: | ||
description: Implement a HttpUrlConnection and Apache HC4 based channels | ||
links: | ||
- https://github.com/palantir/dialogue/pull/335 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
apply from: "$rootDir/gradle/publish-jar.gradle" | ||
|
||
dependencies { | ||
api project(':dialogue-core') | ||
api project(':dialogue-target') | ||
api 'com.palantir.conjure.java.runtime:client-config' | ||
api 'org.apache.httpcomponents:httpclient' | ||
implementation project(':dialogue-blocking-channels') | ||
implementation 'com.palantir.safe-logging:preconditions' | ||
|
||
testCompile project(':dialogue-client-test-lib') | ||
testCompile project(':dialogue-serde') | ||
} |
199 changes: 199 additions & 0 deletions
199
...e-hc4-client/src/main/java/com/palantir/dialogue/hc4/ApacheHttpClientBlockingChannel.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
/* | ||
* (c) Copyright 2020 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
package com.palantir.dialogue.hc4; | ||
|
||
import com.palantir.dialogue.Endpoint; | ||
import com.palantir.dialogue.Headers; | ||
import com.palantir.dialogue.HttpMethod; | ||
import com.palantir.dialogue.Request; | ||
import com.palantir.dialogue.RequestBody; | ||
import com.palantir.dialogue.Response; | ||
import com.palantir.dialogue.UrlBuilder; | ||
import com.palantir.dialogue.blocking.BlockingChannel; | ||
import com.palantir.logsafe.Preconditions; | ||
import com.palantir.logsafe.exceptions.SafeRuntimeException; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.io.OutputStream; | ||
import java.net.URL; | ||
import java.util.ArrayList; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.TreeMap; | ||
import org.apache.http.Header; | ||
import org.apache.http.HttpEntity; | ||
import org.apache.http.client.methods.CloseableHttpResponse; | ||
import org.apache.http.client.methods.RequestBuilder; | ||
import org.apache.http.impl.client.CloseableHttpClient; | ||
import org.apache.http.message.BasicHeader; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
final class ApacheHttpClientBlockingChannel implements BlockingChannel { | ||
private static final Logger log = LoggerFactory.getLogger(ApacheHttpClientBlockingChannel.class); | ||
|
||
private final CloseableHttpClient client; | ||
private final UrlBuilder baseUrl; | ||
|
||
ApacheHttpClientBlockingChannel(CloseableHttpClient client, URL baseUrl) { | ||
this.client = client; | ||
this.baseUrl = UrlBuilder.from(baseUrl); | ||
} | ||
|
||
@Override | ||
public Response execute(Endpoint endpoint, Request request) throws IOException { | ||
// Create base request given the URL | ||
UrlBuilder url = baseUrl.newBuilder(); | ||
endpoint.renderPath(request.pathParams(), url); | ||
request.queryParams().forEach(url::queryParam); | ||
URL target = url.build(); | ||
RequestBuilder builder = | ||
RequestBuilder.create(endpoint.httpMethod().name()).setUri(target.toString()); | ||
|
||
// Fill headers | ||
request.headerParams().forEach(builder::addHeader); | ||
|
||
if (request.body().isPresent()) { | ||
Preconditions.checkArgument( | ||
endpoint.httpMethod() != HttpMethod.GET, "GET endpoints must not have a request body"); | ||
RequestBody body = request.body().get(); | ||
builder.setEntity(new RequestBodyEntity(body)); | ||
} | ||
return new HttpClientResponse(client.execute(builder.build())); | ||
} | ||
|
||
private static final class HttpClientResponse implements Response { | ||
|
||
private final CloseableHttpResponse response; | ||
private Map<String, List<String>> headers; | ||
|
||
HttpClientResponse(CloseableHttpResponse response) { | ||
this.response = response; | ||
} | ||
|
||
@Override | ||
public InputStream body() { | ||
try { | ||
return response.getEntity().getContent(); | ||
} catch (IOException e) { | ||
throw new SafeRuntimeException("Failed to get response stream", e); | ||
} | ||
} | ||
|
||
@Override | ||
public int code() { | ||
return response.getStatusLine().getStatusCode(); | ||
} | ||
|
||
@Override | ||
public Map<String, List<String>> headers() { | ||
if (headers == null) { | ||
Map<String, List<String>> tmpHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); | ||
for (Header header : response.getAllHeaders()) { | ||
String value = header.getValue(); | ||
if (value != null) { | ||
tmpHeaders | ||
.computeIfAbsent(header.getName(), _name -> new ArrayList<>(1)) | ||
.add(header.getValue()); | ||
} | ||
} | ||
headers = tmpHeaders; | ||
} | ||
return headers; | ||
} | ||
|
||
@Override | ||
public Optional<String> getFirstHeader(String header) { | ||
return Optional.ofNullable(response.getFirstHeader(header)).map(Header::getValue); | ||
} | ||
|
||
@Override | ||
public void close() { | ||
try { | ||
response.close(); | ||
} catch (IOException | RuntimeException e) { | ||
log.warn("Failed to close response", e); | ||
} | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "HttpClientResponse{response=" + response + '}'; | ||
} | ||
} | ||
|
||
private static final class RequestBodyEntity implements HttpEntity { | ||
|
||
private final RequestBody requestBody; | ||
private final Header contentType; | ||
|
||
RequestBodyEntity(RequestBody requestBody) { | ||
this.requestBody = requestBody; | ||
this.contentType = new BasicHeader(Headers.CONTENT_TYPE, requestBody.contentType()); | ||
} | ||
|
||
@Override | ||
public boolean isRepeatable() { | ||
return false; | ||
} | ||
|
||
@Override | ||
public boolean isChunked() { | ||
return true; | ||
} | ||
|
||
@Override | ||
public long getContentLength() { | ||
// unknown | ||
return -1; | ||
} | ||
|
||
@Override | ||
public Header getContentType() { | ||
return contentType; | ||
} | ||
|
||
@Override | ||
public Header getContentEncoding() { | ||
return null; | ||
} | ||
|
||
@Override | ||
public InputStream getContent() throws UnsupportedOperationException { | ||
throw new UnsupportedOperationException("getContent is not supported, writeTo should be used"); | ||
} | ||
|
||
@Override | ||
public void writeTo(OutputStream outStream) throws IOException { | ||
requestBody.writeTo(outStream); | ||
} | ||
|
||
@Override | ||
public boolean isStreaming() { | ||
// Applies to responses. | ||
return false; | ||
} | ||
|
||
@Override | ||
public void consumeContent() {} | ||
|
||
@Override | ||
public String toString() { | ||
return "RequestBodyEntity{requestBody=" + requestBody + '}'; | ||
} | ||
} | ||
} |
117 changes: 117 additions & 0 deletions
117
...e-apache-hc4-client/src/main/java/com/palantir/dialogue/hc4/ApacheHttpClientChannels.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/* | ||
* (c) Copyright 2020 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
package com.palantir.dialogue.hc4; | ||
|
||
import com.google.common.collect.ImmutableList; | ||
import com.google.common.primitives.Ints; | ||
import com.palantir.conjure.java.api.config.service.UserAgent; | ||
import com.palantir.conjure.java.client.config.CipherSuites; | ||
import com.palantir.conjure.java.client.config.ClientConfiguration; | ||
import com.palantir.conjure.java.client.config.NodeSelectionStrategy; | ||
import com.palantir.dialogue.Channel; | ||
import com.palantir.dialogue.blocking.BlockingChannelAdapter; | ||
import com.palantir.dialogue.core.Channels; | ||
import com.palantir.logsafe.Preconditions; | ||
import com.palantir.logsafe.SafeArg; | ||
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; | ||
import com.palantir.tritium.metrics.registry.TaggedMetricRegistry; | ||
import java.net.MalformedURLException; | ||
import java.net.URL; | ||
import java.util.concurrent.TimeUnit; | ||
import org.apache.http.client.config.RequestConfig; | ||
import org.apache.http.config.SocketConfig; | ||
import org.apache.http.conn.ssl.DefaultHostnameVerifier; | ||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; | ||
import org.apache.http.impl.client.CloseableHttpClient; | ||
import org.apache.http.impl.client.HttpClients; | ||
import org.apache.http.impl.client.ProxyAuthenticationStrategy; | ||
import org.apache.http.impl.conn.SystemDefaultRoutePlanner; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public final class ApacheHttpClientChannels { | ||
private static final Logger log = LoggerFactory.getLogger(ApacheHttpClientChannels.class); | ||
|
||
private ApacheHttpClientChannels() {} | ||
|
||
public static Channel create(ClientConfiguration conf, UserAgent baseAgent, TaggedMetricRegistry metrics) { | ||
Preconditions.checkArgument( | ||
!conf.fallbackToCommonNameVerification(), "fallback-to-common-name-verification is not supported"); | ||
Preconditions.checkArgument(!conf.meshProxy().isPresent(), "Mesh proxy is not supported"); | ||
Preconditions.checkArgument( | ||
conf.clientQoS() == ClientConfiguration.ClientQoS.ENABLED, "Disabling client QOS is not supported"); | ||
Preconditions.checkArgument( | ||
conf.serverQoS() == ClientConfiguration.ServerQoS.AUTOMATIC_RETRY, | ||
"Propagating QoS exceptions is not supported"); | ||
Preconditions.checkArgument(!conf.proxyCredentials().isPresent(), "Proxy credentials are not supported"); | ||
if (conf.nodeSelectionStrategy() != NodeSelectionStrategy.ROUND_ROBIN) { | ||
log.warn( | ||
"Dialogue currently only supports ROUND_ROBIN node selection strategy. {} will be ignored", | ||
SafeArg.of("requestedStrategy", conf.nodeSelectionStrategy())); | ||
} | ||
long socketTimeoutMillis = | ||
Math.max(conf.readTimeout().toMillis(), conf.writeTimeout().toMillis()); | ||
int connectTimeout = Ints.checkedCast(conf.connectTimeout().toMillis()); | ||
// TODO(ckozak): close resources? | ||
CloseableHttpClient client = HttpClients.custom() | ||
.setDefaultRequestConfig(RequestConfig.custom() | ||
.setSocketTimeout(Ints.checkedCast(socketTimeoutMillis)) | ||
.setConnectTimeout(connectTimeout) | ||
// Don't allow clients to block forever waiting on a connection to become available | ||
.setConnectionRequestTimeout(connectTimeout) | ||
// Match okhttp, disallow redirects | ||
.setRedirectsEnabled(false) | ||
.setRelativeRedirectsAllowed(false) | ||
.build()) | ||
.setDefaultSocketConfig( | ||
SocketConfig.custom().setSoKeepAlive(true).build()) | ||
.evictIdleConnections(55, TimeUnit.SECONDS) | ||
.setMaxConnPerRoute(1000) | ||
.setMaxConnTotal(Integer.MAX_VALUE) | ||
// TODO(ckozak): proxy credentials | ||
.setRoutePlanner(new SystemDefaultRoutePlanner(null, conf.proxy())) | ||
.setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE) | ||
.disableAutomaticRetries() | ||
// Must be disabled otherwise connections are not reused when client certificates are provided | ||
.disableConnectionState() | ||
// Match okhttp behavior disabling cookies | ||
.disableCookieManagement() | ||
// Dialogue handles content-compression with ContentDecodingChannel | ||
.disableContentCompression() | ||
.setSSLSocketFactory( | ||
new SSLConnectionSocketFactory( | ||
conf.sslSocketFactory(), | ||
new String[] {"TLSv1.2"}, | ||
conf.enableGcmCipherSuites() | ||
? CipherSuites.allCipherSuites() | ||
: CipherSuites.fastCipherSuites(), | ||
new DefaultHostnameVerifier())) | ||
.build(); | ||
ImmutableList<Channel> channels = conf.uris().stream() | ||
.map(uri -> BlockingChannelAdapter.of(new ApacheHttpClientBlockingChannel(client, url(uri)))) | ||
.collect(ImmutableList.toImmutableList()); | ||
|
||
return Channels.create(channels, baseAgent, metrics); | ||
} | ||
|
||
private static URL url(String uri) { | ||
try { | ||
return new URL(uri); | ||
} catch (MalformedURLException e) { | ||
throw new SafeIllegalArgumentException("Failed to parse URL", e); | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
...c4-client/src/test/java/com/palantir/dialogue/hc4/ApacheApacheHttpClientChannelsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
/* | ||
* (c) Copyright 2020 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* 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. | ||
*/ | ||
package com.palantir.dialogue.hc4; | ||
|
||
import com.palantir.conjure.java.api.config.service.ServiceConfiguration; | ||
import com.palantir.conjure.java.api.config.service.UserAgent; | ||
import com.palantir.conjure.java.api.config.ssl.SslConfiguration; | ||
import com.palantir.conjure.java.client.config.ClientConfigurations; | ||
import com.palantir.dialogue.AbstractChannelTest; | ||
import com.palantir.dialogue.Channel; | ||
import com.palantir.tritium.metrics.registry.DefaultTaggedMetricRegistry; | ||
import java.net.URL; | ||
import java.nio.file.Paths; | ||
|
||
public final class ApacheApacheHttpClientChannelsTest extends AbstractChannelTest { | ||
|
||
private static final SslConfiguration SSL_CONFIG = SslConfiguration.of( | ||
Paths.get("src/test/resources/trustStore.jks"), Paths.get("src/test/resources/keyStore.jks"), "keystore"); | ||
|
||
@Override | ||
protected Channel createChannel(URL baseUrl) { | ||
ServiceConfiguration serviceConf = ServiceConfiguration.builder() | ||
.addUris(baseUrl.toString()) | ||
.security(SSL_CONFIG) | ||
.build(); | ||
return ApacheHttpClientChannels.create( | ||
ClientConfigurations.of(serviceConf), | ||
UserAgent.of(UserAgent.Agent.of("test-service", "1.0.0")), | ||
new DefaultTaggedMetricRegistry()); | ||
} | ||
} |
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
apply from: "$rootDir/gradle/publish-jar.gradle" | ||
|
||
dependencies { | ||
api project(':dialogue-target') | ||
api 'com.google.guava:guava' | ||
implementation 'com.palantir.tracing:tracing' | ||
|
||
testCompile 'junit:junit' | ||
testCompile 'org.assertj:assertj-core' | ||
testCompile 'org.mockito:mockito-core' | ||
testCompile 'org.awaitility:awaitility' | ||
} |
Oops, something went wrong.