-
Notifications
You must be signed in to change notification settings - Fork 16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New entry-point: Dialogue.newClientPool() #398
Closed
Closed
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
6fdf2a0
braindump
iamdanfox f096c15
Javadoc my intentions
iamdanfox 76409a9
ClientPoolImpl
iamdanfox 9c7f454
WIP
iamdanfox d7a8274
Factory#construct requires a Channel
iamdanfox 14f0e5b
Wire up to Channels#create
iamdanfox 7e448ac
Appease static analysis
iamdanfox 3f50051
ConstructUsing
iamdanfox 3341441
SharedResources
iamdanfox 824d4ef
DialogueApacheHttpClient
iamdanfox 468a7fa
Use RefreshingChannelFactory
iamdanfox 37fcf2f
Clear warnings when we can't live reload something
iamdanfox 59aae08
implement refreshable again
iamdanfox d3a797a
SharedResourcesImpl
iamdanfox ab8f8fd
Test passes from intellij
iamdanfox afa7b94
No cycle
iamdanfox 308b034
No cycle
iamdanfox 1e80e53
Show a sample async client working
iamdanfox 655e485
checkstyle
iamdanfox bd6faee
Use enums instead
iamdanfox 273a0c3
Some more comments
iamdanfox 147de40
ListenableValue is test-only
iamdanfox File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
334 changes: 334 additions & 0 deletions
334
...e-apache-hc4-client/src/main/java/com/palantir/dialogue/hc4/DialogueApacheHttpClient.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,334 @@ | ||
/* | ||
* (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.net.HostAndPort; | ||
import com.google.common.primitives.Ints; | ||
import com.palantir.conjure.java.api.config.service.BasicCredentials; | ||
import com.palantir.conjure.java.client.config.CipherSuites; | ||
import com.palantir.conjure.java.client.config.ClientConfiguration; | ||
import com.palantir.dialogue.Channel; | ||
import com.palantir.dialogue.blocking.BlockingChannelAdapter; | ||
import com.palantir.dialogue.core.DialogueConfig; | ||
import com.palantir.dialogue.core.HttpChannelFactory; | ||
import com.palantir.dialogue.core.Listenable; | ||
import com.palantir.dialogue.core.SharedResources; | ||
import com.palantir.logsafe.Preconditions; | ||
import com.palantir.logsafe.SafeArg; | ||
import com.palantir.logsafe.UnsafeArg; | ||
import com.palantir.logsafe.exceptions.SafeIllegalArgumentException; | ||
import java.net.MalformedURLException; | ||
import java.net.ProxySelector; | ||
import java.net.URL; | ||
import java.time.Duration; | ||
import java.util.ArrayDeque; | ||
import java.util.Collections; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Queue; | ||
import java.util.concurrent.TimeUnit; | ||
import javax.net.ssl.SSLSocketFactory; | ||
import org.apache.http.Header; | ||
import org.apache.http.HttpHost; | ||
import org.apache.http.HttpResponse; | ||
import org.apache.http.auth.AuthOption; | ||
import org.apache.http.auth.AuthScheme; | ||
import org.apache.http.auth.AuthSchemeProvider; | ||
import org.apache.http.auth.AuthScope; | ||
import org.apache.http.auth.Credentials; | ||
import org.apache.http.auth.UsernamePasswordCredentials; | ||
import org.apache.http.client.AuthenticationStrategy; | ||
import org.apache.http.client.CredentialsProvider; | ||
import org.apache.http.client.config.AuthSchemes; | ||
import org.apache.http.client.config.RequestConfig; | ||
import org.apache.http.config.RegistryBuilder; | ||
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.auth.BasicSchemeFactory; | ||
import org.apache.http.impl.client.CloseableHttpClient; | ||
import org.apache.http.impl.client.HttpClientBuilder; | ||
import org.apache.http.impl.client.HttpClients; | ||
import org.apache.http.impl.client.ProxyAuthenticationStrategy; | ||
import org.apache.http.impl.conn.SystemDefaultRoutePlanner; | ||
import org.apache.http.protocol.HttpContext; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
public enum DialogueApacheHttpClient implements HttpChannelFactory { | ||
INSTANCE; | ||
|
||
private static final Logger log = LoggerFactory.getLogger(DialogueApacheHttpClient.class); | ||
private static final String STORE = "DialogueApacheHttpClient"; | ||
|
||
@Override | ||
public Channel construct(String uri, Listenable<DialogueConfig> config, SharedResources sharedResources) { | ||
CloseableHttpClient client = sharedResources | ||
.getStore(STORE) | ||
.getOrComputeIfAbsent( | ||
"one-off-client-construction", | ||
unused -> { | ||
return constructLiveReloadingClient(config); // we'll re-use this instance every time | ||
}, | ||
CloseableHttpClient.class); | ||
|
||
return BlockingChannelAdapter.of(new ApacheHttpClientBlockingChannel(client, url(uri))); | ||
} | ||
|
||
private static CloseableHttpClient constructLiveReloadingClient(Listenable<DialogueConfig> listenable) { | ||
ConfigurationSubset params = deriveSubsetWeCareAbout(listenable.getListenableCurrentValue()); | ||
|
||
listenable.subscribe(() -> { | ||
ConfigurationSubset newParams = deriveSubsetWeCareAbout(listenable.getListenableCurrentValue()); | ||
if (params.equals(newParams)) { | ||
// this means users changed something which is irrelevant to us (e.g. a url) | ||
return; | ||
} | ||
|
||
log.warn( | ||
"Unable to live-reload some configuration changes, ignoring them and using the old configuration " | ||
+ "{} {}", | ||
SafeArg.of("old", params), | ||
SafeArg.of("new", newParams)); | ||
}); | ||
|
||
return createCloseableHttpClient(params); | ||
} | ||
|
||
private static ConfigurationSubset deriveSubsetWeCareAbout(DialogueConfig config) { | ||
ClientConfiguration legacyConf = config.legacyClientConfiguration; | ||
|
||
ConfigurationSubset conf = new ConfigurationSubset(); | ||
conf.connectTimeout = legacyConf.connectTimeout(); | ||
conf.readTimeout = legacyConf.readTimeout(); | ||
conf.writeTimeout = legacyConf.writeTimeout(); | ||
conf.sslSocketFactory = legacyConf.sslSocketFactory(); | ||
conf.enableGcmCipherSuites = legacyConf.enableGcmCipherSuites(); | ||
conf.fallbackToCommonNameVerification = legacyConf.fallbackToCommonNameVerification(); | ||
conf.meshProxy = legacyConf.meshProxy(); | ||
conf.proxy = legacyConf.proxy(); | ||
conf.proxyCredentials = legacyConf.proxyCredentials(); | ||
return conf; | ||
} | ||
|
||
private static CloseableHttpClient createCloseableHttpClient(ConfigurationSubset conf) { | ||
log.info("Constructing ClosableHttpClient with conf {}", UnsafeArg.of("conf", conf)); | ||
Preconditions.checkArgument( | ||
!conf.fallbackToCommonNameVerification, "fallback-to-common-name-verification is not supported"); | ||
Preconditions.checkArgument(!conf.meshProxy.isPresent(), "Mesh proxy is not supported"); | ||
|
||
long socketTimeoutMillis = Math.max(conf.readTimeout.toMillis(), conf.writeTimeout.toMillis()); | ||
int connectTimeout = Ints.checkedCast(conf.connectTimeout.toMillis()); | ||
|
||
HttpClientBuilder builder = 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) | ||
.setRoutePlanner(new SystemDefaultRoutePlanner(null, conf.proxy)) | ||
.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())) | ||
.setDefaultCredentialsProvider(NullCredentialsProvider.INSTANCE) | ||
.setTargetAuthenticationStrategy(NullAuthenticationStrategy.INSTANCE) | ||
.setProxyAuthenticationStrategy(NullAuthenticationStrategy.INSTANCE) | ||
.setDefaultAuthSchemeRegistry( | ||
RegistryBuilder.<AuthSchemeProvider>create().build()); | ||
|
||
conf.proxyCredentials.ifPresent(credentials -> { | ||
builder.setDefaultCredentialsProvider(new SingleCredentialsProvider(credentials)) | ||
.setProxyAuthenticationStrategy(ProxyAuthenticationStrategy.INSTANCE) | ||
.setDefaultAuthSchemeRegistry(RegistryBuilder.<AuthSchemeProvider>create() | ||
.register(AuthSchemes.BASIC, new BasicSchemeFactory()) | ||
.build()); | ||
}); | ||
|
||
CloseableHttpClient build = builder.build(); | ||
// resources will be closed by the 'SharedResources' class | ||
return build; | ||
} | ||
|
||
// can't use immutables because intellij complains of a cycles | ||
static final class ConfigurationSubset { | ||
private Duration connectTimeout; | ||
|
||
private Duration readTimeout; | ||
|
||
private Duration writeTimeout; | ||
|
||
private boolean enableGcmCipherSuites; | ||
|
||
private boolean fallbackToCommonNameVerification; | ||
|
||
private Optional<BasicCredentials> proxyCredentials; | ||
|
||
private Optional<HostAndPort> meshProxy; | ||
|
||
private ProxySelector proxy; | ||
|
||
private SSLSocketFactory sslSocketFactory; | ||
|
||
@Override | ||
@SuppressWarnings("CyclomaticComplexity") | ||
public boolean equals(Object obj) { | ||
if (this == obj) { | ||
return true; | ||
} | ||
if (obj == null || getClass() != obj.getClass()) { | ||
return false; | ||
} | ||
ConfigurationSubset that = (ConfigurationSubset) obj; | ||
return enableGcmCipherSuites == that.enableGcmCipherSuites | ||
&& fallbackToCommonNameVerification == that.fallbackToCommonNameVerification | ||
&& connectTimeout.equals(that.connectTimeout) | ||
&& readTimeout.equals(that.readTimeout) | ||
&& writeTimeout.equals(that.writeTimeout) | ||
&& proxyCredentials.equals(that.proxyCredentials) | ||
&& meshProxy.equals(that.meshProxy) | ||
&& proxy.equals(that.proxy) | ||
&& sslSocketFactory.equals(that.sslSocketFactory); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash( | ||
connectTimeout, | ||
readTimeout, | ||
writeTimeout, | ||
enableGcmCipherSuites, | ||
fallbackToCommonNameVerification, | ||
proxyCredentials, | ||
meshProxy, | ||
proxy, | ||
sslSocketFactory); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return "ConfigurationSubset{" | ||
+ "connectTimeout=" | ||
+ connectTimeout | ||
+ ", readTimeout=" | ||
+ readTimeout | ||
+ ", writeTimeout=" | ||
+ writeTimeout | ||
+ ", enableGcmCipherSuites=" | ||
+ enableGcmCipherSuites | ||
+ ", fallbackToCommonNameVerification=" | ||
+ fallbackToCommonNameVerification | ||
+ ", proxyCredentials=" | ||
+ proxyCredentials | ||
+ ", meshProxy=" | ||
+ meshProxy | ||
+ ", proxy=" | ||
+ proxy | ||
+ ", sslSocketFactory=" | ||
+ sslSocketFactory | ||
+ '}'; | ||
} | ||
} | ||
|
||
private static URL url(String uri) { | ||
try { | ||
return new URL(uri); | ||
} catch (MalformedURLException e) { | ||
throw new SafeIllegalArgumentException("Failed to parse URL", e); | ||
} | ||
} | ||
|
||
private enum NullCredentialsProvider implements CredentialsProvider { | ||
INSTANCE; | ||
|
||
@Override | ||
public void setCredentials(AuthScope _authscope, Credentials _credentials) {} | ||
|
||
@Override | ||
public Credentials getCredentials(AuthScope _authscope) { | ||
return null; | ||
} | ||
|
||
@Override | ||
public void clear() {} | ||
} | ||
|
||
private static final class SingleCredentialsProvider implements CredentialsProvider { | ||
private final Credentials credentials; | ||
|
||
SingleCredentialsProvider(BasicCredentials basicCredentials) { | ||
credentials = new UsernamePasswordCredentials(basicCredentials.username(), basicCredentials.password()); | ||
} | ||
|
||
@Override | ||
public void setCredentials(AuthScope _authscope, Credentials _credentials) {} | ||
|
||
@Override | ||
public Credentials getCredentials(AuthScope _authscope) { | ||
return credentials; | ||
} | ||
|
||
@Override | ||
public void clear() {} | ||
} | ||
|
||
private enum NullAuthenticationStrategy implements AuthenticationStrategy { | ||
INSTANCE; | ||
|
||
@Override | ||
public boolean isAuthenticationRequested(HttpHost _authhost, HttpResponse _response, HttpContext _context) { | ||
return false; | ||
} | ||
|
||
@Override | ||
public Map<String, Header> getChallenges(HttpHost _authhost, HttpResponse _response, HttpContext _context) { | ||
return Collections.emptyMap(); | ||
} | ||
|
||
@Override | ||
public Queue<AuthOption> select( | ||
Map<String, Header> _challenges, HttpHost _authhost, HttpResponse _response, HttpContext _context) { | ||
return new ArrayDeque<>(1); | ||
} | ||
|
||
@Override | ||
public void authSucceeded(HttpHost _authhost, AuthScheme _authScheme, HttpContext _context) {} | ||
|
||
@Override | ||
public void authFailed(HttpHost _authhost, AuthScheme _authScheme, HttpContext _context) {} | ||
} | ||
} |
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
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need a unique CloseableHttpClient instance for each ssl configuration -- each service can have different trust data. A few other prefs from the configuration are applied directly to the CloseableHttpClient so they will require unique instances