Skip to content

Commit 20416e2

Browse files
committed
GH-5124 make the three main HttpClient timeouts configurable with sensible defaults.
1 parent a253fd5 commit 20416e2

3 files changed

Lines changed: 235 additions & 5 deletions

File tree

core/http/client/src/main/java/org/eclipse/rdf4j/http/client/SharedHttpClientSessionManager.java

Lines changed: 211 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.http.client.utils.HttpClientUtils;
3535
import org.apache.http.impl.client.CloseableHttpClient;
3636
import org.apache.http.impl.client.HttpClientBuilder;
37+
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
3738
import org.apache.http.protocol.HttpContext;
3839
import org.eclipse.rdf4j.http.client.util.HttpClientBuilders;
3940
import org.slf4j.Logger;
@@ -53,6 +54,163 @@ public class SharedHttpClientSessionManager implements HttpClientSessionManager,
5354

5455
private static final AtomicLong threadCount = new AtomicLong();
5556

57+
// System property constants for regular timeouts
58+
59+
/**
60+
* Configurable system property {@code org.eclipse.rdf4j.client.http.connectionTimeout} for specifying the HTTP
61+
* connection timeout in milliseconds for general use. Default is 1 hour.
62+
*
63+
* <p>
64+
* The connection timeout determines the maximum time the client will wait to establish a TCP connection to the
65+
* server. A default of 1 hour is set to allow for potential network delays without causing unnecessary timeouts.
66+
* </p>
67+
*/
68+
public static final String CONNECTION_TIMEOUT_PROPERTY = "org.eclipse.rdf4j.client.http.connectionTimeout";
69+
70+
/**
71+
* Configurable system property {@code org.eclipse.rdf4j.client.http.connectionRequestTimeout} for specifying the
72+
* HTTP connection request timeout in milliseconds for general use. Default is 10 days.
73+
*
74+
* <p>
75+
* The connection request timeout defines how long the client will wait for a connection from the connection pool. A
76+
* longer timeout is acceptable here since operations like large file uploads may need to wait for an available
77+
* connection.
78+
* </p>
79+
*/
80+
public static final String CONNECTION_REQUEST_TIMEOUT_PROPERTY = "org.eclipse.rdf4j.client.http.connectionRequestTimeout";
81+
82+
/**
83+
* Configurable system property {@code org.eclipse.rdf4j.client.http.socketTimeout} for specifying the HTTP socket
84+
* timeout in milliseconds for general use. Default is 10 days.
85+
*
86+
* <p>
87+
* The socket timeout controls the maximum period of inactivity between data packets during data transfer. A longer
88+
* timeout is appropriate for large data transfers, ensuring that operations are not interrupted prematurely.
89+
* </p>
90+
*/
91+
public static final String SOCKET_TIMEOUT_PROPERTY = "org.eclipse.rdf4j.client.http.socketTimeout";
92+
93+
// System property constants for SPARQL SERVICE timeouts
94+
95+
/**
96+
* Configurable system property {@code org.eclipse.rdf4j.client.sparql.http.connectionTimeout} for specifying the
97+
* HTTP connection timeout in milliseconds when used in SPARQL SERVICE calls. Default is 10 minutes.
98+
*
99+
* <p>
100+
* A shorter connection timeout is set for SPARQL SERVICE calls to quickly detect unresponsive endpoints in
101+
* federated queries, improving overall query performance by avoiding long waits for unreachable servers.
102+
* </p>
103+
*/
104+
public static final String SPARQL_CONNECTION_TIMEOUT_PROPERTY = "org.eclipse.rdf4j.client.sparql.http.connectionTimeout";
105+
106+
/**
107+
* Configurable system property {@code org.eclipse.rdf4j.client.sparql.http.connectionRequestTimeout} for specifying
108+
* the HTTP connection request timeout in milliseconds when used in SPARQL SERVICE calls. Default is 6 hours.
109+
*
110+
* <p>
111+
* This timeout controls how long the client waits for a connection from the pool when making SPARQL SERVICE calls.
112+
* A shorter timeout than general use ensures that queries fail fast if resources are constrained, maintaining
113+
* responsiveness.
114+
* </p>
115+
*/
116+
public static final String SPARQL_CONNECTION_REQUEST_TIMEOUT_PROPERTY = "org.eclipse.rdf4j.client.sparql.http.connectionRequestTimeout";
117+
118+
/**
119+
* Configurable system property {@code org.eclipse.rdf4j.client.sparql.http.socketTimeout} for specifying the HTTP
120+
* socket timeout in milliseconds when used in SPARQL SERVICE calls. Default is 6 hours.
121+
*
122+
* <p>
123+
* The socket timeout for SPARQL SERVICE calls is set to a shorter duration to detect unresponsive servers during
124+
* data transfer, ensuring that the client does not wait indefinitely for data that may never arrive.
125+
* </p>
126+
*/
127+
public static final String SPARQL_SOCKET_TIMEOUT_PROPERTY = "org.eclipse.rdf4j.client.sparql.http.socketTimeout";
128+
129+
// Default timeout values for general use
130+
131+
/**
132+
* Default HTTP connection timeout in milliseconds for general use. Set to 1 hour.
133+
*/
134+
public static final int DEFAULT_CONNECTION_TIMEOUT = 60 * 60 * 1000; // 1 hour
135+
136+
/**
137+
* Default HTTP connection request timeout in milliseconds for general use. Set to 10 days.
138+
*/
139+
public static final int DEFAULT_CONNECTION_REQUEST_TIMEOUT = 10 * 24 * 60 * 60 * 1000; // 10 days
140+
141+
/**
142+
* Default HTTP socket timeout in milliseconds for general use. Set to 10 days.
143+
*/
144+
public static final int DEFAULT_SOCKET_TIMEOUT = 10 * 24 * 60 * 60 * 1000; // 10 days
145+
146+
// Default timeout values for SPARQL SERVICE calls
147+
148+
/**
149+
* Default HTTP connection timeout in milliseconds for SPARQL SERVICE calls. Set to 10 minutes.
150+
*/
151+
public static final int DEFAULT_SPARQL_CONNECTION_TIMEOUT = 10 * 60 * 1000; // 10 minutes
152+
153+
/**
154+
* Default HTTP connection request timeout in milliseconds for SPARQL SERVICE calls. Set to 6 hours.
155+
*/
156+
public static final int DEFAULT_SPARQL_CONNECTION_REQUEST_TIMEOUT = 6 * 60 * 60 * 1000; // 6 hours
157+
158+
/**
159+
* Default HTTP socket timeout in milliseconds for SPARQL SERVICE calls. Set to 6 hours.
160+
*/
161+
public static final int DEFAULT_SPARQL_SOCKET_TIMEOUT = 6 * 60 * 60 * 1000; // 6 hours
162+
163+
// Timeout values as read from system properties or defaults
164+
165+
/**
166+
* HTTP connection timeout in milliseconds for general use.
167+
*/
168+
public static final int CONNECTION_TIMEOUT = Integer.parseInt(
169+
System.getProperty(CONNECTION_TIMEOUT_PROPERTY, String.valueOf(DEFAULT_CONNECTION_TIMEOUT))
170+
);
171+
172+
/**
173+
* HTTP connection request timeout in milliseconds for general use.
174+
*/
175+
public static final int CONNECTION_REQUEST_TIMEOUT = Integer.parseInt(
176+
System.getProperty(CONNECTION_REQUEST_TIMEOUT_PROPERTY, String.valueOf(DEFAULT_CONNECTION_REQUEST_TIMEOUT))
177+
);
178+
179+
/**
180+
* HTTP socket timeout in milliseconds for general use.
181+
*/
182+
public static final int SOCKET_TIMEOUT = Integer.parseInt(
183+
System.getProperty(SOCKET_TIMEOUT_PROPERTY, String.valueOf(DEFAULT_SOCKET_TIMEOUT))
184+
);
185+
186+
/**
187+
* HTTP connection timeout in milliseconds for SPARQL SERVICE calls.
188+
*/
189+
public static final int SPARQL_CONNECTION_TIMEOUT = Integer.parseInt(
190+
System.getProperty(SPARQL_CONNECTION_TIMEOUT_PROPERTY, String.valueOf(DEFAULT_SPARQL_CONNECTION_TIMEOUT))
191+
);
192+
193+
/**
194+
* HTTP connection request timeout in milliseconds for SPARQL SERVICE calls.
195+
*/
196+
public static final int SPARQL_CONNECTION_REQUEST_TIMEOUT = Integer.parseInt(
197+
System.getProperty(SPARQL_CONNECTION_REQUEST_TIMEOUT_PROPERTY,
198+
String.valueOf(DEFAULT_SPARQL_CONNECTION_REQUEST_TIMEOUT))
199+
);
200+
201+
/**
202+
* HTTP socket timeout in milliseconds for SPARQL SERVICE calls.
203+
*/
204+
public static final int SPARQL_SOCKET_TIMEOUT = Integer.parseInt(
205+
System.getProperty(SPARQL_SOCKET_TIMEOUT_PROPERTY, String.valueOf(DEFAULT_SPARQL_SOCKET_TIMEOUT))
206+
);
207+
208+
// Variables for the currently used timeouts
209+
210+
private int currentConnectionTimeout = CONNECTION_TIMEOUT;
211+
private int currentConnectionRequestTimeout = CONNECTION_REQUEST_TIMEOUT;
212+
private int currentSocketTimeout = SOCKET_TIMEOUT;
213+
56214
private final Logger logger = LoggerFactory.getLogger(SharedHttpClientSessionManager.class);
57215

