Skip to content

Commit 222b682

Browse files
committed
[grid] add some unit tests to provoke the failure
1 parent 1ad9e3b commit 222b682

File tree

3 files changed

+299
-5
lines changed

3 files changed

+299
-5
lines changed

java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

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

20-
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
20+
import static java.net.HttpURLConnection.HTTP_OK;
2121
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
2222
import static org.openqa.selenium.grid.config.StandardGridRoles.EVENT_BUS_ROLE;
2323
import static org.openqa.selenium.grid.config.StandardGridRoles.HTTPD_ROLE;
@@ -131,13 +131,16 @@ protected Handlers createHandlers(Config config) {
131131
HttpHandler readinessCheck =
132132
req -> {
133133
if (node.getStatus().hasCapacity()) {
134-
return new HttpResponse().setStatus(HTTP_NO_CONTENT);
134+
return new HttpResponse()
135+
.setStatus(HTTP_OK)
136+
.setHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString())
137+
.setContent(Contents.utf8String("Node has capacity available"));
135138
}
136139

137140
return new HttpResponse()
138141
.setStatus(HTTP_UNAVAILABLE)
139142
.setHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString())
140-
.setContent(Contents.utf8String("No capacity available"));
143+
.setContent(Contents.utf8String("Node has no capacity available"));
141144
};
142145

