Skip to content

Commit 833efa9

Browse files
authored
[grid] Improvement for Node handling (#14628)
* [grid] Update for node shutdown gracefully * Return HTTP response in case session is not owned * Fix test --------- Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 029d263 commit 833efa9

File tree

6 files changed

+213
-11
lines changed

6 files changed

+213
-11
lines changed

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

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@
1717

1818
package org.openqa.selenium.grid.node;
1919

20+
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
21+
import static org.openqa.selenium.remote.HttpSessionId.getSessionId;
22+
import static org.openqa.selenium.remote.http.Contents.asJson;
23+
24+
import com.google.common.collect.ImmutableMap;
2025
import org.openqa.selenium.internal.Require;
26+
import org.openqa.selenium.remote.SessionId;
2127
import org.openqa.selenium.remote.http.HttpHandler;
2228
import org.openqa.selenium.remote.http.HttpRequest;
2329
import org.openqa.selenium.remote.http.HttpResponse;
@@ -30,8 +36,22 @@ class ForwardWebDriverCommand implements HttpHandler {
3036
this.node = Require.nonNull("Node", node);
3137
}
3238

39+
public boolean matches(HttpRequest req) {
40+
return getSessionId(req.getUri())
41+
.map(id -> node.isSessionOwner(new SessionId(id)))
42+
.orElse(false);
43+
}
44+
3345
@Override
3446
public HttpResponse execute(HttpRequest req) {
35-
return node.executeWebDriverCommand(req);
47+
if (matches(req)) {
48+
return node.executeWebDriverCommand(req);
49+
}
50+
return new HttpResponse()
51+
.setStatus(HTTP_INTERNAL_ERROR)
52+
.setContent(
53+
asJson(
54+
ImmutableMap.of(
55+
"error", String.format("Session not found in node %s", node.getId()))));
3656
}
3757
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ protected Node(
152152
req ->
153153
getSessionId(req.getUri())
154154
.map(SessionId::new)
155-
.map(this::isSessionOwner)
155+
.map(sessionId -> this.getSession(sessionId) != null)
156156
.orElse(false))
157157
.to(() -> new ForwardWebDriverCommand(this))
158158
.with(spanDecorator("node.forward_command")),

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,13 @@ protected LocalNode(
297297
heartbeatPeriod.getSeconds(),
298298
TimeUnit.SECONDS);
299299

300-
Runtime.getRuntime().addShutdownHook(new Thread(this::stopAllSessions));
300+
Runtime.getRuntime()
301+
.addShutdownHook(
302+
new Thread(
303+
() -> {
304+
stopAllSessions();
305+
drain();
306+
}));
301307
new JMXHelper().register(this);
302308
}
303309

@@ -316,7 +322,6 @@ private void stopTimedOutSession(RemovalNotification<SessionId, SessionSlot> not
316322
}
317323
// Attempt to stop the session
318324
slot.stop();
319-
this.sessionToDownloadsDir.invalidate(id);
320325
// Decrement pending sessions if Node is draining
321326
if (this.isDraining()) {
322327
int done = pendingSessions.decrementAndGet();
@@ -473,8 +478,6 @@ public Either<WebDriverException, CreateSessionResponse> newSession(
473478
sessionToDownloadsDir.put(session.getId(), uuidForSessionDownloads);
474479
currentSessions.put(session.getId(), slotToUse);
475480

476-
checkSessionCount();
477-
478481
SessionId sessionId = session.getId();
479482
Capabilities caps = session.getCapabilities();
480483
SESSION_ID.accept(span, sessionId);
@@ -513,6 +516,8 @@ public Either<WebDriverException, CreateSessionResponse> newSession(
513516
span.addEvent("Unable to create session with the driver", attributeMap);
514517
return Either.left(possibleSession.left());
515518
}
519+
} finally {
520+
checkSessionCount();
516521
}
517522
}
518523

