Skip to content

Commit 600a614

Browse files
joerg1985diemol
andauthored
[grid] keep HttpClient alive until unused #12558 (#12978)
* fixed merge conflict * [grid] use a URI as key to the map * [java] make the json parsing exception text more helpful * [java] fixed the get session url endpoint * [java] use sessions.getUri when only the session uri is needed * Revert "[java] make the json parsing exception text more helpful" This reverts commit ce7cfc8. * [java] format script --------- Co-authored-by: Diego Molina <[email protected]>
1 parent de22f34 commit 600a614

File tree

3 files changed

+108
-28
lines changed

3 files changed

+108
-28
lines changed

java/src/org/openqa/selenium/grid/router/HandleSession.java

Lines changed: 103 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,25 @@
2626
import static org.openqa.selenium.remote.tracing.Tags.HTTP_REQUEST_EVENT;
2727
import static org.openqa.selenium.remote.tracing.Tags.HTTP_RESPONSE;
2828

29-
import com.google.common.cache.Cache;
30-
import com.google.common.cache.CacheBuilder;
31-
import com.google.common.cache.RemovalListener;
32-
import java.net.URL;
33-
import java.time.Duration;
29+
import java.io.Closeable;
30+
import java.net.URI;
31+
import java.time.Instant;
32+
import java.time.temporal.ChronoUnit;
33+
import java.util.Iterator;
3434
import java.util.concurrent.Callable;
35+
import java.util.concurrent.ConcurrentHashMap;
36+
import java.util.concurrent.ConcurrentMap;
3537
import java.util.concurrent.Executors;
3638
import java.util.concurrent.ScheduledExecutorService;
3739
import java.util.concurrent.TimeUnit;
40+
import java.util.concurrent.atomic.AtomicLong;
41+
import java.util.logging.Level;
42+
import java.util.logging.Logger;
3843
import org.openqa.selenium.NoSuchSessionException;
3944
import org.openqa.selenium.concurrent.GuardedRunnable;
40-
import org.openqa.selenium.grid.data.Session;
4145
import org.openqa.selenium.grid.sessionmap.SessionMap;
4246
import org.openqa.selenium.grid.web.ReverseProxyHandler;
4347
import org.openqa.selenium.internal.Require;
44-
import org.openqa.selenium.net.Urls;
4548
import org.openqa.selenium.remote.ErrorCodec;
4649
import org.openqa.selenium.remote.SessionId;
4750
import org.openqa.selenium.remote.http.ClientConfig;
@@ -58,24 +61,78 @@
5861

5962
class HandleSession implements HttpHandler {
6063

64+
private static final Logger LOG = Logger.getLogger(HandleSession.class.getName());
65+
66+
private static class CacheEntry {
67+
private final HttpClient httpClient;
68+
private final AtomicLong inUse;
69+
// volatile as the ConcurrentMap will not take care of synchronization
70+
private volatile Instant lastUse;
71+
72+
public CacheEntry(HttpClient httpClient, long initialUsage) {
73+
this.httpClient = httpClient;
74+
this.inUse = new AtomicLong(initialUsage);
75+
this.lastUse = Instant.now();
76+
}
77+
}
78+
79+
private static class UsageCountingReverseProxyHandler extends ReverseProxyHandler
80+
implements Closeable {
81+
private final CacheEntry entry;
82+
83+
public UsageCountingReverseProxyHandler(
84+
Tracer tracer, HttpClient httpClient, CacheEntry entry) {
85+
super(tracer, httpClient);
86+
87+
this.entry = entry;
88+
}
89+
90+
@Override
91+
public void close() {
92+
// set the last use here, to ensure we have to calculate the real inactivity of the client
93+
entry.lastUse = Instant.now();
94+
entry.inUse.decrementAndGet();
95+
}
96+
}
97+
6198
private final Tracer tracer;
6299
private final HttpClient.Factory httpClientFactory;
63100
private final SessionMap sessions;
64-
private final Cache<URL, HttpClient> httpClients;
101+
private final ConcurrentMap<URI, CacheEntry> httpClients;
65102

66103
HandleSession(Tracer tracer, HttpClient.Factory httpClientFactory, SessionMap sessions) {
67104
this.tracer = Require.nonNull("Tracer", tracer);
68105
this.httpClientFactory = Require.nonNull("HTTP client factory", httpClientFactory);
69106
this.sessions = Require.nonNull("Sessions", sessions);
70107

71-
this.httpClients =
72-
CacheBuilder.newBuilder()
73-
// this timeout must be bigger than default connection + read timeout, to ensure we do
74-
// not close HttpClients which might have requests waiting for responses
75-
.expireAfterAccess(Duration.ofMinutes(4))
76-
.removalListener(
77-
(RemovalListener<URL, HttpClient>) removal -> removal.getValue().close())
78-
.build();
108+
this.httpClients = new ConcurrentHashMap<>();
109+
110+
Runnable cleanUpHttpClients =
111+
() -> {
112+
Instant staleBefore = Instant.now().minus(2, ChronoUnit.MINUTES);
113+
Iterator<CacheEntry> iterator = httpClients.values().iterator();
114+
115+
while (iterator.hasNext()) {
116+
CacheEntry entry = iterator.next();
117+
118+
if (entry.inUse.get() != 0) {
119+
// the client is currently in use
120+
return;
121+
} else if (!entry.lastUse.isBefore(staleBefore)) {
122+
// the client was recently used
123+
return;
124+
} else {
125+
// the client has not been used for a while, remove it from the cache
126+
iterator.remove();
127+
128+
try {
129+
entry.httpClient.close();
130+
} catch (Exception ex) {
131+
LOG.log(Level.WARNING, "failed to close a stale httpclient", ex);
132+
}
133+
}
134+
}
135+
};
79136

80137
ScheduledExecutorService cleanUpHttpClientsCacheService =
81138
Executors.newSingleThreadScheduledExecutor(
@@ -86,7 +143,7 @@ class HandleSession implements HttpHandler {
86143
return thread;
87144
});
88145
cleanUpHttpClientsCacheService.scheduleAtFixedRate(
89-
GuardedRunnable.guard(httpClients::cleanUp), 1, 1, TimeUnit.MINUTES);
146+
GuardedRunnable.guard(cleanUpHttpClients), 1, 1, TimeUnit.MINUTES);
90147
}
91148