143146
bus.addListener(

java/test/org/openqa/selenium/grid/distributor/BUILD.bazel

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,56 @@
11
load("@rules_jvm_external//:defs.bzl", "artifact")
2-
load("//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite")
2+
load("//java:defs.bzl", "JUNIT5_DEPS", "java_selenium_test_suite", "java_test_suite")
33
load("//java:version.bzl", "TOOLS_JAVA_VERSION")
44

5+
LARGE_TESTS = [
6+
"DrainTest.java",
7+
]
8+
9+
java_selenium_test_suite(
10+
name = "large-tests",
11+
size = "large",
12+
srcs = LARGE_TESTS,
13+
browsers = [
14+
"chrome",
15+
"firefox",
16+
"edge",
17+
],
18+
javacopts = [
19+
"--release",
20+
TOOLS_JAVA_VERSION,
21+
],
22+
tags = [
23+
"selenium-remote",
24+
],
25+
deps = [
26+
"//java/src/org/openqa/selenium/chrome",
27+
"//java/src/org/openqa/selenium/firefox",
28+
"//java/src/org/openqa/selenium/grid",
29+
"//java/src/org/openqa/selenium/grid/config",
30+
"//java/src/org/openqa/selenium/grid/distributor",
31+
"//java/src/org/openqa/selenium/json",
32+
"//java/src/org/openqa/selenium/remote",
33+
"//java/src/org/openqa/selenium/support",
34+
"//java/test/org/openqa/selenium/environment",
35+
"//java/test/org/openqa/selenium/grid/testing",
36+
"//java/test/org/openqa/selenium/remote/tracing:tracing-support",
37+
"//java/test/org/openqa/selenium/testing:annotations",
38+
"//java/test/org/openqa/selenium/testing:test-base",
39+
artifact("org.junit.jupiter:junit-jupiter-api"),
40+
artifact("org.junit.jupiter:junit-jupiter-params"),
41+
artifact("org.assertj:assertj-core"),
42+
"//java/src/org/openqa/selenium:core",
43+
"//java/src/org/openqa/selenium/remote/http",
44+
] + JUNIT5_DEPS,
45+
)
46+
547
java_test_suite(
648
name = "medium-tests",
749
size = "medium",
8-
srcs = glob(["*.java"]),
50+
srcs = glob(
51+
["*.java"],
52+
exclude = LARGE_TESTS,
53+
),
954
javacopts = [
1055
"--release",
1156
TOOLS_JAVA_VERSION,
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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.distributor;
19+
20+
import static org.assertj.core.api.Assertions.assertThat;
21+
22+
import java.io.StringReader;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Objects;
27+
import java.util.concurrent.CountDownLatch;
28+
import java.util.concurrent.ExecutorService;
29+
import java.util.concurrent.Executors;
30+
import java.util.concurrent.Future;
31+
import java.util.concurrent.TimeUnit;
32+
import java.util.function.Supplier;
33+
import org.assertj.core.api.Assertions;
34+
import org.junit.jupiter.api.Disabled;
35+
import org.junit.jupiter.api.Test;
36+
import org.openqa.selenium.WebDriver;
37+
import org.openqa.selenium.grid.commands.Hub;
38+
import org.openqa.selenium.grid.config.CompoundConfig;
39+
import org.openqa.selenium.grid.config.Config;
40+
import org.openqa.selenium.grid.config.MapConfig;
41+
import org.openqa.selenium.grid.config.MemoizedConfig;
42+
import org.openqa.selenium.grid.config.TomlConfig;
43+
import org.openqa.selenium.grid.node.httpd.NodeServer;
44+
import org.openqa.selenium.grid.server.Server;
45+
import org.openqa.selenium.net.PortProber;
46+
import org.openqa.selenium.net.UrlChecker;
47+
import org.openqa.selenium.remote.RemoteWebDriver;
48+
import org.openqa.selenium.testing.Safely;
49+
import org.openqa.selenium.testing.drivers.Browser;
50+
51+
class DrainTest {
52+
53+
private final Browser browser = Objects.requireNonNull(Browser.detect());
54+
55+
@Disabled("will be fixed with PR 14987")
56+
@Test
57+
void nodeDoesNotTakeTooManySessions() throws Exception {
58+
String[] rawConfig =
59+
new String[] {
60+
"[events]",
61+
"publish = \"tcp://localhost:" + PortProber.findFreePort() + "\"",
62+
"subscribe = \"tcp://localhost:" + PortProber.findFreePort() + "\"",
63+
"",
64+
"[server]",
65+
"registration-secret = \"feta\""
66+
};
67+
68+
Config baseConfig =
69+
new MemoizedConfig(new TomlConfig(new StringReader(String.join("\n", rawConfig))));
70+
71+
Server<?> hub = startHub(baseConfig);
72+
try (AutoCloseable stopHub = () -> Safely.safelyCall(hub::stop); ) {
73+
UrlChecker urlChecker = new UrlChecker();
74+
urlChecker.waitUntilAvailable(
75+
5, TimeUnit.SECONDS, hub.getUrl().toURI().resolve("readyz").toURL());
76+
77+
// the CI has not enough CPUs so use a fixed number here
78+
int nThreads = 4 * 3;
79+
ExecutorService executor = Executors.newFixedThreadPool(nThreads);
80+
81+
try {
82+
List<Future<WebDriver>> pendingSessions = new ArrayList<>();
83+
CountDownLatch allPending = new CountDownLatch(nThreads);
84+
85+
for (int i = 0; i < nThreads; i++) {
86+
Future<WebDriver> future =
87+
executor.submit(
88+
() -> {
89+
allPending.countDown();
90+
91+
return RemoteWebDriver.builder()
92+
.oneOf(browser.getCapabilities())
93+
.address(hub.getUrl())
94+
.build();
95+
});
96+
97+
pendingSessions.add(future);
98+
}
99+
100+
// ensure all sessions are in the queue
101+
Assertions.assertThat(allPending.await(8, TimeUnit.SECONDS)).isTrue();
102+
103+
for (int i = 0; i < nThreads; i += 3) {
104+
// remove all completed futures
105+
assertThat(pendingSessions.removeIf(Future::isDone)).isEqualTo(i != 0);
106+
107+
// start a node draining after 3 sessions
108+
var node = startNode(baseConfig, hub, 6, 3);
109+
110+
urlChecker.waitUntilAvailable(
111+
20, TimeUnit.SECONDS, node.getUrl().toURI().resolve("readyz").toURL());
112+
113+
// we want to check not more than 3 are started, polling won't help here
114+
Thread.sleep(20_000);
115+
int stopped = 0;
116+
117+
for (int j = 0; j < pendingSessions.size(); j++) {
118+
var future = pendingSessions.get(j);
119+
120+
if (future.isDone()) {
121+
stopped++;
122+
future.get().quit();
123+
}
124+
}
125+
126+
// the node should only pick 3 sessions to start, then starts to drain
127+
Assertions.assertThat(stopped).isEqualTo(3);
128+
129+
// check the node stopped
130+
urlChecker.waitUntilUnavailable(
131+
10, TimeUnit.SECONDS, node.getUrl().toURI().resolve("readyz").toURL());
132+
}
133+
} finally {
134+
executor.shutdownNow();
135+
}
136+
}
137+
}
138+
139+
@Test
140+
void sessionIsNotRejectedWhenNodeDrains() throws Exception {
141+
String[] rawConfig =
142+
new String[] {
143+
"[events]",
144+
"publish = \"tcp://localhost:" + PortProber.findFreePort() + "\"",
145+
"subscribe = \"tcp://localhost:" + PortProber.findFreePort() + "\"",
146+
"",
147+
"[server]",
148+
"registration-secret = \"feta\""
149+
};
150+
151+
Config baseConfig =
152+
new MemoizedConfig(new TomlConfig(new StringReader(String.join("\n", rawConfig))));
153+
154+
Server<?> hub = startHub(baseConfig);
155+
try (AutoCloseable stopHub = () -> Safely.safelyCall(hub::stop); ) {
156+
UrlChecker urlChecker = new UrlChecker();
157+
urlChecker.waitUntilAvailable(
158+
5, TimeUnit.SECONDS, hub.getUrl().toURI().resolve("readyz").toURL());
159+
160+
ExecutorService executor = Executors.newFixedThreadPool(2);
161+
162+
Supplier<Future<WebDriver>> newDriver =
163+
() ->
164+
executor.submit(
165+
() ->
166+
RemoteWebDriver.builder()
167+
.oneOf(browser.getCapabilities())
168+
.address(hub.getUrl())
169+
.build());
170+
171+
try {
172+
Future<WebDriver> pendingA = newDriver.get();
173+
Future<WebDriver> pendingB = newDriver.get();
174+
175+
for (int i = 0; i < 16; i++) {
176+
// the node should drain automatically
177+
startNode(baseConfig, hub, 1, 1);
178+
179+
for (int j = 0; j < 2000; j++) {
180+
Thread.sleep(10);
181+
182+
if (pendingA.isDone() || pendingB.isDone()) {
183+
break;
184+
}
185+
}
186+
187+
if (pendingA.isDone() && pendingB.isDone()) {
188+
pendingA.get().quit();
189+
pendingB.get().quit();
190+
191+
throw new IllegalStateException("only one should be started");
192+
} else if (pendingA.isDone()) {
193+
pendingA.get().quit();
194+
pendingA = newDriver.get();
195+
} else if (pendingB.isDone()) {
196+
pendingB.get().quit();
197+
pendingB = newDriver.get();
198+
} else {
199+
throw new IllegalStateException("no browser started");
200+
}
201+
}
202+
} finally {
203+
executor.shutdownNow();
204+
}
205+
}
206+
}
207+
208+
Server<?> startHub(Config baseConfig) {
209+
Config hubConfig =
210+
new MemoizedConfig(
211+
new CompoundConfig(
212+
new MapConfig(
213+
Map.of(
214+
"server",
215+
Map.of("port", PortProber.findFreePort()),
216+
"events",
217+
Map.of("bind", true),
218+
"distributor",
219+
Map.of("newsession-threadpool-size", "6"))),
220+
baseConfig));
221+
222+
return new Hub().asServer(hubConfig).start();
223+
}
224+
225+
Server<?> startNode(Config baseConfig, Server<?> hub, int maxSessions, int drainAfter) {
226+
MapConfig additionalNodeConfig =
227+
new MapConfig(
228+
Map.of(
229+
"server", Map.of("port", PortProber.findFreePort()),
230+
"node",
231+
Map.of(
232+
"hub",
233+
hub.getUrl(),
234+
"driver-implementation",
235+
browser.displayName(),
236+
"override-max-sessions",
237+
"true",
238+
"max-sessions",
239+
Integer.toString(maxSessions),
240+
"drain-after-session-count",
241+
drainAfter)));
242+
243+
Config nodeConfig = new MemoizedConfig(new CompoundConfig(additionalNodeConfig, baseConfig));
244+
return new NodeServer().asServer(nodeConfig).start();
245+
}
246+
}

0 commit comments

Comments
 (0)