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 . 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 ;
34
34
import java .util .concurrent .Callable ;
35
+ import java .util .concurrent .ConcurrentHashMap ;
36
+ import java .util .concurrent .ConcurrentMap ;
35
37
import java .util .concurrent .Executors ;
36
38
import java .util .concurrent .ScheduledExecutorService ;
37
39
import java .util .concurrent .TimeUnit ;
40
+ import java .util .concurrent .atomic .AtomicLong ;
41
+ import java .util .logging .Level ;
42
+ import java .util .logging .Logger ;
38
43
import org .openqa .selenium .NoSuchSessionException ;
39
44
import org .openqa .selenium .concurrent .GuardedRunnable ;
40
- import org .openqa .selenium .grid .data .Session ;
41
45
import org .openqa .selenium .grid .sessionmap .SessionMap ;
42
46
import org .openqa .selenium .grid .web .ReverseProxyHandler ;
43
47
import org .openqa .selenium .internal .Require ;
44
- import org .openqa .selenium .net .Urls ;
45
48
import org .openqa .selenium .remote .ErrorCodec ;
46
49
import org .openqa .selenium .remote .SessionId ;
47
50
import org .openqa .selenium .remote .http .ClientConfig ;
58
61
59
62
class HandleSession implements HttpHandler {
60
63
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
+
61
98
private final Tracer tracer ;
62
99
private final HttpClient .Factory httpClientFactory ;
63
100
private final SessionMap sessions ;
64
- private final Cache < URL , HttpClient > httpClients ;
101
+ private final ConcurrentMap < URI , CacheEntry > httpClients ;
65
102
66
103
HandleSession (Tracer tracer , HttpClient .Factory httpClientFactory , SessionMap sessions ) {
67
104
this .tracer = Require .nonNull ("Tracer" , tracer );
68
105
this .httpClientFactory = Require .nonNull ("HTTP client factory" , httpClientFactory );
69
106
this .sessions = Require .nonNull ("Sessions" , sessions );
70
107
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
+ };
79
136
80
137
ScheduledExecutorService cleanUpHttpClientsCacheService =
81
138
Executors .newSingleThreadScheduledExecutor (
@@ -86,7 +143,7 @@ class HandleSession implements HttpHandler {
86
143
return thread ;
87
144
});
88
145
cleanUpHttpClientsCacheService .scheduleAtFixedRate (
89
- GuardedRunnable .guard (httpClients :: cleanUp ), 1 , 1 , TimeUnit .MINUTES );
146
+ GuardedRunnable .guard (cleanUpHttpClients ), 1 , 1 , TimeUnit .MINUTES );
90
147
}
91
148
92
149
@ Override
@@ -119,7 +176,10 @@ public HttpResponse execute(HttpRequest req) {
119
176
120
177
try {
121
178
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
+ }
123
183
124
184
HTTP_RESPONSE .accept (span , res );
125
185
@@ -154,14 +214,33 @@ public HttpResponse execute(HttpRequest req) {
154
214
}
155
215
}
156
216
157
- private Callable <HttpHandler > loadSessionId (Tracer tracer , Span span , SessionId id ) {
217
+ private Callable <UsageCountingReverseProxyHandler > loadSessionId (
218
+ Tracer tracer , Span span , SessionId id ) {
158
219
return span .wrap (
159
220
() -> {
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
+ }
165
244
});
166
245
}
167
246
}
0 commit comments