92149
@Override
@@ -119,7 +176,10 @@ public HttpResponse execute(HttpRequest req) {
119176

120177
try {
121178
HttpTracing.inject(tracer, span, req);
122-
HttpResponse res = loadSessionId(tracer, span, id).call().execute(req);
179+
HttpResponse res;
180+
try (UsageCountingReverseProxyHandler handler = loadSessionId(tracer, span, id).call()) {
181+
res = handler.execute(req);
182+
}
123183

124184
HTTP_RESPONSE.accept(span, res);
125185

@@ -154,14 +214,33 @@ public HttpResponse execute(HttpRequest req) {
154214
}
155215
}
156216

157-
private Callable<HttpHandler> loadSessionId(Tracer tracer, Span span, SessionId id) {
217+
private Callable<UsageCountingReverseProxyHandler> loadSessionId(
218+
Tracer tracer, Span span, SessionId id) {
158219
return span.wrap(
159220
() -> {
160-
Session session = sessions.get(id);
161-
URL url = Urls.fromUri(session.getUri());
162-
ClientConfig config = ClientConfig.defaultConfig().baseUrl(url).withRetries();
163-
HttpClient client = httpClients.get(url, () -> httpClientFactory.createClient(config));
164-
return new ReverseProxyHandler(tracer, client);
221+
CacheEntry cacheEntry =
222+
httpClients.compute(
223+
sessions.getUri(id),
224+
(sessionUri, entry) -> {
225+
if (entry != null) {
226+
entry.inUse.incrementAndGet();
227+
return entry;
228+
}
229+
230+
ClientConfig config =
231+
ClientConfig.defaultConfig().baseUri(sessionUri).withRetries();
232+
HttpClient httpClient = httpClientFactory.createClient(config);
233+
234+
return new CacheEntry(httpClient, 1);
235+
});
236+
237+
try {
238+
return new UsageCountingReverseProxyHandler(tracer, cacheEntry.httpClient, cacheEntry);
239+
} catch (Throwable t) {
240+
// ensure we do not keep the http client when an unexpected throwable is raised
241+
cacheEntry.inUse.decrementAndGet();
242+
throw t;
243+
}
165244
});
166245
}
167246
}

java/src/org/openqa/selenium/grid/router/ProxyWebsocketsIntoGrid.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@
1919

2020
import static org.openqa.selenium.remote.http.HttpMethod.GET;
2121

22+
import java.net.URI;
2223
import java.util.Objects;
2324
import java.util.Optional;
2425
import java.util.function.BiFunction;
2526
import java.util.function.Consumer;
2627
import java.util.logging.Level;
2728
import java.util.logging.Logger;
2829
import org.openqa.selenium.NoSuchSessionException;
29-
import org.openqa.selenium.grid.data.Session;
3030
import org.openqa.selenium.grid.sessionmap.SessionMap;
3131
import org.openqa.selenium.remote.HttpSessionId;
3232
import org.openqa.selenium.remote.SessionId;
@@ -62,10 +62,10 @@ public Optional<Consumer<Message>> apply(String uri, Consumer<Message> downstrea
6262
}
6363

6464
try {
65-
Session session = sessions.get(sessionId.get());
65+
URI sessionUri = sessions.getUri(sessionId.get());
6666

6767
HttpClient client =
68-
clientFactory.createClient(ClientConfig.defaultConfig().baseUri(session.getUri()));
68+
clientFactory.createClient(ClientConfig.defaultConfig().baseUri(sessionUri));
6969
WebSocket upstream =
7070
client.openSocket(new HttpRequest(GET, uri), new ForwardingListener(downstream));
7171

java/src/org/openqa/selenium/grid/sessionmap/GetSessionUri.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.openqa.selenium.remote.http.Contents.asJson;
2121

2222
import java.io.UncheckedIOException;
23+
import java.util.Map;
2324
import org.openqa.selenium.internal.Require;
2425
import org.openqa.selenium.remote.SessionId;
2526
import org.openqa.selenium.remote.http.HttpHandler;
@@ -37,6 +38,6 @@ class GetSessionUri implements HttpHandler {
3738

3839
@Override
3940
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
40-
return new HttpResponse().setContent(asJson(sessionMap.getUri(sessionId)));
41+
return new HttpResponse().setContent(asJson(Map.of("value", sessionMap.getUri(sessionId))));
4142
}
4243
}

0 commit comments

Comments
 (0)