Skip to content

Commit

Permalink
Enriching Hub Status to include Node info (#6127)
Browse files Browse the repository at this point in the history
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
}
  • Loading branch information
krmahadevan authored and shs96c committed Jul 10, 2018
1 parent 3ffb8eb commit 5289e97
Show file tree
Hide file tree
Showing 7 changed files with 278 additions and 61 deletions.
113 changes: 94 additions & 19 deletions java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,32 @@

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

import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Lists;

import org.openqa.grid.internal.GridRegistry;
import org.openqa.grid.internal.RemoteProxy;
import org.openqa.grid.internal.TestSlot;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonException;
import org.openqa.selenium.json.JsonInput;
import org.openqa.selenium.json.JsonOutput;
import org.openqa.selenium.remote.CapabilityType;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collector;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.reducing;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
Expand All @@ -63,10 +71,18 @@
*/
public class HubStatusServlet extends RegistryBasedServlet {

private static final String SUCCESS = "success";
private static final String CONFIGURATION = "configuration";
private static final String FREE = "free";
private static final String BUSY = "busy";
private static final String NEW_SESSION_REQUEST_COUNT = "newSessionRequestCount";
private static final String SLOT_COUNTS = "slotCounts";
private static final String NODES = "nodes";
private static final String TOTAL = "total";
private final Json json = new Json();

public HubStatusServlet() {
super(null);
this(null);
}

public HubStatusServlet(GridRegistry registry) {
Expand All @@ -76,7 +92,7 @@ public HubStatusServlet(GridRegistry registry) {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException {
process(request, response, new HashMap());
process(request, response, new HashMap<>());
}

@Override
Expand Down Expand Up @@ -109,35 +125,41 @@ private Map<String, Object> getResponse(
HttpServletRequest request,
Map<String, Object> requestJSON) {
Map<String, Object> res = new TreeMap<>();
res.put("success", true);
res.put(SUCCESS, true);

try {
List<String> keysToReturn = null;
String configuration = request.getParameter(CONFIGURATION);

if (request.getParameter("configuration") != null && !"".equals(request.getParameter("configuration"))) {
keysToReturn = Arrays.asList(request.getParameter("configuration").split(","));
} else if (requestJSON != null && requestJSON.containsKey("configuration")) {
//noinspection unchecked
keysToReturn = (List<String>) requestJSON.get("configuration");
if (Strings.isNullOrEmpty(configuration)) {
configuration = "";
if (requestJSON.containsKey(CONFIGURATION)) {
//noinspection unchecked
configuration = requestJSON.get(CONFIGURATION).toString();
}
}

List<String> keysToReturn = Splitter.on(",").omitEmptyStrings().splitToList(configuration);

GridRegistry registry = getRegistry();
Map<String, Object> config = registry.getHub().getConfiguration().toJson();
for (Map.Entry<String, Object> entry : config.entrySet()) {
if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains(entry.getKey())) {
if (isKeyPresentIn(keysToReturn, entry.getKey())) {
res.put(entry.getKey(), entry.getValue());
}
}
if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains("newSessionRequestCount")) {
res.put("newSessionRequestCount", registry.getNewSessionRequestCount());
if (isKeyPresentIn(keysToReturn, NEW_SESSION_REQUEST_COUNT)) {
res.put(NEW_SESSION_REQUEST_COUNT, registry.getNewSessionRequestCount());
}

if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains("slotCounts")) {
res.put("slotCounts", getSlotCounts());
if (isKeyPresentIn(keysToReturn, SLOT_COUNTS)) {
res.put(SLOT_COUNTS, getSlotCounts());
}
if (keysToReturn != null && keysToReturn.contains(NODES)) {
res.put(NODES, getNodesInfo());
}
} catch (Exception e) {
res.remove("success");
res.put("success", false);
res.remove(SUCCESS);
res.put(SUCCESS, false);
res.put("msg", e.getMessage());
}
return res;
Expand All @@ -154,8 +176,8 @@ private Map<String, Object> getSlotCounts() {
}

return ImmutableSortedMap.of(
"free", totalSlots - usedSlots,
"total", totalSlots);
FREE, totalSlots - usedSlots,
TOTAL, totalSlots);
}

private Map<String, Object> getRequestJSON(HttpServletRequest request) throws IOException {
Expand All @@ -167,4 +189,57 @@ private Map<String, Object> getRequestJSON(HttpServletRequest request) throws IO
throw new IOException(e);
}
}

private static boolean isKeyPresentIn(List<String> keys, String key) {
return keys == null || keys.isEmpty() || keys.contains(key);
}

private List<Map<String, Object>> getNodesInfo() {
List<RemoteProxy> proxies = getRegistry().getAllProxies().getSorted();
return proxies.stream().map(this::getNodeInfo).collect(toList());
}

private Map<String, Object> getNodeInfo(RemoteProxy remoteProxy) {
return ImmutableSortedMap.of(
"id", remoteProxy.getId(),
"browsers", getInfoFromAllSlotsInNode(remoteProxy.getTestSlots())
);
}

private List<Map<String, Object>> getInfoFromAllSlotsInNode(List<TestSlot> slots) {
List<Map<String, Object>> browsers = Lists.newArrayList();
Map<String, List<TestSlot>>
slotsInfo = slots.stream().collect(groupingBy(HubStatusServlet::getBrowser));
for (Map.Entry<String, List<TestSlot>> each : slotsInfo.entrySet()) {
String key = each.getKey();
Map<String, Object> value = getSlotInfoPerBrowserFlavor(each.getValue());
browsers.add(ImmutableSortedMap.of("browser", key, "slots", value));
}
return browsers;
}

private Map<String, Object> getSlotInfoPerBrowserFlavor(List<TestSlot> slots) {
Map<String, Integer> byStatus = slots.stream().collect(groupingBy(this::status, counting()));
int busy = byStatus.computeIfAbsent(BUSY, status -> 0);
int free = byStatus.computeIfAbsent(FREE, status -> 0);
int total = busy + free;

return ImmutableSortedMap.of(TOTAL, total, BUSY, busy);
}

private String status(TestSlot slot) {
if (slot.getSession() == null) {
return FREE;
}
return BUSY;
}

private static String getBrowser(TestSlot slot) {
return slot.getCapabilities().get(CapabilityType.BROWSER_NAME).toString();
}

private static <T> Collector<T, ?, Integer> counting() {
return reducing(0, e -> 1, Integer::sum);
}

}
20 changes: 18 additions & 2 deletions java/server/test/org/openqa/grid/web/servlet/BaseServletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.openqa.testing.UrlInfo;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

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

protected FakeHttpServletResponse sendCommand(String method, String commandPath)
throws IOException, ServletException {
return sendCommand(method, commandPath, (Map<String, Object>) null);
return sendCommand(method, commandPath, null);
}

protected FakeHttpServletResponse sendCommand(
String method,
String commandPath,
Map<String, Object> parameters) throws IOException, ServletException {
return sendCommand(this.servlet,method, commandPath, parameters);
}

protected static FakeHttpServletResponse sendCommand(
HttpServlet servlet,
String method,
String commandPath,
Map<String, Object> parameters) throws IOException, ServletException {
FakeHttpServletRequest request = new FakeHttpServletRequest(method, createUrl(commandPath));
if (parameters != null) {
if ("get".equalsIgnoreCase(method) && parameters != null) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, Object> parameter : parameters.entrySet()) {
params.put(parameter.getKey(), parameter.getValue().toString());
}
request.setParameters(params);
}
if ("post".equalsIgnoreCase(method) && parameters != null) {
request.setBody(new Json().toJson(parameters));
}
FakeHttpServletResponse response = new FakeHttpServletResponse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
DisplayHelpServletTest.class,
ResourceServletTest.class,
ConsoleServletTest.class,
RegistrationServletTest.class
RegistrationServletTest.class,
HubStatusServletTest.class
})
public class GridServletTests {
}
121 changes: 121 additions & 0 deletions java/server/test/org/openqa/grid/web/servlet/HubStatusServletTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.grid.web.servlet;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.google.common.collect.ImmutableMap;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.openqa.grid.common.RegistrationRequest;
import org.openqa.grid.internal.DefaultGridRegistry;
import org.openqa.grid.internal.GridRegistry;
import org.openqa.grid.internal.utils.configuration.GridHubConfiguration;
import org.openqa.grid.internal.utils.configuration.GridNodeConfiguration;
import org.openqa.grid.web.Hub;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.json.JsonInput;
import org.openqa.testing.FakeHttpServletResponse;
import org.seleniumhq.jetty9.server.handler.ContextHandler;

