Skip to content

Commit c9c6236

Browse files
committed
[grid] Update for node shutdown gracefully
Signed-off-by: Viet Nguyen Duc <[email protected]>
1 parent 6ea5651 commit c9c6236

File tree

7 files changed

+216
-9
lines changed

7 files changed

+216
-9
lines changed

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

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

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

20+
import static org.openqa.selenium.remote.HttpSessionId.getSessionId;
21+
22+
import org.openqa.selenium.NoSuchSessionException;
2023
import org.openqa.selenium.internal.Require;
24+
import org.openqa.selenium.remote.SessionId;
2125
import org.openqa.selenium.remote.http.HttpHandler;
2226
import org.openqa.selenium.remote.http.HttpRequest;
2327
import org.openqa.selenium.remote.http.HttpResponse;
@@ -30,8 +34,17 @@ class ForwardWebDriverCommand implements HttpHandler {
3034
this.node = Require.nonNull("Node", node);
3135
}
3236

37+
public boolean matches(HttpRequest req) {
38+
return getSessionId(req.getUri())
39+
.map(id -> node.isSessionOwner(new SessionId(id)))
40+
.orElse(false);
41+
}
42+
3343
@Override
3444
public HttpResponse execute(HttpRequest req) {
35-
return node.executeWebDriverCommand(req);
45+
if (matches(req)) {
46+
return node.executeWebDriverCommand(req);
47+
}
48+
throw new NoSuchSessionException(String.format("Session not found in node %s", node.getId()));
3649
}
3750
}

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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
load("@rules_jvm_external//:defs.bzl", "artifact")
2+
load("//java:defs.bzl", "JUNIT5_DEPS", "java_selenium_test_suite")
3+
4+
java_selenium_test_suite(
5+
name = "large-tests",
6+
size = "large",
7+
srcs = glob(["*.java"]),
8+
browsers = [
9+
"chrome",
10+
],
11+
deps = [
12+
"//java/src/org/openqa/selenium:core",
13+
"//java/src/org/openqa/selenium/chrome",
14+
"//java/src/org/openqa/selenium/grid",
15+
"//java/src/org/openqa/selenium/remote/http",
16+
artifact("com.google.guava:guava"),
17+
artifact("org.junit.jupiter:junit-jupiter-api"),
18+
artifact("org.assertj:assertj-core"),
19+
] + JUNIT5_DEPS,
20+
)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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.e2e;
19+
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
23+
import java.net.URI;
24+
import org.assertj.core.api.Assumptions;
25+
import org.junit.jupiter.api.Test;
26+
import org.openqa.selenium.NoSuchSessionException;
27+
import org.openqa.selenium.WebDriver;
28+
import org.openqa.selenium.chrome.ChromeOptions;
29+
import org.openqa.selenium.grid.Bootstrap;
30+
import org.openqa.selenium.remote.RemoteWebDriver;
31+
32+
public class SessionTimeoutTest {
33+
@Test
34+
void testSessionTimeout() throws Exception {
35+
Assumptions.assumeThat(System.getProperty("webdriver.chrome.binary")).isNull();
36+
Bootstrap.main(("hub --host 127.0.0.1 --port 4444").split(" "));
37+
Bootstrap.main(
38+
("node --host 127.0.0.1 --port 5555 --session-timeout 12 --selenium-manager true")
39+
.split(" "));
40+
41+
var options = new ChromeOptions();
42+
options.addArguments("--disable-search-engine-choice-screen");
43+
options.addArguments("--headless=new");
44+
45+
WebDriver driver = new RemoteWebDriver(URI.create("http://localhost:4444").toURL(), options);
46+
driver.get("http://localhost:4444");
47+
Thread.sleep(12000);
48+
NoSuchSessionException exception = assertThrows(NoSuchSessionException.class, driver::getTitle);
49+
assertTrue(exception.getMessage().startsWith("Cannot find session with id:"));
50+
}
51+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertThrows;
22+
import static org.junit.jupiter.api.Assertions.assertTrue;
23+
import static org.mockito.Mockito.*;
24+
25+
import java.util.UUID;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
import org.openqa.selenium.NoSuchSessionException;
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+
NoSuchSessionException exception =
70+
assertThrows(NoSuchSessionException.class, () -> command.execute(mockRequest));
71+
assertTrue(
72+
exception
73+
.getMessage()
74+
.startsWith(String.format("Session not found in node %s", mockNode.getId())));
75+
}
76+
}

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

0 commit comments

Comments
 (0)