26
26
import static org .openqa .selenium .remote .tracing .Tags .HTTP_REQUEST_EVENT ;
27
27
import static org .openqa .selenium .remote .tracing .Tags .HTTP_RESPONSE ;
28
28
29
+ import com .github .benmanes .caffeine .cache .Cache ;
30
+ import com .github .benmanes .caffeine .cache .Caffeine ;
31
+ import com .github .benmanes .caffeine .cache .RemovalListener ;
29
32
import java .io .Closeable ;
30
33
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 ;
34
35
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 ;
41
36
import java .util .logging .Level ;
42
37
import java .util .logging .Logger ;
43
38
import org .openqa .selenium .NoSuchSessionException ;
44
- import org .openqa .selenium .concurrent .ExecutorServices ;
45
- import org .openqa .selenium .concurrent .GuardedRunnable ;
46
39
import org .openqa .selenium .grid .sessionmap .SessionMap ;
47
40
import org .openqa .selenium .grid .web .ReverseProxyHandler ;
48
41
import org .openqa .selenium .internal .Require ;
@@ -64,88 +57,47 @@ class HandleSession implements HttpHandler, Closeable {
64
57
65
58
private static final Logger LOG = Logger .getLogger (HandleSession .class .getName ());
66
59
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
81
61
implements Closeable {
82
- private final CacheEntry entry ;
83
62
84
- public UsageCountingReverseProxyHandler (
85
- Tracer tracer , HttpClient httpClient , CacheEntry entry ) {
63
+ public ReverseProxyHandlerCloseable (Tracer tracer , HttpClient httpClient ) {
86
64
super (tracer , httpClient );
87
-
88
- this .entry = entry ;
89
65
}
90
66
91
67
@ Override
92
68
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
96
70
}
97
71
}
98
72
99
73
private final Tracer tracer ;
100
74
private final HttpClient .Factory httpClientFactory ;
101
75
private final SessionMap sessions ;
102
- private final ConcurrentMap <URI , CacheEntry > httpClients ;
103
- private final ScheduledExecutorService cleanUpHttpClientsCacheService ;
76
+ private final Cache <URI , HttpClient > httpClientCache ;
104
77
105
78
HandleSession (Tracer tracer , HttpClient .Factory httpClientFactory , SessionMap sessions ) {
106
79
this .tracer = Require .nonNull ("Tracer" , tracer );
107
80
this .httpClientFactory = Require .nonNull ("HTTP client factory" , httpClientFactory );
108
81
this .sessions = Require .nonNull ("Sessions" , sessions );
109
82
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 ();
149
101
}
150
102
151
103
@ Override
@@ -179,7 +131,7 @@ public HttpResponse execute(HttpRequest req) {
179
131
try {
180
132
HttpTracing .inject (tracer , span , req );
181
133
HttpResponse res ;
182
- try (UsageCountingReverseProxyHandler handler = loadSessionId (tracer , span , id ).call ()) {
134
+ try (ReverseProxyHandlerCloseable handler = loadSessionId (tracer , span , id ).call ()) {
183
135
res = handler .execute (req );
184
136
}
185
137
@@ -216,46 +168,34 @@ public HttpResponse execute(HttpRequest req) {
216
168
}
217
169
}
218
170
219
- private Callable <UsageCountingReverseProxyHandler > loadSessionId (
171
+ private Callable <ReverseProxyHandlerCloseable > loadSessionId (
220
172
Tracer tracer , Span span , SessionId id ) {
221
173
return span .wrap (
222
174
() -> {
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 );
237
185
});
238
186
239
187
try {
240
- return new UsageCountingReverseProxyHandler (tracer , cacheEntry . httpClient , cacheEntry );
188
+ return new ReverseProxyHandlerCloseable (tracer , httpClient );
241
189
} catch (Throwable t ) {
242
- // ensure we do not keep the http client when an unexpected throwable is raised
243
- cacheEntry .inUse .decrementAndGet ();
244
190
throw t ;
245
191
}
246
192
});
247
193
}
248
194
249
195
@ Override
250
196
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 ();
260
200
}
261
201
}
0 commit comments