@@ -765,6 +770,10 @@ public HttpResponse uploadFile(HttpRequest req, SessionId id) {
765770
public void stop(SessionId id) throws NoSuchSessionException {
766771
Require.nonNull("Session ID", id);
767772

773+
if (sessionToDownloadsDir.getIfPresent(id) != null) {
774+
sessionToDownloadsDir.invalidate(id);
775+
}
776+
768777
SessionSlot slot = currentSessions.getIfPresent(id);
769778
if (slot == null) {
770779
throw new NoSuchSessionException("Cannot find session with id: " + id);
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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 static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.mockito.Mockito.*;
23+
import static org.openqa.selenium.remote.http.Contents.asJson;
24+
25+
import com.google.common.collect.ImmutableMap;
26+
import java.util.UUID;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
import org.openqa.selenium.grid.data.NodeId;
30+
import org.openqa.selenium.remote.SessionId;
31+
import org.openqa.selenium.remote.http.HttpRequest;
32+
import org.openqa.selenium.remote.http.HttpResponse;
33+
34+
class ForwardWebDriverCommandTest {
35+
36+
private Node mockNode;
37+
private ForwardWebDriverCommand command;
38+
39+
@BeforeEach
40+
void setUp() {
41+
mockNode = mock(Node.class);
42+
when(mockNode.getId()).thenReturn(new NodeId(UUID.randomUUID()));
43+
command = new ForwardWebDriverCommand(mockNode);
44+
}
45+
46+
@Test
47+
void testExecuteWithValidSessionOwner() {
48+
HttpRequest mockRequest = mock(HttpRequest.class);
49+
when(mockRequest.getUri()).thenReturn("/session/1234");
50+
51+
SessionId sessionId = new SessionId("1234");
52+
when(mockNode.isSessionOwner(sessionId)).thenReturn(true);
53+
54+
HttpResponse expectedResponse = new HttpResponse();
55+
when(mockNode.executeWebDriverCommand(mockRequest)).thenReturn(expectedResponse);
56+
57+
HttpResponse actualResponse = command.execute(mockRequest);
58+
assertEquals(expectedResponse, actualResponse);
59+
}
60+
61+
@Test
62+
void testExecuteWithInvalidSessionOwner() {
63+
HttpRequest mockRequest = mock(HttpRequest.class);
64+
when(mockRequest.getUri()).thenReturn("/session/5678");
65+
66+
SessionId sessionId = new SessionId("5678");
67+
when(mockNode.isSessionOwner(sessionId)).thenReturn(false);
68+
69+
HttpResponse actualResponse = command.execute(mockRequest);
70+
HttpResponse expectResponse =
71+
new HttpResponse()
72+
.setStatus(HTTP_INTERNAL_ERROR)
73+
.setContent(
74+
asJson(
75+
ImmutableMap.of(
76+
"error", String.format("Session not found in node %s", mockNode.getId()))));
77+
assertEquals(expectResponse.getStatus(), actualResponse.getStatus());
78+
assertEquals(expectResponse.getContentEncoding(), actualResponse.getContentEncoding());
79+
}
80+
}

java/test/org/openqa/selenium/grid/node/NodeTest.java

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
2424
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2525
import static org.assertj.core.api.InstanceOfAssertFactories.MAP;
26+
import static org.junit.jupiter.api.Assertions.assertThrows;
27+
import static org.junit.jupiter.api.Assertions.assertTrue;
2628
import static org.openqa.selenium.json.Json.MAP_TYPE;
2729
import static org.openqa.selenium.remote.http.Contents.string;
2830
import static org.openqa.selenium.remote.http.HttpMethod.DELETE;
@@ -102,7 +104,9 @@ class NodeTest {
102104
private Tracer tracer;
103105
private EventBus bus;
104106
private LocalNode local;
107+
private LocalNode local2;
105108
private Node node;
109+
private Node node2;
106110
private ImmutableCapabilities stereotype;
107111
private ImmutableCapabilities caps;
108112
private URI uri;
@@ -150,6 +154,7 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
150154
builder = builder.enableManagedDownloads(true).sessionTimeout(Duration.ofSeconds(1));
151155
}
152156
local = builder.build();
157+
local2 = builder.build();
153158

154159
node =
155160
new RemoteNode(
@@ -160,6 +165,16 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
160165
registrationSecret,
161166
local.getSessionTimeout(),
162167
ImmutableSet.of(caps));
168+
169+
node2 =
170+
new RemoteNode(
171+
tracer,
172+
new PassthroughHttpClient.Factory(local2),
173+
new NodeId(UUID.randomUUID()),
174+
uri,
175+
registrationSecret,
176+
local2.getSessionTimeout(),
177+
ImmutableSet.of(caps));
163178
}
164179

165180
@Test
@@ -371,13 +386,36 @@ void shouldOnlyRespondToWebDriverCommandsForSessionsTheNodeOwns() {
371386
assertThatEither(response).isRight();
372387
Session session = response.right().getSession();
373388

389+
Either<WebDriverException, CreateSessionResponse> response2 =
390+
node2.newSession(createSessionRequest(caps));
391+
assertThatEither(response2).isRight();
392+
Session session2 = response2.right().getSession();
393+
394+
// Assert that should respond to commands for sessions Node 1 owns
374395
HttpRequest req = new HttpRequest(POST, String.format("/session/%s/url", session.getId()));
375396
assertThat(local.matches(req)).isTrue();
376397
assertThat(node.matches(req)).isTrue();
377398

378-
req = new HttpRequest(POST, String.format("/session/%s/url", UUID.randomUUID()));
379-
assertThat(local.matches(req)).isFalse();
380-
assertThat(node.matches(req)).isFalse();
399+
// Assert that should respond to commands for sessions Node 2 owns
400+
HttpRequest req2 = new HttpRequest(POST, String.format("/session/%s/url", session2.getId()));
401+
assertThat(local2.matches(req2)).isTrue();
402+
assertThat(node2.matches(req2)).isTrue();
403+
404+
// Assert that should not respond to commands for sessions Node 1 does not own
405+
NoSuchSessionException exception =
406+
assertThrows(NoSuchSessionException.class, () -> node.execute(req2));
407+
assertTrue(
408+
exception
409+
.getMessage()
410+
.startsWith(String.format("Cannot find session with id: %s", session2.getId())));
411+
412+
// Assert that should not respond to commands for sessions Node 2 does not own
413+
NoSuchSessionException exception2 =
414+
assertThrows(NoSuchSessionException.class, () -> node2.execute(req));
415+
assertTrue(
416+
exception2
417+
.getMessage()
418+
.startsWith(String.format("Cannot find session with id: %s", session.getId())));
381419
}
382420

383421
@Test

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

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import static java.nio.charset.StandardCharsets.UTF_8;
2121
import static java.util.concurrent.TimeUnit.MINUTES;
2222
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.junit.jupiter.api.Assertions.assertThrows;
24+
import static org.junit.jupiter.api.Assertions.assertTrue;
2325

2426
import java.io.StringReader;
2527
import java.util.LinkedList;
@@ -33,7 +35,10 @@
3335
import org.junit.jupiter.api.BeforeEach;
3436
import org.junit.jupiter.api.Test;
3537
import org.openqa.selenium.By;
38+
import org.openqa.selenium.MutableCapabilities;
39+
import org.openqa.selenium.NoSuchSessionException;
3640
import org.openqa.selenium.WebDriver;
41+
import org.openqa.selenium.WebDriverException;
3742
import org.openqa.selenium.grid.config.MapConfig;
3843
import org.openqa.selenium.grid.config.MemoizedConfig;
3944
import org.openqa.selenium.grid.config.TomlConfig;
@@ -65,7 +70,14 @@ public void setupServers() {
6570
DeploymentTypes.DISTRIBUTED.start(
6671
browser.getCapabilities(),
6772
new TomlConfig(
68-
new StringReader("[node]\n" + "driver-implementation = " + browser.displayName())));
73+
new StringReader(
74+
"[node]\n"
75+
+ "driver-implementation = "
76+
+ browser.displayName()
77+
+ "\n"
78+
+ "session-timeout = 11"
79+
+ "\n"
80+
+ "enable-managed-downloads = true")));
6981
tearDowns.add(deployment);
7082

7183
server = deployment.getServer();
@@ -106,7 +118,10 @@ void multipleSimultaneousSessions() throws Exception {
106118
try {
107119
WebDriver driver =
108120
RemoteWebDriver.builder()
109-
.oneOf(browser.getCapabilities())
121+
.oneOf(
122+
browser
123+
.getCapabilities()
124+
.merge(new MutableCapabilities(Map.of("se:downloadsEnabled", true))))
110125
.address(server.getUrl())
111126
.build();
112127

@@ -124,4 +139,44 @@ void multipleSimultaneousSessions() throws Exception {
124139

125140
CompletableFuture.allOf(futures).get(4, MINUTES);
126141
}
142+
143+
@Test
144+
void multipleSimultaneousSessionsTimedOut() throws Exception {
145+
assertThat(server.isStarted()).isTrue();
146+
147+
CompletableFuture<?>[] futures = new CompletableFuture<?>[10];
148+
for (int i = 0; i < futures.length; i++) {
149+
CompletableFuture<Object> future = new CompletableFuture<>();
150+
futures[i] = future;
151+
152+
executor.submit(
153+
() -> {
154+
try {
155+
WebDriver driver =
156+
RemoteWebDriver.builder()
157+
.oneOf(browser.getCapabilities())
158+
.address(server.getUrl())
159+
.build();
160+
driver.get(appServer.getUrl().toString());
161+
Thread.sleep(11000);
162+
NoSuchSessionException exception =
163+
assertThrows(NoSuchSessionException.class, driver::getTitle);
164+
assertTrue(exception.getMessage().startsWith("Cannot find session with id:"));
165+
WebDriverException webDriverException =
166+
assertThrows(
167+
WebDriverException.class,
168+
() -> ((RemoteWebDriver) driver).getDownloadableFiles());
169+
assertTrue(
170+
webDriverException
171+
.getMessage()
172+
.startsWith("Cannot find downloads file system for session id:"));
173+
future.complete(true);
174+
} catch (Exception e) {
175+
future.completeExceptionally(e);
176+
}
177+
});
178+
}
179+
180+
CompletableFuture.allOf(futures).get(5, MINUTES);
181+
}
127182
}

0 commit comments

Comments
 (0)