Skip to content

Commit d0baaee

Browse files
committed
[grid] Simplify httpClientCache manages in handle session of Router
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 8b45f02 commit d0baaee

File tree

3 files changed

+53
-103
lines changed

3 files changed

+53
-103
lines changed

java/src/org/openqa/selenium/grid/router/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ java_library(
2424
"//java/src/org/openqa/selenium/remote",
2525
"//java/src/org/openqa/selenium/status",
2626
artifact("com.google.guava:guava"),
27+
artifact("com.github.ben-manes.caffeine:caffeine"),
2728
],
2829
)

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
import java.util.concurrent.ExecutorService;
3636
import java.util.concurrent.Executors;
3737
import java.util.concurrent.TimeoutException;
38+
import java.util.logging.Logger;
39+
import org.openqa.selenium.concurrent.ExecutorServices;
3840
import org.openqa.selenium.grid.data.DistributorStatus;
3941
import org.openqa.selenium.grid.distributor.Distributor;
4042
import org.openqa.selenium.internal.Require;
@@ -47,8 +49,9 @@
4749
import org.openqa.selenium.remote.tracing.Status;
4850
import org.openqa.selenium.remote.tracing.Tracer;
4951

50-
class GridStatusHandler implements HttpHandler {
52+
class GridStatusHandler implements HttpHandler, AutoCloseable {
5153

54+
private static final Logger LOG = Logger.getLogger(GridStatusHandler.class.getName());
5255
private static final ExecutorService EXECUTOR_SERVICE =
5356
Executors.newCachedThreadPool(
5457
r -> {
@@ -165,4 +168,10 @@ public HttpResponse execute(HttpRequest req) {
165168
return res;
166169
}
167170
}
171+
172+
@Override
173+
public void close() {
174+
LOG.info("Shutting down GridStatusHandler executor service");
175+
ExecutorServices.shutdownGracefully("Grid status executor", EXECUTOR_SERVICE);
176+
}
168177
}

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

Lines changed: 42 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,16 @@
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.github.benmanes.caffeine.cache.Cache;
30+
import com.github.benmanes.caffeine.cache.Caffeine;
31+
import com.github.benmanes.caffeine.cache.RemovalListener;
2932
import java.io.Closeable;
3033
import java.net.URI;
31-
import java.time.Instant;
32-
import java.time.temporal.ChronoUnit;
33-
import java.util.Iterator;
34+
import java.time.Duration;
3435
import java.util.concurrent.Callable;
35-
import java.util.concurrent.ConcurrentHashMap;
36-
import java.util.concurrent.ConcurrentMap;
37-
import java.util.concurrent.Executors;
38-
import java.util.concurrent.ScheduledExecutorService;
39-
import java.util.concurrent.TimeUnit;
40-
import java.util.concurrent.atomic.AtomicLong;
4136
import java.util.logging.Level;
4237
import java.util.logging.Logger;
4338
import org.openqa.selenium.NoSuchSessionException;
44-
import org.openqa.selenium.concurrent.ExecutorServices;
45-
import org.openqa.selenium.concurrent.GuardedRunnable;
4639
import org.openqa.selenium.grid.sessionmap.SessionMap;
4740
import org.openqa.selenium.grid.web.ReverseProxyHandler;
4841
import org.openqa.selenium.internal.Require;
@@ -64,88 +57,47 @@ class HandleSession implements HttpHandler, Closeable {
6457

6558
private static final Logger LOG = Logger.getLogger(HandleSession.class.getName());
6659

67-
private static class CacheEntry {
68-
private final HttpClient httpClient;
69-
private final AtomicLong inUse;
70-
// volatile as the ConcurrentMap will not take care of synchronization
71-
private volatile Instant lastUse;
72-
73-
public CacheEntry(HttpClient httpClient, long initialUsage) {
74-
this.httpClient = httpClient;
75-
this.inUse = new AtomicLong(initialUsage);
76-
this.lastUse = Instant.now();
77-
}
78-
}
79-
80-
private static class UsageCountingReverseProxyHandler extends ReverseProxyHandler
60+
private static class ReverseProxyHandlerCloseable extends ReverseProxyHandler
8161
implements Closeable {
82-
private final CacheEntry entry;
8362

84-
public UsageCountingReverseProxyHandler(
85-
Tracer tracer, HttpClient httpClient, CacheEntry entry) {
63+
public ReverseProxyHandlerCloseable(Tracer tracer, HttpClient httpClient) {
8664
super(tracer, httpClient);
87-
88-
this.entry = entry;
8965
}
9066

9167
@Override
9268
public void close() {
93-
// set the last use here, to ensure we have to calculate the real inactivity of the client
94-
entry.lastUse = Instant.now();
95-
entry.inUse.decrementAndGet();
69+
// No operation needed - cache management is handled by Cache builder
9670
}
9771
}
9872

9973
private final Tracer tracer;
10074
private final HttpClient.Factory httpClientFactory;
10175
private final SessionMap sessions;
102-
private final ConcurrentMap<URI, CacheEntry> httpClients;
103-
private final ScheduledExecutorService cleanUpHttpClientsCacheService;
76+
private final Cache<URI, HttpClient> httpClientCache;
10477

10578
HandleSession(Tracer tracer, HttpClient.Factory httpClientFactory, SessionMap sessions) {
10679
this.tracer = Require.nonNull("Tracer", tracer);
10780
this.httpClientFactory = Require.nonNull("HTTP client factory", httpClientFactory);
10881
this.sessions = Require.nonNull("Sessions", sessions);
10982

110-
this.httpClients = new ConcurrentHashMap<>();
111-
112-
Runnable cleanUpHttpClients =
113-
() -> {
114-
Instant staleBefore = Instant.now().minus(2, ChronoUnit.MINUTES);
115-
Iterator<CacheEntry> iterator = httpClients.values().iterator();
116-
117-
while (iterator.hasNext()) {
118-
CacheEntry entry = iterator.next();
119-
120-
if (entry.inUse.get() != 0) {
121-
// the client is currently in use
122-
return;
123-
} else if (!entry.lastUse.isBefore(staleBefore)) {
124-
// the client was recently used
125-
return;
126-
} else {
127-
// the client has not been used for a while, remove it from the cache
128-
iterator.remove();
129-
130-
try {
131-
entry.httpClient.close();
132-
} catch (Exception ex) {
133-
LOG.log(Level.WARNING, "failed to close a stale httpclient", ex);
134-
}
135-
}
136-
}
137-
};
138-
139-
this.cleanUpHttpClientsCacheService =
140-
Executors.newSingleThreadScheduledExecutor(
141-
r -> {
142-
Thread thread = new Thread(r);
143-
thread.setDaemon(true);
144-
thread.setName("HandleSession - Clean up http clients cache");
145-
return thread;
146-
});
147-
cleanUpHttpClientsCacheService.scheduleAtFixedRate(
148-
GuardedRunnable.guard(cleanUpHttpClients), 1, 1, TimeUnit.MINUTES);
83+
// Create Cache with 2 minute expiry after last access
84+
// and a removal listener to close HTTP clients
85+
this.httpClientCache =
86+
Caffeine.newBuilder()
87+
.expireAfterAccess(Duration.ofMinutes(2))
88+
.removalListener(
89+
(RemovalListener<URI, HttpClient>)
90+
(uri, httpClient, cause) -> {
91+
if (httpClient != null) {
92+
try {
93+
LOG.fine("Closing HTTP client for " + uri + ", removal cause: " + cause);
94+
httpClient.close();
95+
} catch (Exception ex) {
96+
LOG.log(Level.WARNING, "Failed to close HTTP client for " + uri, ex);
97+
}
98+
}
99+
})
100+
.build();
149101
}
150102

151103
@Override
@@ -179,7 +131,7 @@ public HttpResponse execute(HttpRequest req) {
179131
try {
180132
HttpTracing.inject(tracer, span, req);
181133
HttpResponse res;
182-
try (UsageCountingReverseProxyHandler handler = loadSessionId(tracer, span, id).call()) {
134+
try (ReverseProxyHandlerCloseable handler = loadSessionId(tracer, span, id).call()) {
183135
res = handler.execute(req);
184136
}
185137

@@ -216,46 +168,34 @@ public HttpResponse execute(HttpRequest req) {
216168
}
217169
}
218170

219-
private Callable<UsageCountingReverseProxyHandler> loadSessionId(
171+
private Callable<ReverseProxyHandlerCloseable> loadSessionId(
220172
Tracer tracer, Span span, SessionId id) {
221173
return span.wrap(
222174
() -> {
223-
CacheEntry cacheEntry =
224-
httpClients.compute(
225-
sessions.getUri(id),
226-
(sessionUri, entry) -> {
227-
if (entry != null) {
228-
entry.inUse.incrementAndGet();
229-
return entry;
230-
}
231-
232-
ClientConfig config =
233-
ClientConfig.defaultConfig().baseUri(sessionUri).withRetries();
234-
HttpClient httpClient = httpClientFactory.createClient(config);
235-
236-
return new CacheEntry(httpClient, 1);
175+
URI sessionUri = sessions.getUri(id);
176+
177+
// Get or create the HTTP client from cache (this also updates the "last access" time)
178+
HttpClient httpClient =
179+
httpClientCache.get(
180+
sessionUri,
181+
uri -> {
182+
LOG.fine("Creating new HTTP client for " + uri);
183+
ClientConfig config = ClientConfig.defaultConfig().baseUri(uri).withRetries();
184+
return httpClientFactory.createClient(config);
237185
});
238186

239187
try {
240-
return new UsageCountingReverseProxyHandler(tracer, cacheEntry.httpClient, cacheEntry);
188+
return new ReverseProxyHandlerCloseable(tracer, httpClient);
241189
} catch (Throwable t) {
242-
// ensure we do not keep the http client when an unexpected throwable is raised
243-
cacheEntry.inUse.decrementAndGet();
244190
throw t;
245191
}
246192
});
247193
}
248194

249195
@Override
250196
public void close() {
251-
ExecutorServices.shutdownGracefully(
252-
"HandleSession - Clean up http clients cache", cleanUpHttpClientsCacheService);
253-
httpClients
254-
.values()
255-
.removeIf(
256-
(entry) -> {
257-
entry.httpClient.close();
258-
return true;
259-
});
197+
// This will trigger the removal listener for all entries, which will close all HTTP clients
198+
httpClientCache.invalidateAll();
199+
httpClientCache.cleanUp();
260200
}
261201
}

0 commit comments

Comments
 (0)