Skip to content

Commit ae71cf3

Browse files
authored
Allow users to supply a custom HttpClientBuilder and HttpClientConnectionManager (#352)
Resolves #310 by allowing users to pass in an existing HttpClientBuilder and HttpClientConnectionManager using MantaConnectionFactoryConfigurator. USAGE.md, changelog, and JavaDocs have been update to explain expectations around this feature.
1 parent e36725e commit ae71cf3

9 files changed

Lines changed: 457 additions & 92 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ This project aims to adhere to [Semantic Versioning](http://semver.org/).
99
directory listings
1010
- Added [connection request timeout](https://github.com/joyent/java-manta/issues/347)
1111
configuration parameter: `manta.connection_request_timeout` / `MANTA_CONNECTION_REQUEST_TIMEOUT`
12+
- `MantaClient` will now accept an externally-created `HttpClientBuilder` in order to
13+
allow for customization now available through standard configuration parameters. Users are expected to provide a
14+
`MantaConnectionFactoryConfigurator` containing their custom instance to the
15+
`MantaClient(ConfigContext, MantaConnectionFactoryConfigurator)` constructor. Constructor documentation
16+
explains the benefits and trade offs.
1217
### Fixed
1318
- Clarify version history of `MantaInputStreamEntity`
1419
- MPU parts which were missing an ETag in their response were

USAGE.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,12 @@ and you wish to debug the establishment and leasing of HTTP connections:
335335

336336
Please note that the Commons Logger adaptor is not a dependency of `java-manta-client-unshaded` and it is the user's
337337
responsibility to add their own dependency if they wish to collect Apache HttpClient logs. For more information on log
338-
bridging in SLF4J please review [this page](https://www.slf4j.org/legacy.html).
338+
bridging in SLF4J please review [this page](https://www.slf4j.org/legacy.html).
339+
340+
### Customizing the client further
341+
342+
It is possible to supply an `HttpClientBuilder` in order to further customize the behavior of a `MantaClient` instance.
343+
Users leveraging this feature should be comfortable with the internals of the Apache HttpClient library and
344+
familiarity with the
345+
[`MantaConnectionFactory`](/java-manta-client-unshaded/src/main/java/com/joyent/manta/http/MantaConnectionFactory.java)
346+
class is recommended.

java-manta-client-unshaded/src/main/java/com/joyent/manta/client/MantaClient.java

Lines changed: 39 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.joyent.manta.http.HttpHelper;
3030
import com.joyent.manta.http.MantaApacheHttpClientContext;
3131
import com.joyent.manta.http.MantaConnectionFactory;
32+
import com.joyent.manta.http.MantaConnectionFactoryConfigurator;
3233
import com.joyent.manta.http.MantaContentTypes;
3334
import com.joyent.manta.http.MantaHttpHeaders;
3435
import com.joyent.manta.http.StandardHttpHelper;
@@ -39,7 +40,6 @@
3940
import org.apache.commons.io.FilenameUtils;
4041
import org.apache.commons.io.IOUtils;
4142
import org.apache.commons.lang3.BooleanUtils;
42-
import org.apache.commons.lang3.ObjectUtils;
4343
import org.apache.commons.lang3.Validate;
4444
import org.apache.http.HttpEntity;
4545
import org.apache.http.HttpHeaders;
@@ -199,27 +199,49 @@ public class MantaClient implements AutoCloseable {
199199
* @param config The configuration context that provides all of the configuration values.
200200
*/
201201
public MantaClient(final ConfigContext config) {
202+
this(config, null);
203+
}
204+
202205

206+
/**
207+
* Creates a new instance of the Manta client based on user-provided connection objects. This allows for a higher
208+
* degree of customization at the cost of more involvement from the consumer.
209+
*
210+
* Users opting into advanced configuration (i.e. not passing {@code null} as the second parameter)
211+
* should be comfortable with the internals of {@link CloseableHttpClient} and accept that we can only make a
212+
* best effort to support all possible use-cases. For example, uses may pass in a builder which is wired to a
213+
* {@link org.apache.http.impl.conn.BasicHttpClientConnectionManager} and effectively make the client
214+
* single-threaded by eliminating the connection pool. Bug or feature? You decide!
215+
*
216+
* @param config The configuration context that provides all of the configuration values
217+
* @param connectionFactoryConfigurator pre-configured objects for use with a MantaConnectionFactory
218+
*/
219+
public MantaClient(final ConfigContext config,
220+
final MantaConnectionFactoryConfigurator connectionFactoryConfigurator) {
203221
dumpConfig(config);
204222

205223
ConfigContext.validate(config);
206224

207-
this.config = config;
208225
this.url = config.getMantaURL();
226+
this.config = config;
209227
this.home = ConfigContext.deriveHomeDirectoryFromUser(config.getMantaUser());
228+
this.beanSupervisor = new MantaMBeanSupervisor();
210229

211230
final KeyPair keyPair = new KeyPairFactory(config).createKeyPair();
212231

213232
final Signer.Builder builder = new Signer.Builder(keyPair);
214-
if (ObjectUtils.firstNonNull(
215-
config.disableNativeSignatures(),
216-
DefaultsConfigContext.DEFAULT_DISABLE_NATIVE_SIGNATURES)) {
233+
if (BooleanUtils.isTrue(config.disableNativeSignatures())) {
217234
builder.providerCode("stdlib");
218235
}
219236
final ThreadLocalSigner signer = new ThreadLocalSigner(builder);
220237
this.signerRef = new WeakReference<>(signer);
221238

222-
final MantaConnectionFactory connectionFactory = new MantaConnectionFactory(config, keyPair, signer);
239+
final MantaConnectionFactory connectionFactory = new MantaConnectionFactory(
240+
config,
241+
keyPair,
242+
signer,
243+
connectionFactoryConfigurator);
244+
223245
final MantaApacheHttpClientContext connectionContext = new MantaApacheHttpClientContext(connectionFactory);
224246

225247
if (BooleanUtils.isTrue(config.isClientEncryptionEnabled())) {
@@ -230,48 +252,14 @@ public MantaClient(final ConfigContext config) {
230252

231253
this.uriSigner = new UriSigner(this.config, keyPair, signer);
232254

233-
this.beanSupervisor = new MantaMBeanSupervisor();
234-
235255
beanSupervisor.expose(this.config);
236256
beanSupervisor.expose(connectionFactory);
237257
}
238258

239-
/**
240-
* Creates a new instance of the Manta client with the specified connection factory.
241-
* This method is typically used by unit tests to inject connection factory
242-
* customizations so we can simulate different network events.
243-
*
244-
* @param config The configuration context that provides all of the configuration values
245-
* @param keyPair cryptographic signing key pair used for HTTP signatures
246-
* @param connectionFactory connection factory instance used to tune connection settings
247-
*/
248-
MantaClient(final ConfigContext config,
249-
final KeyPair keyPair,
250-
final MantaConnectionFactory connectionFactory,
251-
final ThreadLocalSigner signer) {
252-
dumpConfig(config);
253-
254-
ConfigContext.validate(config);
255-
256-
this.url = config.getMantaURL();
257-
this.config = config;
258-
this.home = ConfigContext.deriveHomeDirectoryFromUser(config.getMantaUser());
259-
260-
this.signerRef = new WeakReference<>(signer);
261-
262-
final MantaApacheHttpClientContext connectionContext =
263-
new MantaApacheHttpClientContext(connectionFactory);
264-
265-
if (BooleanUtils.isTrue(config.isClientEncryptionEnabled())) {
266-
this.httpHelper = new EncryptionHttpHelper(connectionContext, config);
267-
} else {
268-
this.httpHelper = new StandardHttpHelper(connectionContext, config);
269-
}
270-
271-
this.uriSigner = new UriSigner(this.config, keyPair, signer);
259+
/* ======================================================================
260+
* Constructor Helpers
261+
* ====================================================================== */
272262

273-
this.beanSupervisor = new MantaMBeanSupervisor();
274-
}
275263

276264
/**
277265
* Dumps the configuration that is used to load a {@link MantaClient} if
@@ -295,6 +283,10 @@ private static void dumpConfig(final ConfigContext context) {
295283
}
296284
}
297285

286+
/* ======================================================================
287+
* Object Access
288+
* ====================================================================== */
289+
298290
/**
299291
* Deletes an object from Manta.
300292
*
@@ -2414,6 +2406,10 @@ protected Stream<String> responseAsStream(final HttpResponse response)
24142406
return stream;
24152407
}
24162408

2409+
/* ======================================================================
2410+
* Lifecyle Methods
2411+
* ====================================================================== */
2412+
24172413
/**
24182414
* Flag indicating if the client is closed or is in the process of being
24192415
* closed.

java-manta-client-unshaded/src/main/java/com/joyent/manta/http/MantaConnectionFactory.java

Lines changed: 69 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import com.joyent.manta.config.DefaultsConfigContext;
1616
import com.joyent.manta.exception.ConfigurationException;
1717
import com.joyent.manta.util.MantaVersion;
18+
import org.apache.commons.lang3.BooleanUtils;
1819
import org.apache.commons.lang3.ObjectUtils;
1920
import org.apache.commons.lang3.Validate;
2021
import org.apache.http.Header;
@@ -118,36 +119,40 @@ public class MantaConnectionFactory implements Closeable, MantaMBeanable {
118119
/**
119120
* Create new instance using the passed configuration.
120121
*
121-
* @param config configuration of the connection parameters
122-
* @param keyPair cryptographic signing key pair used for HTTP signatures
123-
* @param signer Signer configured to use the given keyPair
122+
* @param config configuration of the connection parameters
123+
* @param keyPair cryptographic signing key pair used for HTTP signatures
124+
* @param signer Signer configured to use the given keyPair
124125
*/
125126
public MantaConnectionFactory(final ConfigContext config,
126127
final KeyPair keyPair,
127128
final ThreadLocalSigner signer) {
128-
Validate.notNull(config, "Configuration context must not be null");
129+
this(config, keyPair, signer, null);
130+
}
129131

132+
/**
133+
* Create a new instance based on a shared {@link HttpClientBuilder} and {@link HttpClientConnectionManager}.
134+
*
135+
* @param config configuration of the connection parameters
136+
* @param keyPair cryptographic signing key pair used for HTTP signatures
137+
* @param signer Signer configured to use the given keyPair
138+
* @param connectionFactoryConfigurator existing HttpClient objects to reuse
139+
*/
140+
public MantaConnectionFactory(final ConfigContext config,
141+
final KeyPair keyPair,
142+
final ThreadLocalSigner signer,
143+
final MantaConnectionFactoryConfigurator connectionFactoryConfigurator) {
144+
Validate.notNull(config, "Configuration context must not be null");
130145
this.config = config;
131146

132-
this.connectionManager = buildConnectionManager();
133-
134-
this.httpClientBuilder = createBuilder();
135-
136-
final boolean authDisabled = ObjectUtils.firstNonNull(
137-
config.noAuth(),
138-
DefaultsConfigContext.DEFAULT_NO_AUTH);
139-
140-
if (!authDisabled) {
141-
Validate.notNull(keyPair, "KeyPair must not be null if authentication is enabled");
142-
Validate.notNull(signer, "Signer must not be null if authentication is enabled");
143-
144-
// pass true directly to the constructor because auth is enabled
145-
final HttpRequestInterceptor authInterceptor = new HttpSignatureRequestInterceptor(
146-
new HttpSignatureAuthScheme(keyPair, signer),
147-
new UsernamePasswordCredentials(config.getMantaUser(), null),
148-
true);
149-
this.httpClientBuilder.addInterceptorLast(authInterceptor);
147+
if (connectionFactoryConfigurator != null) {
148+
this.connectionManager = null;
149+
this.httpClientBuilder = connectionFactoryConfigurator.getHttpClientBuilder();
150+
} else {
151+
this.connectionManager = buildConnectionManager();
152+
this.httpClientBuilder = createStandardBuilder();
150153
}
154+
155+
configureHttpClientBuilderDefaults(keyPair, signer);
151156
}
152157

153158
/**
@@ -186,6 +191,7 @@ protected SocketConfig buildSocketConfig() {
186191

187192
/**
188193
* Builds and configures a {@link ConnectionConfig} instance.
194+
*
189195
* @return fully configured instance
190196
*/
191197
protected ConnectionConfig buildConnectionConfig() {
@@ -204,7 +210,7 @@ protected ConnectionConfig buildConnectionConfig() {
204210
*
205211
* @return fully configured connection manager
206212
*/
207-
protected PoolingHttpClientConnectionManager buildConnectionManager() {
213+
protected HttpClientConnectionManager buildConnectionManager() {
208214
final int maxConns = ObjectUtils.firstNonNull(
209215
config.getMaximumConnections(),
210216
DefaultsConfigContext.DEFAULT_MAX_CONNS);
@@ -241,7 +247,7 @@ protected PoolingHttpClientConnectionManager buildConnectionManager() {
241247
*
242248
* @return configured instance
243249
*/
244-
protected HttpClientBuilder createBuilder() {
250+
protected HttpClientBuilder createStandardBuilder() {
245251
final int maxConns = ObjectUtils.firstNonNull(
246252
config.getMaximumConnections(),
247253
DefaultsConfigContext.DEFAULT_MAX_CONNS);
@@ -264,7 +270,6 @@ protected HttpClientBuilder createBuilder() {
264270
final HttpClientBuilder builder = HttpClients.custom()
265271
.disableAuthCaching()
266272
.disableCookieManagement()
267-
.setDefaultHeaders(HEADERS)
268273
.setUserAgent(USER_AGENT)
269274
.setConnectionReuseStrategy(new DefaultConnectionReuseStrategy())
270275
.setMaxConnTotal(maxConns)
@@ -274,24 +279,51 @@ protected HttpClientBuilder createBuilder() {
274279
.setRequestExecutor(new MantaHttpRequestExecutor())
275280
.setConnectionBackoffStrategy(new DefaultBackoffStrategy());
276281

282+
final HttpHost proxyHost = findProxyServer();
283+
284+
if (proxyHost != null) {
285+
builder.setProxy(proxyHost);
286+
}
287+
288+
return builder;
289+
}
290+
291+
/**
292+
* Apply required configuration to an HttpClientBuilder that may have been created by us or provided externally.
293+
*
294+
* @param keyPair the keypair to use with signature authentication
295+
* @param signer Signer configured to use the given keyPair
296+
*/
297+
private void configureHttpClientBuilderDefaults(final KeyPair keyPair,
298+
final ThreadLocalSigner signer) {
277299
if (config.getRetries() > 0) {
278-
builder.setRetryHandler(new MantaHttpRequestRetryHandler(config))
279-
.setServiceUnavailableRetryStrategy(new MantaServiceUnavailableRetryStrategy(config));
300+
httpClientBuilder.setRetryHandler(new MantaHttpRequestRetryHandler(config));
301+
httpClientBuilder.setServiceUnavailableRetryStrategy(new MantaServiceUnavailableRetryStrategy(config));
280302
} else {
281303
LOGGER.info("Retry of failed requests is disabled");
282-
builder.disableAutomaticRetries();
304+
httpClientBuilder.disableAutomaticRetries();
283305
}
284306

285-
final HttpHost proxyHost = findProxyServer();
286-
287-
if (proxyHost != null) {
288-
builder.setProxy(proxyHost);
307+
// attach the connection manager if it was created by us
308+
// users providing a custom HttpClientBuilder are expected to wire this up themselves
309+
if (this.connectionManager != null) {
310+
httpClientBuilder.setConnectionManager(this.connectionManager);
289311
}
290312

291-
builder.addInterceptorFirst(new RequestIdInterceptor());
292-
builder.setConnectionManager(this.connectionManager);
313+
httpClientBuilder.setDefaultHeaders(HEADERS);
314+
httpClientBuilder.addInterceptorFirst(new RequestIdInterceptor());
293315

294-
return builder;
316+
if (BooleanUtils.isNotTrue(config.noAuth())) {
317+
Validate.notNull(keyPair, "KeyPair must not be null if authentication is enabled");
318+
Validate.notNull(signer, "Signer must not be null if authentication is enabled");
319+
320+
// pass true directly to the constructor because auth is enabled
321+
final HttpRequestInterceptor authInterceptor = new HttpSignatureRequestInterceptor(
322+
new HttpSignatureAuthScheme(keyPair, signer),
323+
new UsernamePasswordCredentials(config.getMantaUser(), null),
324+
true);
325+
this.httpClientBuilder.addInterceptorLast(authInterceptor);
326+
}
295327
}
296328

297329
/**
@@ -343,16 +375,6 @@ public CloseableHttpClient createConnection() {
343375
return httpClientBuilder.build();
344376
}
345377

346-
/**
347-
* package-private method for building a {@link MantaHttpRequestFactory} from this object's
348-
* config. Should be removed with the deprecated constructor for {@link MantaApacheHttpClientContext}.
349-
*
350-
* @return a request factory pointed at the same url as {@code this}
351-
*/
352-
ConfigContext getConfig() {
353-
return config;
354-
}
355-
356378
@Override
357379
public DynamicMBean toMBean() {
358380
if (!(connectionManager instanceof PoolingHttpClientConnectionManager)) {
@@ -365,8 +387,10 @@ public DynamicMBean toMBean() {
365387
@Override
366388
public void close() throws IOException {
367389
if (connectionManager == null) {
390+
// user provided their own connectionManager in the httpClientBuilder
368391
return;
369392
}
393+
370394
connectionManager.shutdown();
371395
}
372396
}

0 commit comments

Comments
 (0)