Skip to content

Commit 5289e97

Browse files
krmahadevanshs96c
authored andcommitted
Enriching Hub Status to include Node info (#6127)
Currently the end-point /grid/api/hub/status does not provide information about the nodes attached to it and also the busy/free status per browser flavor on each of the nodes. Enriching the end-point to provide node info when invoked with the option /grid/api/hub/status?configuration=nodes Sample payload as below: { "nodes": [ { "Id": "http://192.168.1.6:5555", "browsers": [ { "browser": "safari", "slots": { "busy": 0, "total": 1 } }, { "browser": "chrome", "slots": { "busy": 0, "total": 5 } }, { "browser": "firefox", "slots": { "busy": 0, "total": 5 } } ] } ], "success": true }
1 parent 3ffb8eb commit 5289e97

7 files changed

Lines changed: 278 additions & 61 deletions

File tree

java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java

Lines changed: 94 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,32 @@
1919

2020
import static org.openqa.selenium.json.Json.MAP_TYPE;
2121

22+
import com.google.common.base.Splitter;
23+
import com.google.common.base.Strings;
2224
import com.google.common.collect.ImmutableSortedMap;
25+
import com.google.common.collect.Lists;
2326

2427
import org.openqa.grid.internal.GridRegistry;
2528
import org.openqa.grid.internal.RemoteProxy;
29+
import org.openqa.grid.internal.TestSlot;
2630
import org.openqa.selenium.json.Json;
2731
import org.openqa.selenium.json.JsonException;
2832
import org.openqa.selenium.json.JsonInput;
2933
import org.openqa.selenium.json.JsonOutput;
34+
import org.openqa.selenium.remote.CapabilityType;
3035

3136
import java.io.BufferedReader;
3237
import java.io.IOException;
3338
import java.io.InputStreamReader;
3439
import java.io.Writer;
35-
import java.util.Arrays;
3640
import java.util.HashMap;
3741
import java.util.List;
3842
import java.util.Map;
3943
import java.util.TreeMap;
44+
import java.util.stream.Collector;
45+
import static java.util.stream.Collectors.groupingBy;
46+
import static java.util.stream.Collectors.toList;
47+
import static java.util.stream.Collectors.reducing;
4048

4149
import javax.servlet.http.HttpServletRequest;
4250
import javax.servlet.http.HttpServletResponse;
@@ -63,10 +71,18 @@
6371
*/
6472
public class HubStatusServlet extends RegistryBasedServlet {
6573

74+
private static final String SUCCESS = "success";
75+
private static final String CONFIGURATION = "configuration";
76+
private static final String FREE = "free";
77+
private static final String BUSY = "busy";
78+
private static final String NEW_SESSION_REQUEST_COUNT = "newSessionRequestCount";
79+
private static final String SLOT_COUNTS = "slotCounts";
80+
private static final String NODES = "nodes";
81+
private static final String TOTAL = "total";
6682
private final Json json = new Json();
6783

6884
public HubStatusServlet() {
69-
super(null);
85+
this(null);
7086
}
7187

7288
public HubStatusServlet(GridRegistry registry) {
@@ -76,7 +92,7 @@ public HubStatusServlet(GridRegistry registry) {
7692
@Override
7793
protected void doGet(HttpServletRequest request, HttpServletResponse response)
7894
throws IOException {
79-
process(request, response, new HashMap());
95+
process(request, response, new HashMap<>());
8096
}
8197

8298
@Override
@@ -109,35 +125,41 @@ private Map<String, Object> getResponse(
109125
HttpServletRequest request,
110126
Map<String, Object> requestJSON) {
111127
Map<String, Object> res = new TreeMap<>();
112-
res.put("success", true);
128+
res.put(SUCCESS, true);
113129

114130
try {
115-
List<String> keysToReturn = null;
131+
String configuration = request.getParameter(CONFIGURATION);
116132

117-
if (request.getParameter("configuration") != null && !"".equals(request.getParameter("configuration"))) {
118-
keysToReturn = Arrays.asList(request.getParameter("configuration").split(","));
119-
} else if (requestJSON != null && requestJSON.containsKey("configuration")) {
120-
//noinspection unchecked
121-
keysToReturn = (List<String>) requestJSON.get("configuration");
133+
if (Strings.isNullOrEmpty(configuration)) {
134+
configuration = "";
135+
if (requestJSON.containsKey(CONFIGURATION)) {
136+
//noinspection unchecked
137+
configuration = requestJSON.get(CONFIGURATION).toString();
138+
}
122139
}
123140

141+
List<String> keysToReturn = Splitter.on(",").omitEmptyStrings().splitToList(configuration);
142+
124143
GridRegistry registry = getRegistry();
125144
Map<String, Object> config = registry.getHub().getConfiguration().toJson();
126145
for (Map.Entry<String, Object> entry : config.entrySet()) {
127-
if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains(entry.getKey())) {
146+
if (isKeyPresentIn(keysToReturn, entry.getKey())) {
128147
res.put(entry.getKey(), entry.getValue());
129148
}
130149
}
131-
if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains("newSessionRequestCount")) {
132-
res.put("newSessionRequestCount", registry.getNewSessionRequestCount());
150+
if (isKeyPresentIn(keysToReturn, NEW_SESSION_REQUEST_COUNT)) {
151+
res.put(NEW_SESSION_REQUEST_COUNT, registry.getNewSessionRequestCount());
133152
}
134153

135-
if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains("slotCounts")) {
136-
res.put("slotCounts", getSlotCounts());
154+
if (isKeyPresentIn(keysToReturn, SLOT_COUNTS)) {
155+
res.put(SLOT_COUNTS, getSlotCounts());
156+
}
157+
if (keysToReturn != null && keysToReturn.contains(NODES)) {
158+
res.put(NODES, getNodesInfo());
137159
}
138160
} catch (Exception e) {
139-
res.remove("success");
140-
res.put("success", false);
161+
res.remove(SUCCESS);
162+
res.put(SUCCESS, false);
141163
res.put("msg", e.getMessage());
142164
}
143165
return res;
@@ -154,8 +176,8 @@ private Map<String, Object> getSlotCounts() {
154176
}
155177

156178
return ImmutableSortedMap.of(
157-
"free", totalSlots - usedSlots,
158-
"total", totalSlots);
179+
FREE, totalSlots - usedSlots,
180+
TOTAL, totalSlots);
159181
}
160182

161183
private Map<String, Object> getRequestJSON(HttpServletRequest request) throws IOException {
@@ -167,4 +189,57 @@ private Map<String, Object> getRequestJSON(HttpServletRequest request) throws IO
167189
throw new IOException(e);
168190
}
169191
}
192+
193+
private static boolean isKeyPresentIn(List<String> keys, String key) {
194+
return keys == null || keys.isEmpty() || keys.contains(key);
195+
}
196+
197+
private List<Map<String, Object>> getNodesInfo() {
198+
List<RemoteProxy> proxies = getRegistry().getAllProxies().getSorted();
199+
return proxies.stream().map(this::getNodeInfo).collect(toList());
200+
}
201+
202+
private Map<String, Object> getNodeInfo(RemoteProxy remoteProxy) {
203+
return ImmutableSortedMap.of(
204+
"id", remoteProxy.getId(),
205+
"browsers", getInfoFromAllSlotsInNode(remoteProxy.getTestSlots())
206+
);
207+
}
208+
209+
private List<Map<String, Object>> getInfoFromAllSlotsInNode(List<TestSlot> slots) {
210+
List<Map<String, Object>> browsers = Lists.newArrayList();
211+
Map<String, List<TestSlot>>
212+
slotsInfo = slots.stream().collect(groupingBy(HubStatusServlet::getBrowser));
213+
for (Map.Entry<String, List<TestSlot>> each : slotsInfo.entrySet()) {
214+
String key = each.getKey();
215+
Map<String, Object> value = getSlotInfoPerBrowserFlavor(each.getValue());
216+
browsers.add(ImmutableSortedMap.of("browser", key, "slots", value));
217+
}
218+
return browsers;
219+
}
220+
221+
private Map<String, Object> getSlotInfoPerBrowserFlavor(List<TestSlot> slots) {
222+
Map<String, Integer> byStatus = slots.stream().collect(groupingBy(this::status, counting()));
223+
int busy = byStatus.computeIfAbsent(BUSY, status -> 0);
224+
int free = byStatus.computeIfAbsent(FREE, status -> 0);
225+
int total = busy + free;
226+
227+
return ImmutableSortedMap.of(TOTAL, total, BUSY, busy);
228+
}
229+
230+
private String status(TestSlot slot) {
231+
if (slot.getSession() == null) {
232+
return FREE;
233+
}
234+
return BUSY;
235+
}
236+
237+
private static String getBrowser(TestSlot slot) {
238+
return slot.getCapabilities().get(CapabilityType.BROWSER_NAME).toString();
239+
}
240+
241+
private static <T> Collector<T, ?, Integer> counting() {
242+
return reducing(0, e -> 1, Integer::sum);
243+
}
244+
170245
}

java/server/test/org/openqa/grid/web/servlet/BaseServletTest.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.openqa.testing.UrlInfo;
2424

2525
import java.io.IOException;
26+
import java.util.HashMap;
2627
import java.util.Map;
2728

2829
import javax.servlet.ServletException;
@@ -41,15 +42,30 @@ protected static UrlInfo createUrl(String path) {
4142

4243
protected FakeHttpServletResponse sendCommand(String method, String commandPath)
4344
throws IOException, ServletException {
44-
return sendCommand(method, commandPath, (Map<String, Object>) null);
45+
return sendCommand(method, commandPath, null);
4546
}
4647

4748
protected FakeHttpServletResponse sendCommand(
4849
String method,
4950
String commandPath,
5051
Map<String, Object> parameters) throws IOException, ServletException {
52+
return sendCommand(this.servlet,method, commandPath, parameters);
53+
}
54+
55+
protected static FakeHttpServletResponse sendCommand(
56+
HttpServlet servlet,
57+
String method,
58+
String commandPath,
59+
Map<String, Object> parameters) throws IOException, ServletException {
5160
FakeHttpServletRequest request = new FakeHttpServletRequest(method, createUrl(commandPath));
52-
if (parameters != null) {
61+
if ("get".equalsIgnoreCase(method) && parameters != null) {
62+
Map<String, String> params = new HashMap<>();
63+
for (Map.Entry<String, Object> parameter : parameters.entrySet()) {
64+
params.put(parameter.getKey(), parameter.getValue().toString());
65+
}
66+
request.setParameters(params);
67+
}
68+
if ("post".equalsIgnoreCase(method) && parameters != null) {
5369
request.setBody(new Json().toJson(parameters));
5470
}
5571
FakeHttpServletResponse response = new FakeHttpServletResponse();

java/server/test/org/openqa/grid/web/servlet/GridServletTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
DisplayHelpServletTest.class,
2626
ResourceServletTest.class,
2727
ConsoleServletTest.class,
28-
RegistrationServletTest.class
28+
RegistrationServletTest.class,
29+
HubStatusServletTest.class
2930
})
3031
public class GridServletTests {
3132
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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.grid.web.servlet;
19+
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import com.google.common.collect.ImmutableMap;
25+
26+
import org.junit.Before;
27+
import org.junit.Test;
28+
import org.junit.runner.RunWith;
29+
import org.junit.runners.JUnit4;
30+
import org.openqa.grid.common.RegistrationRequest;
31+
import org.openqa.grid.internal.DefaultGridRegistry;
32+
import org.openqa.grid.internal.GridRegistry;
33+
import org.openqa.grid.internal.utils.configuration.GridHubConfiguration;
34+
import org.openqa.grid.internal.utils.configuration.GridNodeConfiguration;
35+
import org.openqa.grid.web.Hub;
36+
import org.openqa.selenium.json.Json;
37+
import org.openqa.selenium.json.JsonInput;
38+
import org.openqa.testing.FakeHttpServletResponse;
39+
import org.seleniumhq.jetty9.server.handler.ContextHandler;
40+
41+
import java.io.IOException;
42+
import java.io.StringReader;
43+
import java.util.List;
44+
import java.util.Map;
45+
46+
import javax.servlet.ServletContext;
47+
import javax.servlet.ServletException;
48+
import javax.servlet.http.HttpServlet;
49+
50+
@RunWith(JUnit4.class)
51+
public class HubStatusServletTest extends RegistrationAwareServletTest {
52+
53+
private static final GridRegistry registry = DefaultGridRegistry
54+
.newInstance(new Hub(new GridHubConfiguration()));
55+
56+
@Before
57+
public void setUp() throws Exception {
58+
servlet = new HubStatusServlet() {
59+
@Override
60+
public ServletContext getServletContext() {
61+
final ContextHandler.Context servletContext = new ContextHandler().getServletContext();
62+
servletContext.setAttribute(GridRegistry.KEY, registry);
63+
return servletContext;
64+
}
65+
};
66+
servlet.init();
67+
}
68+
69+
@Test
70+
public void testGetConfiguration() throws IOException, ServletException {
71+
Map<String, Object> map = invokeCommand("post", null);
72+
assertTrue("capabilityMatcher should be present", map.containsKey("capabilityMatcher"));
73+
}
74+
75+
@Test
76+
public void testSelectiveGetConfiguration() throws IOException, ServletException {
77+
Map<String, Object> map = invokeCommand("get",
78+
ImmutableMap.of("configuration", "debug"));
79+
assertEquals("There should be only 2 keys in the map", 2, map.size());
80+
assertTrue("debug should be present", map.containsKey("debug"));
81+
}
82+
83+
@Test
84+
public void testGetNodeInformation() throws Exception {
85+
wireInNode();
86+
Map<String, Object> map = invokeCommand("get",
87+
ImmutableMap.of("configuration", "nodes"));
88+
assertFalse("Node configuration should not be empty", map.isEmpty());
89+
List<?> nodes = (List<?>) map.get("nodes");
90+
assertEquals("Exactly 1 node info should be present", 1, nodes.size());
91+
Map<?, ?> node = (Map<?, ?>) nodes.get(0);
92+
assertEquals("Two keys should be present per node", 2, node.keySet().size());
93+
}
94+
95+
private void wireInNode() throws Exception {
96+
final GridNodeConfiguration config = new GridNodeConfiguration();
97+
config.id = "http://dummynode:3456";
98+
final RegistrationRequest request = RegistrationRequest.build(config);
99+
request.getConfiguration().proxy = null;
100+
HttpServlet servlet = new RegistrationServlet() {
101+
@Override
102+
public ServletContext getServletContext() {
103+
final ContextHandler.Context servletContext = new ContextHandler().getServletContext();
104+
servletContext.setAttribute(GridRegistry.KEY, registry);
105+
return servletContext;
106+
}
107+
};
108+
servlet.init();
109+
sendCommand(servlet, "POST", "/", request.toJson());
110+
waitForServletToAddProxy();
111+
}
112+
113+
private Map<String, Object> invokeCommand(String method, Map<String, Object> params)
114+
throws IOException, ServletException {
115+
FakeHttpServletResponse fakeResponse = sendCommand(method, "/", params);
116+
Json json = new Json();
117+
JsonInput jin = json.newInput(new StringReader(fakeResponse.getBody()));
118+
return jin.read(Json.MAP_TYPE);
119+
}
120+
121+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.grid.web.servlet;
19+
20+
public class RegistrationAwareServletTest extends BaseServletTest {
21+
22+
/**
23+
* Gives the servlet some time to add the proxy -- which happens on a separate thread.
24+
*/
25+
protected void waitForServletToAddProxy() throws Exception {
26+
int tries = 0;
27+
int size = 0;
28+
while (tries < 10) {
29+
size = ((RegistryBasedServlet) servlet).getRegistry().getAllProxies().size();
30+
if (size > 0) {
31+
break;
32+
}
33+
Thread.sleep(1000);
34+
tries += 1;
35+
}
36+
}
37+
38+
}

0 commit comments

Comments
 (0)