Skip to content

Commit b1e96f3

Browse files
namedgraphclaude
andcommitted
Bound HTTP client timeouts and reclaim pooled connections
Fixes connection-pool exhaustion: a stalled read on the end-user backend route was held indefinitely because the pooled HTTP clients had no socket/read timeout (Apache default SO_TIMEOUT = 0 = infinite). The worker thread blocked inside the read and never reached Response.close(), so the leased connection was never returned. With connectionRequestTimeout also unset, new requests blocked forever waiting for a lease instead of failing fast, wedging the listener. Application.getClient()/getNoCertClient(): - set socket (read) and connect timeouts on the RequestConfig - always set connectionRequestTimeout (default 30s) so lease waits fail fast instead of blocking forever - give pooled connections a TTL and revalidate after inactivity so stuck/stale connections are reclaimed - timeouts exposed as named constants for tuning SignUp: close the PublicKey/Agent/Authorization client Responses on all paths (defense-in-depth; these leaked on the admin signup path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 12e1bc9 commit b1e96f3

2 files changed

Lines changed: 88 additions & 56 deletions

File tree

src/main/java/com/atomgraph/linkeddatahub/Application.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1505,9 +1505,24 @@ public void submitImport(RDFImport rdfImport, com.atomgraph.linkeddatahub.apps.m
15051505
new ImportExecutor(importThreadPool).start(service, adminService, this, baseURI, gsc, rdfImport);
15061506
}
15071507

1508+
/** Default socket (read) timeout in milliseconds for pooled HTTP client connections. Bounds stalled reads so a hung connection cannot be held indefinitely. */
1509+
public static final int CLIENT_SOCKET_TIMEOUT = 120000;
1510+
1511+
/** Default connection (connect) timeout in milliseconds for pooled HTTP client connections. */
1512+
public static final int CLIENT_CONNECT_TIMEOUT = 10000;
1513+
1514+
/** Default timeout in milliseconds to wait for a connection lease from the pool before failing fast. Used when no explicit connectionRequestTimeout is configured. */
1515+
public static final int CLIENT_CONNECTION_REQUEST_TIMEOUT = 30000;
1516+
1517+
/** Time-to-live in milliseconds after which pooled connections are discarded rather than reused. */
1518+
public static final long CLIENT_CONNECTION_TIME_TO_LIVE = 300000;
1519+
1520+
/** Period of inactivity in milliseconds after which a pooled connection is revalidated before reuse. */
1521+
public static final int CLIENT_VALIDATE_AFTER_INACTIVITY = 10000;
1522+
15081523
/**
15091524
* Builds JAX-RS client instance from given configuration.
1510-
*
1525+
*
15111526
* @param keyStore keystore
15121527
* @param keyStorePassword keystore password
15131528
* @param trustStore truststore
@@ -1543,7 +1558,7 @@ public static Client getClient(KeyStore keyStore, String keyStorePassword, KeySt
15431558
register("http", new PlainConnectionSocketFactory()).
15441559
build();
15451560

1546-
PoolingHttpClientConnectionManager conman = new PoolingHttpClientConnectionManager(socketFactoryRegistry)
1561+
PoolingHttpClientConnectionManager conman = new PoolingHttpClientConnectionManager(socketFactoryRegistry, null, null, null, CLIENT_CONNECTION_TIME_TO_LIVE, TimeUnit.MILLISECONDS)
15471562
{
15481563

15491564
// https://github.com/eclipse-ee4j/jersey/issues/4449
@@ -1574,7 +1589,8 @@ public void releaseConnection(final HttpClientConnection managedConn, final Obje
15741589
};
15751590
if (maxConnPerRoute != null) conman.setDefaultMaxPerRoute(maxConnPerRoute);
15761591
if (maxTotalConn != null) conman.setMaxTotal(maxTotalConn);
1577-
1592+
conman.setValidateAfterInactivity(CLIENT_VALIDATE_AFTER_INACTIVITY);
1593+
15781594
ClientConfig config = new ClientConfig();
15791595
config.connectorProvider(new ApacheConnectorProvider());
15801596
config.register(MultiPartFeature.class);
@@ -1586,10 +1602,11 @@ public void releaseConnection(final HttpClientConnection managedConn, final Obje
15861602
config.property(ClientProperties.FOLLOW_REDIRECTS, true);
15871603
config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); // https://stackoverflow.com/questions/42139436/jersey-client-throws-cannot-retry-request-with-a-non-repeatable-request-entity
15881604
config.property(ApacheClientProperties.CONNECTION_MANAGER, conman);
1589-
if (connectionRequestTimeout != null)
1590-
config.property(ApacheClientProperties.REQUEST_CONFIG, RequestConfig.custom().
1591-
setConnectionRequestTimeout(connectionRequestTimeout).
1592-
build());
1605+
config.property(ApacheClientProperties.REQUEST_CONFIG, RequestConfig.custom().
1606+
setConnectionRequestTimeout(connectionRequestTimeout != null ? connectionRequestTimeout : CLIENT_CONNECTION_REQUEST_TIMEOUT).
1607+
setConnectTimeout(CLIENT_CONNECT_TIMEOUT).
1608+
setSocketTimeout(CLIENT_SOCKET_TIMEOUT).
1609+
build());
15931610

15941611
if (maxRequestRetries != null)
15951612
config.property(ApacheClientProperties.RETRY_HANDLER, (HttpRequestRetryHandler) (IOException ex, int executionCount, HttpContext context) ->
@@ -1643,11 +1660,11 @@ public static Client getNoCertClient(KeyStore trustStore, Integer maxConnPerRout
16431660
register("http", new PlainConnectionSocketFactory()).
16441661
build();
16451662

1646-
PoolingHttpClientConnectionManager conman = new PoolingHttpClientConnectionManager(socketFactoryRegistry)
1663+
PoolingHttpClientConnectionManager conman = new PoolingHttpClientConnectionManager(socketFactoryRegistry, null, null, null, CLIENT_CONNECTION_TIME_TO_LIVE, TimeUnit.MILLISECONDS)
16471664
{
16481665

16491666
// https://github.com/eclipse-ee4j/jersey/issues/4449
1650-
1667+
16511668
@Override
16521669
public void close()
16531670
{
@@ -1674,6 +1691,7 @@ public void releaseConnection(final HttpClientConnection managedConn, final Obje
16741691
};
16751692
if (maxConnPerRoute != null) conman.setDefaultMaxPerRoute(maxConnPerRoute);
16761693
if (maxTotalConn != null) conman.setMaxTotal(maxTotalConn);
1694+
conman.setValidateAfterInactivity(CLIENT_VALIDATE_AFTER_INACTIVITY);
16771695

16781696
ClientConfig config = new ClientConfig();
16791697
config.connectorProvider(new ApacheConnectorProvider());
@@ -1686,10 +1704,11 @@ public void releaseConnection(final HttpClientConnection managedConn, final Obje
16861704
config.property(ClientProperties.FOLLOW_REDIRECTS, true);
16871705
config.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED); // https://stackoverflow.com/questions/42139436/jersey-client-throws-cannot-retry-request-with-a-non-repeatable-request-entity
16881706
config.property(ApacheClientProperties.CONNECTION_MANAGER, conman);
1689-
if (connectionRequestTimeout != null)
1690-
config.property(ApacheClientProperties.REQUEST_CONFIG, RequestConfig.custom().
1691-
setConnectionRequestTimeout(connectionRequestTimeout).
1692-
build());
1707+
config.property(ApacheClientProperties.REQUEST_CONFIG, RequestConfig.custom().
1708+
setConnectionRequestTimeout(connectionRequestTimeout != null ? connectionRequestTimeout : CLIENT_CONNECTION_REQUEST_TIMEOUT).
1709+
setConnectTimeout(CLIENT_CONNECT_TIMEOUT).
1710+
setSocketTimeout(CLIENT_SOCKET_TIMEOUT).
1711+
build());
16931712

16941713
if (maxRequestRetries != null)
16951714
config.property(ApacheClientProperties.RETRY_HANDLER, (HttpRequestRetryHandler) (IOException ex, int executionCount, HttpContext context) ->

src/main/java/com/atomgraph/linkeddatahub/resource/admin/SignUp.java

Lines changed: 56 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -242,67 +242,80 @@ public Response post(Model agentModel)
242242
certPublicKey);
243243
new Skolemizer(publicKeyGraphUri.toString()).apply(publicKeyModel);
244244

245-
Response publicKeyResponse = super.put(publicKeyModel, false, publicKeyGraphUri);
246-
if (publicKeyResponse.getStatus() != Response.Status.CREATED.getStatusCode())
245+
try (Response publicKeyResponse = super.put(publicKeyModel, false, publicKeyGraphUri))
247246
{
248-
if (log.isErrorEnabled()) log.error("Cannot create PublicKey");
249-
throw new InternalServerErrorException("Cannot create PublicKey");
247+
if (publicKeyResponse.getStatus() != Response.Status.CREATED.getStatusCode())
248+
{
249+
if (log.isErrorEnabled()) log.error("Cannot create PublicKey");
250+
throw new InternalServerErrorException("Cannot create PublicKey");
251+
}
250252
}
251253

252254
Resource publicKey = publicKeyModel.createResource(publicKeyGraphUri.toString()).getPropertyResourceValue(FOAF.primaryTopic);
253255
agent.addProperty(Cert.key, publicKey); // add public key
254256
agentModel.add(agentModel.createResource(getSystem().getSecretaryWebIDURI().toString()), ACL.delegates, agent); // make secretary delegate whis agent
255257

256258
Response agentResponse = super.put(agentModel, false, agentGraphUri);
257-
if (agentResponse.getStatus() != Response.Status.CREATED.getStatusCode())
259+
boolean returnAgentResponse = false;
260+
try
258261
{
259-
if (log.isErrorEnabled()) log.error("Cannot create Agent");
260-
throw new InternalServerErrorException("Cannot create Agent");
261-
}
262+
if (agentResponse.getStatus() != Response.Status.CREATED.getStatusCode())
263+
{
264+
if (log.isErrorEnabled()) log.error("Cannot create Agent");
265+
throw new InternalServerErrorException("Cannot create Agent");
266+
}
262267

263-
URI authGraphUri = getUriInfo().getBaseUriBuilder().path(AUTHORIZATION_PATH).path("{slug}/").build(UUID.randomUUID().toString());
264-
Model authModel = ModelFactory.createDefaultModel();
265-
// creating authorizations for the Agent and PublicKey documents
266-
createAuthorization(authModel,
267-
authGraphUri,
268-
authModel.createResource(getUriInfo().getBaseUri().resolve(AUTHORIZATION_PATH).toString()),
269-
agentGraphUri,
270-
publicKeyGraphUri);
271-
new Skolemizer(authGraphUri.toString()).apply(authModel);
268+
URI authGraphUri = getUriInfo().getBaseUriBuilder().path(AUTHORIZATION_PATH).path("{slug}/").build(UUID.randomUUID().toString());
269+
Model authModel = ModelFactory.createDefaultModel();
270+
// creating authorizations for the Agent and PublicKey documents
271+
createAuthorization(authModel,
272+
authGraphUri,
273+
authModel.createResource(getUriInfo().getBaseUri().resolve(AUTHORIZATION_PATH).toString()),
274+
agentGraphUri,
275+
publicKeyGraphUri);
276+
new Skolemizer(authGraphUri.toString()).apply(authModel);
272277

273-
Response authResponse = super.put(authModel, false, authGraphUri);
274-
if (authResponse.getStatus() != Response.Status.CREATED.getStatusCode())
275-
{
276-
if (log.isErrorEnabled()) log.error("Cannot create Authorization");
277-
throw new InternalServerErrorException("Cannot create Authorization");
278-
}
278+
try (Response authResponse = super.put(authModel, false, authGraphUri))
279+
{
280+
if (authResponse.getStatus() != Response.Status.CREATED.getStatusCode())
281+
{
282+
if (log.isErrorEnabled()) log.error("Cannot create Authorization");
283+
throw new InternalServerErrorException("Cannot create Authorization");
284+
}
285+
}
279286

280-
// purge agent lookup from proxy cache
281-
URI agentServiceBackendProxy = getSystem().getServiceContext(getAgentService()).getBackendProxy();
282-
if (agentServiceBackendProxy != null)
283-
{
284-
try (Response response = ban(agentServiceBackendProxy, mbox.getURI()))
287+
// purge agent lookup from proxy cache
288+
URI agentServiceBackendProxy = getSystem().getServiceContext(getAgentService()).getBackendProxy();
289+
if (agentServiceBackendProxy != null)
285290
{
286-
// Response automatically closed by try-with-resources
291+
try (Response response = ban(agentServiceBackendProxy, mbox.getURI()))
292+
{
293+
// Response automatically closed by try-with-resources
294+
}
287295
}
288-
}
289296

290-
// remove secretary WebID from cache
291-
getSystem().getEventBus().post(new com.atomgraph.linkeddatahub.server.event.SignUp(getSystem().getSecretaryWebIDURI()));
297+
// remove secretary WebID from cache
298+
getSystem().getEventBus().post(new com.atomgraph.linkeddatahub.server.event.SignUp(getSystem().getSecretaryWebIDURI()));
292299

293-
if (download)
294-
{
295-
return Response.ok(keyStoreBytes).
296-
type(PKCS12_MEDIA_TYPE).
297-
header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=cert.p12").
298-
build();
300+
if (download)
301+
{
302+
return Response.ok(keyStoreBytes).
303+
type(PKCS12_MEDIA_TYPE).
304+
header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=cert.p12").
305+
build();
306+
}
307+
else
308+
{
309+
LocalDate certExpires = LocalDate.now().plusDays(getValidityDays()); // ((X509Certificate)cert).getNotAfter();
310+
sendEmail(agent, certExpires, keyStoreBytes, keyStoreFileName);
311+
312+
returnAgentResponse = true;
313+
return agentResponse; // 201 Created - ownership passes to the JAX-RS runtime
314+
}
299315
}
300-
else
316+
finally
301317
{
302-
LocalDate certExpires = LocalDate.now().plusDays(getValidityDays()); // ((X509Certificate)cert).getNotAfter();
303-
sendEmail(agent, certExpires, keyStoreBytes, keyStoreFileName);
304-
305-
return agentResponse; // 201 Created
318+
if (!returnAgentResponse) agentResponse.close();
306319
}
307320
}
308321
}

0 commit comments

Comments
 (0)