Skip to content

Commit

Permalink
Optimize _cat/nodes api by sending request to each node and receiving…
Browse files Browse the repository at this point in the history
… response separately

Signed-off-by: kkewwei <[email protected]>
  • Loading branch information
kkewwei committed Jul 31, 2024
1 parent eb306d2 commit 3a496eb
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 36 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Changed
- Add lower limit for primary and replica batch allocators timeout ([#14979](https://github.com/opensearch-project/OpenSearch/pull/14979))
- Optimize _cat/nodes api by sending request to each node and receiving response separately ([#14853](https://github.com/opensearch-project/OpenSearch/pull/14853))

### Deprecated

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.http;

import org.apache.hc.core5.http.ParseException;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.opensearch.client.Request;
import org.opensearch.client.Response;
import org.opensearch.client.ResponseException;
import org.opensearch.client.RestClient;
import org.opensearch.test.OpenSearchIntegTestCase.ClusterScope;
import org.opensearch.test.OpenSearchIntegTestCase.Scope;

import java.io.IOException;

import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.hamcrest.Matchers.containsString;

@ClusterScope(scope = Scope.SUITE, supportsDedicatedMasters = false, numDataNodes = 5, numClientNodes = 0)
public class HttpCatIT extends HttpSmokeTestCase {

public void testdoCatRequest() throws IOException, ParseException {
try (RestClient restClient = getRestClient()) {
int nodesCount = restClient.getNodes().size();
assertEquals(5, nodesCount);

for (int i = 0; i < 2; i++) {
Request nodesRequest = new Request("GET", "/_cat/nodes");
Response response = restClient.performRequest(nodesRequest);
assertEquals(SC_OK, response.getStatusLine().getStatusCode());
String result = EntityUtils.toString(response.getEntity());
String[] NodeInfos = result.split("\n");
assertEquals(nodesCount, NodeInfos.length);
}

for (int i = 1; i < 1500; i+= 50) {
Request nodesRequest = new Request("GET", "/_cat/nodes?timeout=" + i + "ms");
try {
Response response = restClient.performRequest(nodesRequest);
assertEquals(SC_OK, response.getStatusLine().getStatusCode());
String result = EntityUtils.toString(response.getEntity());
String[] NodeInfos = result.split("\n");
assertEquals(nodesCount, NodeInfos.length);
} catch (ResponseException e) {
// it means that it costs too long to get ClusterState from the master.
assertThat(e.getMessage(), containsString("There is not enough time to obtain nodesInfo metric from the cluster manager"));
}
}
}
}

}
210 changes: 174 additions & 36 deletions server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

package org.opensearch.rest.action.cat;

import org.opensearch.OpenSearchTimeoutException;
import org.opensearch.action.FailedNodeException;
import org.opensearch.action.admin.cluster.node.info.NodeInfo;
import org.opensearch.action.admin.cluster.node.info.NodesInfoRequest;
import org.opensearch.action.admin.cluster.node.info.NodesInfoResponse;
Expand All @@ -47,6 +49,8 @@
import org.opensearch.common.Table;
import org.opensearch.common.logging.DeprecationLogger;
import org.opensearch.common.network.NetworkAddress;
import org.opensearch.common.unit.TimeValue;
import org.opensearch.core.action.ActionListener;
import org.opensearch.core.common.Strings;
import org.opensearch.core.common.transport.TransportAddress;
import org.opensearch.core.common.unit.ByteSizeValue;
Expand All @@ -68,15 +72,20 @@
import org.opensearch.monitor.os.OsStats;
import org.opensearch.monitor.process.ProcessInfo;
import org.opensearch.monitor.process.ProcessStats;
import org.opensearch.rest.RestChannel;
import org.opensearch.rest.RestRequest;
import org.opensearch.rest.RestResponse;
import org.opensearch.rest.action.RestActionListener;
import org.opensearch.rest.action.RestResponseListener;
import org.opensearch.script.ScriptStats;
import org.opensearch.search.suggest.completion.CompletionStats;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import static java.util.Collections.singletonList;
Expand All @@ -88,6 +97,7 @@
* @opensearch.api
*/
public class RestNodesAction extends AbstractCatAction {
public static final long TIMEOUT_THRESHOLD_NANO = 5_000_000;
private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RestNodesAction.class);
static final String LOCAL_DEPRECATED_MESSAGE = "Deprecated parameter [local] used. This parameter does not cause this API to act "
+ "locally, and should not be used. It will be unsupported in version 8.0.";
Expand Down Expand Up @@ -120,49 +130,177 @@ public RestChannelConsumer doCatRequest(final RestRequest request, final NodeCli
);
parseDeprecatedMasterTimeoutParameter(clusterStateRequest, request, deprecationLogger, getName());
final boolean fullId = request.paramAsBoolean("full_id", false);
return channel -> client.admin().cluster().state(clusterStateRequest, new RestActionListener<ClusterStateResponse>(channel) {
final long beginTimeNano = System.nanoTime();
final long timeoutNano = request.hasParam("timeout")
? TimeValue.parseTimeValue(request.param("timeout"), "timeout").nanos()

Check warning on line 135 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L135

Added line #L135 was not covered by tests
: Long.MAX_VALUE;
return channel -> client.admin().cluster().state(clusterStateRequest, new RestActionListener<>(channel) {
@Override
public void processResponse(final ClusterStateResponse clusterStateResponse) {
NodesInfoRequest nodesInfoRequest = new NodesInfoRequest();
nodesInfoRequest.timeout(request.param("timeout"));
nodesInfoRequest.setIncludeDiscoveryNodes(false);
nodesInfoRequest.clear()
.addMetrics(
NodesInfoRequest.Metric.JVM.metricName(),
NodesInfoRequest.Metric.OS.metricName(),
NodesInfoRequest.Metric.PROCESS.metricName(),
NodesInfoRequest.Metric.HTTP.metricName()
long leftTimeNano = timeoutNano - System.nanoTime() + beginTimeNano;

Check warning on line 140 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L140

Added line #L140 was not covered by tests
if (leftTimeNano < TIMEOUT_THRESHOLD_NANO) {
onFailure(

Check warning on line 142 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L142

Added line #L142 was not covered by tests
new OpenSearchTimeoutException(
"There is not enough time to obtain nodesInfo metric from the cluster manager:"
+ clusterStateResponse.getState().nodes().getMasterNode().getName()

Check warning on line 145 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L145

Added line #L145 was not covered by tests
)
);
client.admin().cluster().nodesInfo(nodesInfoRequest, new RestActionListener<NodesInfoResponse>(channel) {
@Override
public void processResponse(final NodesInfoResponse nodesInfoResponse) {
NodesStatsRequest nodesStatsRequest = new NodesStatsRequest();
nodesStatsRequest.timeout(request.param("timeout"));
nodesStatsRequest.setIncludeDiscoveryNodes(false);
nodesStatsRequest.clear()
.indices(true)
.addMetrics(
NodesStatsRequest.Metric.JVM.metricName(),
NodesStatsRequest.Metric.OS.metricName(),
NodesStatsRequest.Metric.FS.metricName(),
NodesStatsRequest.Metric.PROCESS.metricName(),
NodesStatsRequest.Metric.SCRIPT.metricName()
return;

Check warning on line 148 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L148

Added line #L148 was not covered by tests
}
String[] nodeIds = clusterStateResponse.getState().nodes().resolveNodes();
ConcurrentMap<String, NodeInfo> successNodeInfos = new ConcurrentHashMap<>(nodeIds.length);
ConcurrentMap<String, FailedNodeException> failNodeInfos = new ConcurrentHashMap<>(nodeIds.length);
ConcurrentMap<String, NodeStats> successNodeStats = new ConcurrentHashMap<>(nodeIds.length);
ConcurrentMap<String, FailedNodeException> failNodeStats = new ConcurrentHashMap<>(nodeIds.length);
AtomicInteger counter = new AtomicInteger();

Check warning on line 155 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L150-L155

Added lines #L150 - L155 were not covered by tests
for (String nodeId : nodeIds) {
NodesInfoRequest nodesInfoRequest = createNodesInfoRequest(timeoutNano, leftTimeNano, nodeId);
nodesInfoRequest.setIncludeDiscoveryNodes(false);
client.admin().cluster().nodesInfo(nodesInfoRequest, new ActionListener<>() {

Check warning on line 159 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L157-L159

Added lines #L157 - L159 were not covered by tests
@Override
public void onResponse(NodesInfoResponse nodesInfoResponse) {
assert nodesInfoResponse.getNodes().size() + nodesInfoResponse.failures().size() == 1;
NodesStatsRequest nodesStatsRequest = checkAndCreateNodesStatsRequest(
nodesInfoResponse.failures(),

Check warning on line 164 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L163-L164

Added lines #L163 - L164 were not covered by tests
timeoutNano,
beginTimeNano,
nodeId,
this::onFailure,
clusterStateResponse.getState().nodes().get(nodeId).getName()

Check warning on line 169 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L169

Added line #L169 was not covered by tests
);
client.admin().cluster().nodesStats(nodesStatsRequest, new RestResponseListener<NodesStatsResponse>(channel) {
@Override
public RestResponse buildResponse(NodesStatsResponse nodesStatsResponse) throws Exception {
return RestTable.buildResponse(
buildTable(fullId, request, clusterStateResponse, nodesInfoResponse, nodesStatsResponse),
channel
);
if (nodesStatsRequest == null) {
return;

Check warning on line 172 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L172

Added line #L172 was not covered by tests
}
});
}
});
successNodeInfos.put(nodeId, nodesInfoResponse.getNodes().get(0));
client.admin().cluster().nodesStats(nodesStatsRequest, ActionListener.runAfter(new ActionListener<>() {

Check warning on line 175 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L174-L175

Added lines #L174 - L175 were not covered by tests
@Override
public void onResponse(NodesStatsResponse nodesStatsResponse) {
assert nodesStatsResponse.getNodes().size() + nodesStatsResponse.failures().size() == 1;
if (nodesStatsResponse.getNodes().size() == 1) {
successNodeStats.put(nodeId, nodesStatsResponse.getNodes().get(0));

Check warning on line 180 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L180

Added line #L180 was not covered by tests
} else {
failNodeStats.put(nodeId, nodesStatsResponse.failures().get(0));

Check warning on line 182 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L182

Added line #L182 was not covered by tests
}
}

Check warning on line 184 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L184

Added line #L184 was not covered by tests

@Override
public void onFailure(Exception e) {
assert e instanceof FailedNodeException;
failNodeStats.put(nodeId, (FailedNodeException) e);
}

Check warning on line 190 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L189-L190

Added lines #L189 - L190 were not covered by tests
}, this::onOperation));
}

Check warning on line 192 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L192

Added line #L192 was not covered by tests

@Override
public void onFailure(Exception e) {
assert e instanceof FailedNodeException;
failNodeInfos.put(nodeId, (FailedNodeException) e);
onOperation();
}

Check warning on line 199 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L197-L199

Added lines #L197 - L199 were not covered by tests

private void onOperation() {
if (counter.incrementAndGet() == nodeIds.length) {
try {
sendResponse(
channel,

Check warning on line 205 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L204-L205

Added lines #L204 - L205 were not covered by tests
clusterStateResponse,
request,
fullId,
successNodeInfos.values(),
failNodeInfos.values(),
successNodeStats.values(),
failNodeStats.values()

Check warning on line 212 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L209-L212

Added lines #L209 - L212 were not covered by tests
);
} catch (Exception e) {
e.addSuppressed(e);
logger.error("failed to send failure response", e);
}

Check warning on line 217 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L214-L217

Added lines #L214 - L217 were not covered by tests
}
}

Check warning on line 219 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L219

Added line #L219 was not covered by tests
});
}
}
});
}

private NodesInfoRequest createNodesInfoRequest(long timeoutNano, long leftTimeNano, String nodeId) {
NodesInfoRequest nodesInfoRequest = new NodesInfoRequest();

Check warning on line 227 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L227

Added line #L227 was not covered by tests
if (timeoutNano != Long.MAX_VALUE) {
nodesInfoRequest.timeout(TimeValue.timeValueNanos(leftTimeNano));

Check warning on line 229 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L229

Added line #L229 was not covered by tests
}
nodesInfoRequest.clear()
.nodesIds(nodeId)
.addMetrics(
NodesInfoRequest.Metric.JVM.metricName(),
NodesInfoRequest.Metric.OS.metricName(),
NodesInfoRequest.Metric.PROCESS.metricName(),
NodesInfoRequest.Metric.HTTP.metricName()

Check warning on line 237 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L231-L237

Added lines #L231 - L237 were not covered by tests
);
return nodesInfoRequest;

Check warning on line 239 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L239

Added line #L239 was not covered by tests
}

private NodesStatsRequest checkAndCreateNodesStatsRequest(
List<FailedNodeException> failedNodeExceptions,
long timeoutNano,
long beginTimeNano,
String nodeId,
Consumer<FailedNodeException> failedConsumer,
String nodeName
) {
if (failedNodeExceptions.isEmpty() == false) {
failedConsumer.accept(failedNodeExceptions.get(0));
return null;

Check warning on line 252 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L251-L252

Added lines #L251 - L252 were not covered by tests
}
long leftTime = timeoutNano - System.nanoTime() + beginTimeNano;

Check warning on line 254 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L254

Added line #L254 was not covered by tests
if (leftTime < TIMEOUT_THRESHOLD_NANO) {
failedConsumer.accept(

Check warning on line 256 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L256

Added line #L256 was not covered by tests
new FailedNodeException(nodeId, "There is not enough time to obtain nodesStats metric from " + nodeName, null)
);
return null;

Check warning on line 259 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L259

Added line #L259 was not covered by tests
}
NodesStatsRequest nodesStatsRequest = new NodesStatsRequest();

Check warning on line 261 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L261

Added line #L261 was not covered by tests
if (timeoutNano != Long.MAX_VALUE) {
nodesStatsRequest.timeout(TimeValue.timeValueMillis(leftTime));

Check warning on line 263 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L263

Added line #L263 was not covered by tests
}
nodesStatsRequest.setIncludeDiscoveryNodes(false);
nodesStatsRequest.clear()
.nodesIds(nodeId)
.indices(true)
.addMetrics(
NodesStatsRequest.Metric.JVM.metricName(),
NodesStatsRequest.Metric.OS.metricName(),
NodesStatsRequest.Metric.FS.metricName(),
NodesStatsRequest.Metric.PROCESS.metricName(),
NodesStatsRequest.Metric.SCRIPT.metricName()

Check warning on line 274 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L265-L274

Added lines #L265 - L274 were not covered by tests
);
return nodesStatsRequest;

Check warning on line 276 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L276

Added line #L276 was not covered by tests
}

private void sendResponse(
RestChannel channel,
ClusterStateResponse clusterStateResponse,
RestRequest request,
boolean fullId,
Collection<NodeInfo> successNodeInfos,
Collection<FailedNodeException> failNodeInfos,
Collection<NodeStats> successNodeStats,
Collection<FailedNodeException> failNodeStats
) throws Exception {
NodesInfoResponse nodesInfoResponse = new NodesInfoResponse(
clusterStateResponse.getClusterName(),

Check warning on line 290 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L289-L290

Added lines #L289 - L290 were not covered by tests
new ArrayList<>(successNodeInfos),
new ArrayList<>(failNodeInfos)
);
NodesStatsResponse nodesStatsResponse = new NodesStatsResponse(
clusterStateResponse.getClusterName(),

Check warning on line 295 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L294-L295

Added lines #L294 - L295 were not covered by tests
new ArrayList<>(successNodeStats),
new ArrayList<>(failNodeStats)
);
channel.sendResponse(
RestTable.buildResponse(buildTable(fullId, request, clusterStateResponse, nodesInfoResponse, nodesStatsResponse), channel)

Check warning on line 300 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L299-L300

Added lines #L299 - L300 were not covered by tests
);
}

Check warning on line 302 in server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java

View check run for this annotation

Codecov / codecov/patch

server/src/main/java/org/opensearch/rest/action/cat/RestNodesAction.java#L302

Added line #L302 was not covered by tests

@Override
protected Table getTableWithHeader(final RestRequest request) {
Table table = new Table();
Expand Down

0 comments on commit 3a496eb

Please sign in to comment.