Skip to content

Commit 4ea71ca

Browse files
authored
[grid] decrement the connection per session counter on close #14842 (#14854)
1 parent 622cb43 commit 4ea71ca

File tree

10 files changed

+174
-11
lines changed

10 files changed

+174
-11
lines changed

java/src/org/openqa/selenium/grid/node/Node.java

+11
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@
101101
* by {@code sessionId}. This returns a boolean.</td>
102102
* </tr>
103103
* <tr>
104+
* <td>DELETE</td>
105+
* <td>/se/grid/node/connection/{sessionId}</td>
106+
* <td>Notifies the node about closure of a websocket connection for the {@link Session}
107+
* identified by {@code sessionId}.</td>
108+
* </tr>
109+
* <tr>
104110
* <td>POST</td>
105111
* <td>/se/grid/node/connection/{sessionId}</td>
106112
* <td>Allows the node to be ask about whether or not new websocket connections are allowed for the {@link Session}
@@ -173,6 +179,9 @@ protected Node(
173179
get("/se/grid/node/owner/{sessionId}")
174180
.to(params -> new IsSessionOwner(this, sessionIdFrom(params)))
175181
.with(spanDecorator("node.is_session_owner").andThen(requiresSecret)),
182+
delete("/se/grid/node/connection/{sessionId}")
183+
.to(params -> new ReleaseConnection(this, sessionIdFrom(params)))
184+
.with(spanDecorator("node.is_session_owner").andThen(requiresSecret)),
176185
post("/se/grid/node/connection/{sessionId}")
177186
.to(params -> new TryAcquireConnection(this, sessionIdFrom(params)))
178187
.with(spanDecorator("node.is_session_owner").andThen(requiresSecret)),
@@ -250,6 +259,8 @@ public TemporaryFilesystem getDownloadsFilesystem(UUID uuid) throws IOException
250259

251260
public abstract boolean tryAcquireConnection(SessionId id);
252261

262+
public abstract void releaseConnection(SessionId id);
263+
253264
public abstract boolean isSupporting(Capabilities capabilities);
254265

255266
public abstract NodeStatus getStatus();

java/src/org/openqa/selenium/grid/node/ProxyNodeWebsockets.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ private Consumer<Message> createWsEndPoint(
237237
WebSocket upstream =
238238
client.openSocket(
239239
new HttpRequest(GET, uri.toString()),
240-
new ForwardingListener(downstream, sessionConsumer, sessionId));
240+
new ForwardingListener(node, downstream, sessionConsumer, sessionId));
241241

242242
return (msg) -> {
243243
try {
@@ -260,12 +260,17 @@ private Consumer<Message> createWsEndPoint(
260260
}
261261

262262
private static class ForwardingListener implements WebSocket.Listener {
263+
private final Node node;
263264
private final Consumer<Message> downstream;
264265
private final Consumer<SessionId> sessionConsumer;
265266
private final SessionId sessionId;
266267

267268
public ForwardingListener(
268-
Consumer<Message> downstream, Consumer<SessionId> sessionConsumer, SessionId sessionId) {
269+
Node node,
270+
Consumer<Message> downstream,
271+
Consumer<SessionId> sessionConsumer,
272+
SessionId sessionId) {
273+
this.node = node;
269274
this.downstream = Objects.requireNonNull(downstream);
270275
this.sessionConsumer = Objects.requireNonNull(sessionConsumer);
271276
this.sessionId = Objects.requireNonNull(sessionId);
@@ -280,7 +285,7 @@ public void onBinary(byte[] data) {
280285
@Override
281286
public void onClose(int code, String reason) {
282287
downstream.accept(new CloseMessage(code, reason));
283-
sessionConsumer.accept(sessionId);
288+
node.releaseConnection(sessionId);
284289
}
285290

286291
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package org.openqa.selenium.grid.node;
19+
20+
import java.io.UncheckedIOException;
21+
import org.openqa.selenium.internal.Require;
22+
import org.openqa.selenium.remote.SessionId;
23+
import org.openqa.selenium.remote.http.HttpHandler;
24+
import org.openqa.selenium.remote.http.HttpRequest;
25+
import org.openqa.selenium.remote.http.HttpResponse;
26+
27+
class ReleaseConnection implements HttpHandler {
28+
29+
private final Node node;
30+
private final SessionId id;
31+
32+
ReleaseConnection(Node node, SessionId id) {
33+
this.node = Require.nonNull("Node", node);
34+
this.id = Require.nonNull("Session id", id);
35+
}
36+
37+
@Override
38+
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
39+
node.releaseConnection(id);
40+
41+
return new HttpResponse().setStatus(200);
42+
}
43+
}

java/src/org/openqa/selenium/grid/node/TryAcquireConnection.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
import static org.openqa.selenium.remote.http.Contents.asJson;
2121

22-
import com.google.common.collect.ImmutableMap;
2322
import java.io.UncheckedIOException;
23+
import java.util.Map;
2424
import org.openqa.selenium.internal.Require;
2525
import org.openqa.selenium.remote.SessionId;
2626
import org.openqa.selenium.remote.http.HttpHandler;
@@ -39,7 +39,6 @@ class TryAcquireConnection implements HttpHandler {
3939

4040
@Override
4141
public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
42-
return new HttpResponse()
43-
.setContent(asJson(ImmutableMap.of("value", node.tryAcquireConnection(id))));
42+
return new HttpResponse().setContent(asJson(Map.of("value", node.tryAcquireConnection(id))));
4443
}
4544
}

java/src/org/openqa/selenium/grid/node/config/NodeFlags.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ public class NodeFlags implements HasRoles {
8181
@Parameter(
8282
names = {"--connection-limit-per-session"},
8383
description =
84-
"Let X be the maximum number of websocket connections per session.This will ensure one"
85-
+ " session is not able to exhaust the connection limit of the host")
84+
"Let X be the maximum number of concurrent websocket connections per session. This will"
85+
+ " ensure one session is not able to exhaust the connection limit of the host")
8686
@ConfigValue(section = NODE_SECTION, name = "connection-limit-per-session", example = "8")
8787
public int connectionLimitPerSession = DEFAULT_CONNECTION_LIMIT;
8888

java/src/org/openqa/selenium/grid/node/k8s/OneShotNode.java

+18-1
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,24 @@ public boolean isSessionOwner(SessionId id) {
365365

366366
@Override
367367
public boolean tryAcquireConnection(SessionId id) {
368-
return sessionId.equals(id) && connectionLimitPerSession > connectionCounter.getAndIncrement();
368+
if (!sessionId.equals(id)) {
369+
return false;
370+
}
371+
372+
if (connectionLimitPerSession > connectionCounter.getAndIncrement()) {
373+
return true;
374+
}
375+
376+
// ensure a rejected connection will not be counted
377+
connectionCounter.getAndDecrement();
378+
return false;
379+
}
380+
381+
@Override
382+
public void releaseConnection(SessionId id) {
383+
if (sessionId.equals(id)) {
384+
connectionCounter.getAndDecrement();
385+
}
369386
}
370387

371388
@Override

java/src/org/openqa/selenium/grid/node/local/LocalNode.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,31 @@ public boolean tryAcquireConnection(SessionId id) throws NoSuchSessionException
623623

624624
AtomicLong counter = slot.getConnectionCounter();
625625

626-
return connectionLimitPerSession > counter.getAndIncrement();
626+
if (connectionLimitPerSession > counter.getAndIncrement()) {
627+
return true;
628+
}
629+
630+
// ensure a rejected connection will not be counted
631+
counter.getAndDecrement();
632+
return false;
633+
}
634+
635+
@Override
636+
public void releaseConnection(SessionId id) {
637+
SessionSlot slot = currentSessions.getIfPresent(id);
638+
639+
if (slot == null) {
640+
return;
641+
}
642+
643+
if (connectionLimitPerSession == -1) {
644+
// no limit
645+
return;
646+
}
647+
648+
AtomicLong counter = slot.getConnectionCounter();
649+
650+
counter.decrementAndGet();
627651
}
628652

629653
@Override

java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java

+12
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,18 @@ public boolean tryAcquireConnection(SessionId id) {
196196
return Boolean.TRUE.equals(Values.get(res, Boolean.class));
197197
}
198198

199+
@Override
200+
public void releaseConnection(SessionId id) {
201+
Require.nonNull("Session ID", id);
202+
203+
HttpRequest req = new HttpRequest(DELETE, "/se/grid/node/connection/" + id);
204+
HttpTracing.inject(tracer, tracer.getCurrentContext(), req);
205+
206+
HttpResponse res = client.with(addSecret).execute(req);
207+
208+
Values.get(res, Void.class);
209+
}
210+
199211
@Override
200212
public Session getSession(SessionId id) throws NoSuchSessionException {
201213
Require.nonNull("Session ID", id);

java/test/org/openqa/selenium/grid/distributor/AddingNodesTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,9 @@ public boolean tryAcquireConnection(SessionId id) {
450450
return false;
451451
}
452452

453+
@Override
454+
public void releaseConnection(SessionId id) {}
455+
453456
@Override
454457
public boolean isSupporting(Capabilities capabilities) {
455458
return Objects.equals("cake", capabilities.getCapability("cheese"));

java/test/org/openqa/selenium/grid/router/DistributedTest.java

+50-1
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,12 @@
3030
import org.junit.jupiter.api.Assertions;
3131
import org.junit.jupiter.api.BeforeEach;
3232
import org.junit.jupiter.api.Test;
33+
import org.openqa.selenium.Capabilities;
34+
import org.openqa.selenium.HasCapabilities;
3335
import org.openqa.selenium.SessionNotCreatedException;
3436
import org.openqa.selenium.WebDriver;
37+
import org.openqa.selenium.bidi.BiDi;
38+
import org.openqa.selenium.bidi.BiDiProvider;
3539
import org.openqa.selenium.grid.config.MapConfig;
3640
import org.openqa.selenium.grid.config.MemoizedConfig;
3741
import org.openqa.selenium.grid.config.TomlConfig;
@@ -43,6 +47,7 @@
4347
import org.openqa.selenium.netty.server.NettyServer;
4448
import org.openqa.selenium.remote.RemoteWebDriver;
4549
import org.openqa.selenium.remote.http.ClientConfig;
50+
import org.openqa.selenium.remote.http.ConnectionFailedException;
4651
import org.openqa.selenium.remote.http.Contents;
4752
import org.openqa.selenium.remote.http.HttpClient;
4853
import org.openqa.selenium.remote.http.HttpMethod;
@@ -76,7 +81,9 @@ public void setupServers() {
7681
+ "\n"
7782
+ "override-max-sessions = true"
7883
+ "\n"
79-
+ "max-sessions = 2")));
84+
+ "max-sessions = 2"
85+
+ "\n"
86+
+ "connection-limit-per-session = 3")));
8087
tearDowns.add(deployment);
8188

8289
server = deployment.getServer();
@@ -192,4 +199,46 @@ void clientTimeoutDoesNotLeakARunningBrowser() throws Exception {
192199
Safely.safelyCall(healthy::quit);
193200
}
194201
}
202+
203+
@Test
204+
void connectionLimitIsRespected() throws Exception {
205+
assertThat(server.isStarted()).isTrue();
206+
207+
// don't use the RemoteWebDriver.builder here, using it does create an unknown number of
208+
// connections
209+
WebDriver driver = new RemoteWebDriver(server.getUrl(), browser.getCapabilities());
210+
211+
try {
212+
Capabilities caps = ((HasCapabilities) driver).getCapabilities();
213+
BiDiProvider biDiProvider = new BiDiProvider();
214+
215+
BiDi cnn1 = biDiProvider.getImplementation(caps, null).getBiDi();
216+
BiDi cnn2 = biDiProvider.getImplementation(caps, null).getBiDi();
217+
BiDi cnn3 = biDiProvider.getImplementation(caps, null).getBiDi();
218+
219+
Assertions.assertThrows(
220+
ConnectionFailedException.class,
221+
() -> biDiProvider.getImplementation(caps, null).getBiDi());
222+
cnn1.close();
223+
BiDi cnn4 = biDiProvider.getImplementation(caps, null).getBiDi();
224+
225+
Assertions.assertThrows(
226+
ConnectionFailedException.class,
227+
() -> biDiProvider.getImplementation(caps, null).getBiDi());
228+
cnn2.close();
229+
cnn3.close();
230+
BiDi cnn5 = biDiProvider.getImplementation(caps, null).getBiDi();
231+
BiDi cnn6 = biDiProvider.getImplementation(caps, null).getBiDi();
232+
233+
Assertions.assertThrows(
234+
ConnectionFailedException.class,
235+
() -> biDiProvider.getImplementation(caps, null).getBiDi());
236+
237+
cnn4.close();
238+
cnn5.close();
239+
cnn6.close();
240+
} finally {
241+
Safely.safelyCall(driver::quit);
242+
}
243+
}
195244
}

0 commit comments

Comments
 (0)