58216
/**
@@ -267,6 +425,7 @@ public void close() {
267425
@Override
268426
public void shutDown() {
269427
try {
428+
// Close open sessions
270429
openSessions.keySet().forEach(session -> {
271430
try {
272431
session.close();
@@ -280,11 +439,11 @@ public void shutDown() {
280439
HttpClientUtils.closeQuietly(toCloseDependentClient);
281440
}
282441
} finally {
442+
// Shutdown the executor
283443
try {
284444
executor.shutdown();
285445
executor.awaitTermination(10, TimeUnit.SECONDS);
286446
} catch (InterruptedException e) {
287-
// Preserve the interrupt status so others can check it as necessary
288447
Thread.currentThread().interrupt();
289448
} finally {
290449
if (!executor.isTerminated()) {
@@ -314,17 +473,67 @@ protected final ExecutorService getExecutorService() {
314473
}
315474

316475
private CloseableHttpClient createHttpClient() {
476+
317477
HttpClientBuilder nextHttpClientBuilder = httpClientBuilder;
318478
if (nextHttpClientBuilder != null) {
319479
return nextHttpClientBuilder.build();
320480
}
321481

482+
RequestConfig requestConfig = getDefaultRequestConfig();
483+
322484
return HttpClientBuilder.create()
323485
.evictExpiredConnections()
486+
.evictIdleConnections(30, TimeUnit.MINUTES)
324487
.setRetryHandler(retryHandlerStale)
325488
.setServiceUnavailableRetryStrategy(serviceUnavailableRetryHandler)
326489
.useSystemProperties()
327-
.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build())
490+
.setDefaultRequestConfig(requestConfig)
491+
.build();
492+
}
493+
494+
/**
495+
* Returns the default {@link RequestConfig} using the currently set timeout values.
496+
*
497+
* @return a configured {@link RequestConfig} with the current timeouts.
498+
*/
499+
public RequestConfig getDefaultRequestConfig() {
500+
return RequestConfig.custom()
501+
.setConnectTimeout(currentConnectionTimeout)
502+
.setConnectionRequestTimeout(currentConnectionRequestTimeout)
503+
.setSocketTimeout(currentSocketTimeout)
504+
.setCookieSpec(CookieSpecs.STANDARD)
328505
.build();
329506
}
507+
508+
/**
509+
* Switches the current timeout settings to use the SPARQL-specific timeouts. This method should be called when
510+
* making SPARQL SERVICE calls to apply shorter timeout values.
511+
*
512+
* <p>
513+
* The SPARQL-specific timeouts are shorter to ensure that unresponsive or slow SPARQL endpoints do not cause long
514+
* delays in federated query processing. Quick detection of such issues improves the responsiveness and reliability
515+
* of SPARQL queries.
516+
* </p>
517+
*/
518+
public void setDefaultSparqlServiceTimeouts() {
519+
this.currentConnectionTimeout = SPARQL_CONNECTION_TIMEOUT;
520+
this.currentConnectionRequestTimeout = SPARQL_CONNECTION_REQUEST_TIMEOUT;
521+
this.currentSocketTimeout = SPARQL_SOCKET_TIMEOUT;
522+
}
523+
524+
/**
525+
* Resets the current timeout settings to the general timeouts. This method should be called to revert any changes
526+
* made by {@link #setDefaultSparqlServiceTimeouts()} and apply the general timeout values.
527+
*
528+
* <p>
529+
* The general timeouts are longer to accommodate operations that may take more time, such as large data transfers
530+
* or extensive processing, without causing premature timeouts.
531+
* </p>
532+
*/
533+
public void setDefaultTimeouts() {
534+
this.currentConnectionTimeout = CONNECTION_TIMEOUT;
535+
this.currentConnectionRequestTimeout = CONNECTION_REQUEST_TIMEOUT;
536+
this.currentSocketTimeout = SOCKET_TIMEOUT;
537+
}
538+
330539
}