import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

@RunWith(JUnit4.class)
public class HubStatusServletTest extends RegistrationAwareServletTest {

private static final GridRegistry registry = DefaultGridRegistry
.newInstance(new Hub(new GridHubConfiguration()));

@Before
public void setUp() throws Exception {
servlet = new HubStatusServlet() {
@Override
public ServletContext getServletContext() {
final ContextHandler.Context servletContext = new ContextHandler().getServletContext();
servletContext.setAttribute(GridRegistry.KEY, registry);
return servletContext;
}
};
servlet.init();
}

@Test
public void testGetConfiguration() throws IOException, ServletException {
Map<String, Object> map = invokeCommand("post", null);
assertTrue("capabilityMatcher should be present", map.containsKey("capabilityMatcher"));
}

@Test
public void testSelectiveGetConfiguration() throws IOException, ServletException {
Map<String, Object> map = invokeCommand("get",
ImmutableMap.of("configuration", "debug"));
assertEquals("There should be only 2 keys in the map", 2, map.size());
assertTrue("debug should be present", map.containsKey("debug"));
}

@Test
public void testGetNodeInformation() throws Exception {
wireInNode();
Map<String, Object> map = invokeCommand("get",
ImmutableMap.of("configuration", "nodes"));
assertFalse("Node configuration should not be empty", map.isEmpty());
List<?> nodes = (List<?>) map.get("nodes");
assertEquals("Exactly 1 node info should be present", 1, nodes.size());
Map<?, ?> node = (Map<?, ?>) nodes.get(0);
assertEquals("Two keys should be present per node", 2, node.keySet().size());
}

private void wireInNode() throws Exception {
final GridNodeConfiguration config = new GridNodeConfiguration();
config.id = "http://dummynode:3456";
final RegistrationRequest request = RegistrationRequest.build(config);
request.getConfiguration().proxy = null;
HttpServlet servlet = new RegistrationServlet() {
@Override
public ServletContext getServletContext() {
final ContextHandler.Context servletContext = new ContextHandler().getServletContext();
servletContext.setAttribute(GridRegistry.KEY, registry);
return servletContext;
}
};
servlet.init();
sendCommand(servlet, "POST", "/", request.toJson());
waitForServletToAddProxy();
}

private Map<String, Object> invokeCommand(String method, Map<String, Object> params)
throws IOException, ServletException {
FakeHttpServletResponse fakeResponse = sendCommand(method, "/", params);
Json json = new Json();
JsonInput jin = json.newInput(new StringReader(fakeResponse.getBody()));
return jin.read(Json.MAP_TYPE);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package org.openqa.grid.web.servlet;

public class RegistrationAwareServletTest extends BaseServletTest {

/**
* Gives the servlet some time to add the proxy -- which happens on a separate thread.
*/
protected void waitForServletToAddProxy() throws Exception {
int tries = 0;
int size = 0;
while (tries < 10) {
size = ((RegistryBasedServlet) servlet).getRegistry().getAllProxies().size();
if (size > 0) {
break;
}
Thread.sleep(1000);
tries += 1;
}
}

}
Loading

1 comment on commit 5289e97

@barancev
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. There is already StatusServletTests in org.openqa.grid.internal package, I think we should better add more tests to this calss instead of adding another one.

  2. This commit broke some tests in StatusServletTests, please resolve these failures.

Please sign in to comment.