2626import static org .openqa .selenium .remote .tracing .Tags .HTTP_REQUEST_EVENT ;
2727import 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 ;
2932import java .io .Closeable ;
3033import java .net .URI ;
31- import java .time .Instant ;
32- import java .time .temporal .ChronoUnit ;
33- import java .util .Iterator ;
34+ import java .time .Duration ;
3435import 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 ;
4136import java .util .logging .Level ;
4237import java .util .logging .Logger ;
4338import org .openqa .selenium .NoSuchSessionException ;
44- import org .openqa .selenium .concurrent .ExecutorServices ;
45- import org .openqa .selenium .concurrent .GuardedRunnable ;
4639import org .openqa .selenium .grid .sessionmap .SessionMap ;
4740import org .openqa .selenium .grid .web .ReverseProxyHandler ;
4841import 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