Skip to contentMethod: HttpClientFactory.ServiceUnavailableRetryHandler()
1: /*
2: * JOPA
3: * Copyright (C) 2024 Czech Technical University in Prague
4: *
5: * This library is free software; you can redistribute it and/or
6: * modify it under the terms of the GNU Lesser General Public
7: * License as published by the Free Software Foundation; either
8: * version 3.0 of the License, or (at your option) any later version.
9: *
10: * This library is distributed in the hope that it will be useful,
11: * but WITHOUT ANY WARRANTY; without even the implied warranty of
12: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13: * Lesser General Public License for more details.
14: *
15: * You should have received a copy of the GNU Lesser General Public
16: * License along with this library.
17: */
18: package cz.cvut.kbss.ontodriver.rdf4j.connector.init;
19:
20: import cz.cvut.kbss.ontodriver.config.DriverConfiguration;
21: import cz.cvut.kbss.ontodriver.rdf4j.config.Constants;
22: import cz.cvut.kbss.ontodriver.rdf4j.config.Rdf4jConfigParam;
23: import org.apache.http.HttpConnection;
24: import org.apache.http.HttpResponse;
25: import org.apache.http.client.HttpClient;
26: import org.apache.http.client.HttpRequestRetryHandler;
27: import org.apache.http.client.ServiceUnavailableRetryStrategy;
28: import org.apache.http.client.config.CookieSpecs;
29: import org.apache.http.client.config.RequestConfig;
30: import org.apache.http.client.protocol.HttpClientContext;
31: import org.apache.http.impl.client.HttpClientBuilder;
32: import org.apache.http.protocol.HttpContext;
33: import org.eclipse.rdf4j.http.client.SharedHttpClientSessionManager;
34: import org.slf4j.Logger;
35: import org.slf4j.LoggerFactory;
36:
37: import java.io.IOException;
38: import java.net.HttpURLConnection;
39:
40: class HttpClientFactory {
41:
42: /**
43: * Creates a customized {@link HttpClient} instance.
44: * <p>
45: * The client's configuration is basically the same as the default one used by RDF4J, but it sets a timeout on
46: * connection requests from the connection pool, so that an application with exhausted connection pool fails
47: * gracefully instead of potentially going into a deadlock.
48: *
49: * @param configuration Driver configuration
50: * @return HttpClient instance with connection pool request timeout
51: */
52: static HttpClient createHttpClient(DriverConfiguration configuration) {
53: final RequestConfig customRequestConfig = RequestConfig.custom()
54: .setCookieSpec(CookieSpecs.STANDARD)
55: .setConnectionRequestTimeout(getConnectionRequestTimeout(configuration))
56: .build();
57: final int maxConnections = getMaxConnections(configuration);
58: return HttpClientBuilder.create()
59: .evictExpiredConnections()
60: .setRetryHandler(new RetryHandlerStale())
61: .setServiceUnavailableRetryStrategy(new ServiceUnavailableRetryHandler())
62: .useSystemProperties()
63: // Set max connections per route to the same value as max connections, as we are always
64: // connecting to the same remote (RDF4J server repository)
65: .setMaxConnPerRoute(maxConnections)
66: .setMaxConnTotal(maxConnections)
67: .setDefaultRequestConfig(customRequestConfig).build();
68: }
69:
70: private static int getConnectionRequestTimeout(DriverConfiguration config) {
71: return config.getProperty(Rdf4jConfigParam.CONNECTION_REQUEST_TIMEOUT, Constants.DEFAULT_CONNECTION_REQUEST_TIMEOUT);
72: }
73:
74: private static int getMaxConnections(DriverConfiguration config) {
75: final int defaultMaxConnections = Math.max(Constants.DEFAULT_MAX_CONNECTIONS, Runtime.getRuntime()
76: .availableProcessors() * 2);
77: return config.getProperty(Rdf4jConfigParam.MAX_CONNECTION_POOL_SIZE, defaultMaxConnections);
78: }
79:
80: /**
81: * Copied from {@link SharedHttpClientSessionManager}
82: */
83: private static class RetryHandlerStale implements HttpRequestRetryHandler {
84: private final Logger logger = LoggerFactory.getLogger(RetryHandlerStale.class);
85:
86: @Override
87: public boolean retryRequest(IOException ioe, int count, HttpContext context) {
88: // only try this once
89: if (count > 1) {
90: return false;
91: }
92: HttpClientContext clientContext = HttpClientContext.adapt(context);
93: HttpConnection conn = clientContext.getConnection();
94: if (conn != null) {
95: synchronized (this) {
96: if (conn.isStale()) {
97: try {
98: logger.warn("Closing stale connection");
99: conn.close();
100: return true;
101: } catch (IOException e) {
102: logger.error("Error closing stale connection", e);
103: }
104: }
105: }
106: }
107: return false;
108: }
109: }
110:
111: /**
112: * Copied from {@link SharedHttpClientSessionManager}
113: */
114: private static class ServiceUnavailableRetryHandler implements ServiceUnavailableRetryStrategy {
115: private final Logger logger = LoggerFactory.getLogger(ServiceUnavailableRetryHandler.class);
116:
117: @Override
118: public boolean retryRequest(HttpResponse response, int executionCount, HttpContext context) {
119: // only retry on `408`
120: if (response.getStatusLine().getStatusCode() != HttpURLConnection.HTTP_CLIENT_TIMEOUT) {
121: return false;
122: }
123:
124: // when `keepAlive` is disabled every connection is fresh (with the default `useSystemProperties` http
125: // client configuration we use), a 408 in that case is an unexpected issue we don't handle here
126: String keepAlive = System.getProperty("http.keepAlive", "true");
127: if (!"true".equalsIgnoreCase(keepAlive)) {
128: return false;
129: }
130:
131: // worst case, the connection pool is filled to the max and all of them idled out on the server already
132: // we then need to clean up the pool and finally retry with a fresh connection. Hence, we need at most
133: // pooledConnections+1 retries.
134: // the pool size setting used here is taken from `HttpClientBuilder` when `useSystemProperties()` is used
135: int pooledConnections = Integer.parseInt(System.getProperty("http.maxConnections", "5"));
136: if (executionCount > (pooledConnections + 1)) {
137: return false;
138: }
139:
140: HttpClientContext clientContext = HttpClientContext.adapt(context);
141: HttpConnection conn = clientContext.getConnection();
142:
143: synchronized (this) {
144: try {
145: logger.info("Cleaning up closed connection");
146: conn.close();
147: return true;
148: } catch (IOException e) {
149: logger.error("Error cleaning up closed connection", e);
150: }
151: }
152: return false;
153: }
154:
155: @Override
156: public long getRetryInterval() {
157: return 1000;
158: }
159: }
160: }