core/repository/sparql/src/main/java/org/eclipse/rdf4j/repository/sparql/federation/SPARQLServiceResolver.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,19 @@ public void setHttpClientSessionManager(HttpClientSessionManager client) {
7676

7777
@Override
7878
public HttpClient getHttpClient() {
79-
return getHttpClientSessionManager().getHttpClient();
79+
HttpClientSessionManager httpClientSessionManager = getHttpClientSessionManager();
80+
81+
try {
82+
if (httpClientSessionManager instanceof SharedHttpClientSessionManager) {
83+
((SharedHttpClientSessionManager) httpClientSessionManager).setDefaultSparqlServiceTimeouts();
84+
}
85+
return getHttpClientSessionManager().getHttpClient();
86+
} finally {
87+
if (httpClientSessionManager instanceof SharedHttpClientSessionManager) {
88+
((SharedHttpClientSessionManager) httpClientSessionManager).setDefaultTimeouts();
89+
}
90+
}
91+
8092
}
8193

8294
@Override
@@ -86,6 +98,7 @@ public void setHttpClient(HttpClient httpClient) {
8698
getHttpClientSessionManager();
8799
toSetDependentClient = dependentClient;
88100
}
101+
89102
// The strange lifecycle results in the possibility that the
90103
// dependentClient will be null due to a call to setSesameClient, so add
91104
// a null guard here for that possibility

tools/federation/src/main/java/org/eclipse/rdf4j/federated/endpoint/provider/RemoteRepositoryProvider.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
*******************************************************************************/
1111
package org.eclipse.rdf4j.federated.endpoint.provider;
1212

13+
import org.apache.http.client.config.CookieSpecs;
14+
import org.apache.http.client.config.RequestConfig;
1315
import org.apache.http.impl.client.HttpClientBuilder;
1416
import org.apache.http.impl.client.HttpClients;
1517
import org.eclipse.rdf4j.federated.endpoint.Endpoint;
@@ -40,13 +42,19 @@ public Endpoint loadEndpoint(RemoteRepositoryRepositoryInformation repoInfo)
4042
}
4143

4244
try {
45+
4346
HTTPRepository repo = new HTTPRepository(repositoryServer, repositoryName);
47+
SharedHttpClientSessionManager httpClientSessionManager = (SharedHttpClientSessionManager) repo
48+
.getHttpClientSessionManager();
49+
4450
HttpClientBuilder httpClientBuilder = HttpClients.custom()
4551
.useSystemProperties()
52+
.setDefaultRequestConfig(httpClientSessionManager.getDefaultRequestConfig())
4653
.setMaxConnTotal(20)
4754
.setMaxConnPerRoute(20);
48-
((SharedHttpClientSessionManager) repo.getHttpClientSessionManager())
49-
.setHttpClientBuilder(httpClientBuilder);
55+
56+
httpClientSessionManager.setHttpClientBuilder(httpClientBuilder);
57+
5058
try {
5159
repo.init();
5260
} finally {

0 commit comments

Comments
 (0)