From dc96ce24ad1bbb9029bef891a98963a267c6fa1e Mon Sep 17 00:00:00 2001 From: Ed Coleman Date: Thu, 16 May 2024 19:48:19 +0000 Subject: [PATCH 1/3] Add service status as command option to admin command --- .../apache/accumulo/server/util/Admin.java | 42 +- .../server/util/ServiceStatusCmd.java | 336 +++++++++++++++ .../serviceStatus/ServiceStatusReport.java | 168 ++++++++ .../util/serviceStatus/StatusSummary.java | 90 ++++ .../server/util/ServiceStatusCmdTest.java | 388 ++++++++++++++++++ .../ServiceStatusReportTest.java | 117 ++++++ 6 files changed, 1128 insertions(+), 13 deletions(-) create mode 100644 server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java create mode 100644 server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java create mode 100644 server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java create mode 100644 server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java create mode 100644 server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/Admin.java b/server/base/src/main/java/org/apache/accumulo/server/util/Admin.java index ff9c4d95926..b8351ee43f9 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/Admin.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/Admin.java @@ -283,6 +283,9 @@ public void execute(final String[] args) { JCommander cl = new JCommander(opts); cl.setProgramName("accumulo admin"); + ServiceStatusCmd.Opts serviceStatusCommandOpts = new ServiceStatusCmd.Opts(); + cl.addCommand("serviceStatus", serviceStatusCommandOpts); + ChangeSecretCommand changeSecretCommand = new ChangeSecretCommand(); cl.addCommand("changeSecret", changeSecretCommand); @@ -398,6 +401,8 @@ public void execute(final String[] args) { tServerLocksOpts.delete); } else if (cl.getParsedCommand().equals("fate")) { executeFateOpsCommand(context, fateOpsCommand); + } else if (cl.getParsedCommand().equals("serviceStatus")) { + printServiceStatus(context, serviceStatusCommandOpts); } else { everything = cl.getParsedCommand().equals("stopAll"); @@ -425,6 +430,11 @@ public void execute(final String[] args) { } } + private static void printServiceStatus(ServerContext context, ServiceStatusCmd.Opts opts) { + ServiceStatusCmd ssc = new ServiceStatusCmd(); + ssc.execute(context, opts); + } + private static int ping(ClientContext context, List args) { InstanceOperations io = context.instanceOperations(); @@ -590,21 +600,11 @@ static String qualifyWithZooKeeperSessionId(String zTServerRoot, ZooCache zooCac private Map siteConfig, systemConfig; private List localUsers; - @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", - justification = "code runs in same security context as user who provided input") public void printConfig(ClientContext context, DumpConfigCommand opts) throws Exception { - File outputDirectory = null; - if (opts.directory != null) { - outputDirectory = new File(opts.directory); - if (!outputDirectory.isDirectory()) { - throw new IllegalArgumentException( - opts.directory + " does not exist on the local filesystem."); - } - if (!outputDirectory.canWrite()) { - throw new IllegalArgumentException(opts.directory + " is not writable"); - } - } + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", + justification = "app is run in same security context as user providing the filename") + File outputDirectory = getOutputDirectory(opts.directory); defaultConfig = DefaultConfiguration.getInstance(); siteConfig = context.instanceOperations().getSiteConfiguration(); systemConfig = context.instanceOperations().getSystemConfiguration(); @@ -651,6 +651,22 @@ public void printConfig(ClientContext context, DumpConfigCommand opts) throws Ex } } + @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", + justification = "app is run in same security context as user providing the filename") + private static File getOutputDirectory(final String directory) { + File outputDirectory = null; + if (directory != null) { + outputDirectory = new File(directory); + if (!outputDirectory.isDirectory()) { + throw new IllegalArgumentException(directory + " does not exist on the local filesystem."); + } + if (!outputDirectory.canWrite()) { + throw new IllegalArgumentException(directory + " is not writable"); + } + } + return outputDirectory; + } + private String getDefaultConfigValue(String key) { if (key == null) { return null; diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java b/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java new file mode 100644 index 00000000000..41186ef21ea --- /dev/null +++ b/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java @@ -0,0 +1,336 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * https://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.apache.accumulo.server.util; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.apache.accumulo.core.Constants; +import org.apache.accumulo.core.fate.zookeeper.ZooReader; +import org.apache.accumulo.core.util.Pair; +import org.apache.accumulo.core.util.ServerServices; +import org.apache.accumulo.server.ServerContext; +import org.apache.accumulo.server.util.serviceStatus.ServiceStatusReport; +import org.apache.accumulo.server.util.serviceStatus.StatusSummary; +import org.apache.zookeeper.KeeperException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.google.common.annotations.VisibleForTesting; + +public class ServiceStatusCmd { + private static final Logger LOG = LoggerFactory.getLogger(ServiceStatusCmd.class); + + public ServiceStatusCmd() {} + + /** + * ServerServices writes lock data as SERVICE=host. This strips the SERVICE= from the string. + * + * @return a sort set of host names. + */ + private static Set stripServiceName(Set hostnames) { + return hostnames.stream().map(h -> { + if (h.contains(ServerServices.SEPARATOR_CHAR)) { + return h.substring(h.indexOf(ServerServices.SEPARATOR_CHAR) + 1); + } + return h; + }).collect(Collectors.toCollection(TreeSet::new)); + } + + /** + * Read the service statuses from ZooKeeper, build the status report and then output the report to + * stdout. + */ + public void execute(final ServerContext context, final Opts opts) { + + ZooReader zooReader = context.getZooReader(); + + final String zooRoot = context.getZooKeeperRoot(); + LOG.trace("zooRoot: {}", zooRoot); + + final Map services = new TreeMap<>(); + + services.put(ServiceStatusReport.ReportKey.MANAGER, getManagerStatus(zooReader, zooRoot)); + services.put(ServiceStatusReport.ReportKey.MONITOR, getMonitorStatus(zooReader, zooRoot)); + services.put(ServiceStatusReport.ReportKey.T_SERVER, getTServerStatus(zooReader, zooRoot)); + services.put(ServiceStatusReport.ReportKey.S_SERVER, getScanServerStatus(zooReader, zooRoot)); + services.put(ServiceStatusReport.ReportKey.COORDINATOR, + getCoordinatorStatus(zooReader, zooRoot)); + services.put(ServiceStatusReport.ReportKey.COMPACTOR, getCompactorStatus(zooReader, zooRoot)); + services.put(ServiceStatusReport.ReportKey.GC, getGcStatus(zooReader, zooRoot)); + + ServiceStatusReport report = new ServiceStatusReport(services, opts.noHosts); + + if (opts.json) { + System.out.println(report.toJson()); + } else { + StringBuilder sb = new StringBuilder(8192); + report.report(sb); + System.out.println(sb); + } + } + + /** + * The manager paths in ZooKeeper are: {@code /accumulo/[IID]/managers/lock/zlock#[NUM]} with the + * lock data providing host:port. + */ + @VisibleForTesting + StatusSummary getManagerStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZMANAGER_LOCK; + return getStatusSummary(ServiceStatusReport.ReportKey.MANAGER, zooReader, lockPath); + } + + /** + * The monitor paths in ZooKeeper are: {@code /accumulo/[IID]/monitor/lock/zlock#[NUM]} with the + * lock data providing host:port. + */ + @VisibleForTesting + StatusSummary getMonitorStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZMONITOR_LOCK; + return getStatusSummary(ServiceStatusReport.ReportKey.MONITOR, zooReader, lockPath); + } + + /** + * The tserver paths in ZooKeeper are: {@code /accumulo/[IID]/tservers/[host:port]/zlock#[NUM]} + * with the lock data providing TSERV_CLIENT=host:port. + */ + @VisibleForTesting + StatusSummary getTServerStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZTSERVERS; + return getServerHostStatus(zooReader, lockPath, ServiceStatusReport.ReportKey.T_SERVER); + } + + /** + * The sserver paths in ZooKeeper are: {@code /accumulo/[IID]/sservers/[host:port]/zlock#[NUM]} + * with the lock data providing [UUID],[GROUP] + */ + @VisibleForTesting + StatusSummary getScanServerStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZSSERVERS; + return getServerHostStatus(zooReader, lockPath, ServiceStatusReport.ReportKey.S_SERVER); + } + + /** + * handles paths for tservers and servers with the lock stored beneath the host: port like: + * {@code /accumulo/IID/[tservers | sservers]/HOST:PORT/[LOCK]} + */ + private StatusSummary getServerHostStatus(final ZooReader zooReader, String basePath, + ServiceStatusReport.ReportKey displayNames) { + AtomicInteger errorSum = new AtomicInteger(0); + + Set hostNames = new TreeSet<>(); + Set groupNames = new TreeSet<>(); + + var nodeNames = readNodeNames(zooReader, basePath); + + nodeNames.getSecond().forEach(name -> { + var lock = readNodeNames(zooReader, basePath + "/" + name); + lock.getSecond().forEach(l -> { + var r = readNodeData(zooReader, basePath + "/" + name + "/" + l); + int err = r.getFirst(); + if (err > 0) { + errorSum.addAndGet(r.getFirst()); + } else { + // process resource groups + var payload = r.getSecond(); + String[] tokens = payload.split(","); + String groupSeparator = ""; + if (tokens.length == 2) { + groupNames.add(tokens[1]); + groupSeparator = tokens[1] + ": "; + } + hostNames.add(groupSeparator + name); + } + + }); + errorSum.addAndGet(lock.getFirst()); + }); + + LOG.trace("Current data: {}", hostNames); + + return new StatusSummary(displayNames, groupNames, new TreeSet<>(hostNames), errorSum.get()); + } + + /** + * The gc paths in ZooKeeper are: {@code /accumulo/[IID]/gc/lock/zlock#[NUM]} with the lock data + * providing GC_CLIENT=host:port + */ + @VisibleForTesting + StatusSummary getGcStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZGC_LOCK; + var temp = getStatusSummary(ServiceStatusReport.ReportKey.GC, zooReader, lockPath); + // remove GC_CLIENT= from displayed host:port + Set hosts = stripServiceName(temp.getServiceNames()); + return new StatusSummary(temp.getReportKey(), temp.getResourceGroups(), hosts, + temp.getErrorCount()); + + } + + /** + * The coordinator paths in ZooKeeper are: {@code /accumulo/[IID]/coordinators/lock/zlock#[NUM]} + * with the lock data providing host:port + */ + @VisibleForTesting + StatusSummary getCoordinatorStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZCOORDINATOR_LOCK; + return getStatusSummary(ServiceStatusReport.ReportKey.COORDINATOR, zooReader, lockPath); + } + + /** + * The compactor paths in ZooKeeper are: + * {@code /accumulo/[IID]/compactors/[QUEUE_NAME]/host:port/zlock#[NUM]} with the host:port pulled + * from the path + */ + @VisibleForTesting + StatusSummary getCompactorStatus(final ZooReader zooReader, String zRootPath) { + String lockPath = zRootPath + Constants.ZCOMPACTORS; + return getCompactorHosts(zooReader, lockPath); + } + + /** + * Used to return status information when path is {@code /accumulo/IID/SERVICE_NAME/lock} like + * manager, monitor and others + * + * @return service status + */ + private StatusSummary getStatusSummary(ServiceStatusReport.ReportKey displayNames, + ZooReader zooReader, String lockPath) { + var result = readAllNodesData(zooReader, lockPath); + return new StatusSummary(displayNames, Set.of(), result.getSecond(), result.getFirst()); + } + + /** + * Pull host:port from path {@code /accumulo/IID/compactors/[QUEUE][host:port]} + */ + private StatusSummary getCompactorHosts(final ZooReader zooReader, final String zRootPath) { + final AtomicInteger errors = new AtomicInteger(0); + final Set hostAndQueue = new TreeSet<>(); + // get group names + Pair> r1 = readNodeNames(zooReader, zRootPath); + errors.addAndGet(r1.getFirst()); + Set queues = new TreeSet<>(r1.getSecond()); + + queues.forEach(g -> { + var r2 = readNodeNames(zooReader, zRootPath + "/" + g); + errors.addAndGet(r2.getFirst()); + Collection hosts = r2.getSecond(); + hosts.forEach(h -> hostAndQueue.add(g + ": " + h)); + + }); + + return new StatusSummary(ServiceStatusReport.ReportKey.COMPACTOR, queues, hostAndQueue, + errors.get()); + } + + /** + * Read the node names from ZooKeeper. Exceptions are counted but ignored. + * + * @return Pair with error count, Collection of the node names. + */ + @VisibleForTesting + Pair> readNodeNames(final ZooReader zooReader, final String path) { + Set nodeNames = new TreeSet<>(); + final AtomicInteger errorCount = new AtomicInteger(0); + try { + var children = zooReader.getChildren(path); + if (children != null) { + nodeNames.addAll(children); + } + } catch (KeeperException | InterruptedException ex) { + if (Thread.currentThread().isInterrupted()) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + errorCount.incrementAndGet(); + } + return new Pair<>(errorCount.get(), nodeNames); + } + + /** + * Read the data from a ZooKeeper node, tracking if an error occurred. ZooKeeper's exceptions are + * counted but otherwise ignored. + * + * @return Pair with error count, the node data as String. + */ + @VisibleForTesting + Pair readNodeData(final ZooReader zooReader, final String path) { + try { + byte[] data = zooReader.getData(path); + return new Pair<>(0, new String(data, UTF_8)); + } catch (KeeperException | InterruptedException ex) { + if (Thread.currentThread().isInterrupted()) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + LOG.info("Could not read locks from ZooKeeper for path {}", path, ex); + return new Pair<>(1, ""); + } + } + + /** + * Read the data from all ZooKeeper nodes under a ptah, tracking if errors occurred. ZooKeeper's + * exceptions are counted but otherwise ignored. + * + * @return Pair with error count, the data from each node as a String. + */ + @VisibleForTesting + Pair> readAllNodesData(final ZooReader zooReader, final String path) { + Set hosts = new TreeSet<>(); + final AtomicInteger errorCount = new AtomicInteger(0); + try { + var locks = zooReader.getChildren(path); + locks.forEach(lock -> { + var r = readNodeData(zooReader, path + "/" + lock); + int err = r.getFirst(); + if (err > 0) { + errorCount.addAndGet(r.getFirst()); + } else { + hosts.add(r.getSecond()); + } + }); + } catch (KeeperException | InterruptedException ex) { + if (Thread.currentThread().isInterrupted()) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + LOG.info("Could not read node names from ZooKeeper for path {}", path, ex); + errorCount.incrementAndGet(); + } + return new Pair<>(errorCount.get(), hosts); + } + + @Parameters(commandDescription = "show service status") + public static class Opts { + @Parameter(names = "--json", description = "provide output in json format (--noHosts ignored)") + boolean json = false; + @Parameter(names = "--noHosts", + description = "provide a summary of service counts without host details") + boolean noHosts = false; + } + +} diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java new file mode 100644 index 00000000000..0c9b1fd64e2 --- /dev/null +++ b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * https://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.apache.accumulo.server.util.serviceStatus; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * Wrapper for JSON formatted report. + */ +public class ServiceStatusReport { + + private static final Logger LOG = LoggerFactory.getLogger(ServiceStatusReport.class); + + private static final Gson gson = new Gson(); + + private static final DateTimeFormatter rptTimeFmt = + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private static final String I2 = " "; + private static final String I4 = " "; + + private final String reportTime; + private final int zkReadErrors; + private final boolean noHosts; + private final Map summaryMap; + + public ServiceStatusReport(final Map summaryMap, final boolean noHosts) { + reportTime = rptTimeFmt.format(ZonedDateTime.now(ZoneId.of("UTC"))); + zkReadErrors = summaryMap.values().stream().map(StatusSummary::getErrorCount) + .reduce(Integer::sum).orElse(0); + this.noHosts = noHosts; + this.summaryMap = summaryMap; + } + + public String getReportTime() { + return reportTime; + } + + public Map getSummaryMap() { + return summaryMap; + } + + public String toJson() { + return gson.toJson(this, ServiceStatusReport.class); + } + + public String report(final StringBuilder sb) { + sb.append("Report time: ").append(rptTimeFmt.format(ZonedDateTime.now(ZoneId.of("UTC")))) + .append("\n"); + int zkErrors = summaryMap.values().stream().map(StatusSummary::getErrorCount) + .reduce(Integer::sum).orElse(0); + sb.append("ZooKeeper read errors: ").append(zkErrors).append("\n"); + + writeServiceStatus(sb, ReportKey.MANAGER, summaryMap.get(ReportKey.MANAGER), noHosts); + writeServiceStatus(sb, ReportKey.MONITOR, summaryMap.get(ReportKey.MONITOR), noHosts); + writeServiceStatus(sb, ReportKey.GC, summaryMap.get(ReportKey.GC), noHosts); + writeServiceStatus(sb, ReportKey.T_SERVER, summaryMap.get(ReportKey.T_SERVER), noHosts); + writeResourceGroups(sb, ReportKey.S_SERVER, summaryMap.get(ReportKey.S_SERVER), noHosts); + writeServiceStatus(sb, ReportKey.COORDINATOR, summaryMap.get(ReportKey.COORDINATOR), noHosts); + writeResourceGroups(sb, ReportKey.COMPACTOR, summaryMap.get(ReportKey.COMPACTOR), noHosts); + + sb.append("\n"); + LOG.trace("fmtStatus - with hosts: {}", summaryMap); + return sb.toString(); + } + + public void writeServiceStatus(final StringBuilder sb, final ReportKey displayNames, + final StatusSummary summary, boolean noHosts) { + if (summary == null) { + sb.append(displayNames).append(": unavailable").append("\n"); + return; + } + + writeCounts(sb, summary); + + // skip host info if requested + if (noHosts) { + return; + } + if (summary.getServiceCount() > 0) { + var hosts = summary.getServiceNames(); + hosts.forEach(h -> sb.append(I2).append(h).append("\n")); + } + } + + private void writeCounts(StringBuilder sb, StatusSummary summary) { + sb.append(summary.getDisplayName()).append(": count: ").append(summary.getServiceCount()); + if (summary.getErrorCount() > 0) { + sb.append(", (ZooKeeper errors: ").append(summary.getErrorCount()).append(")\n"); + } else { + sb.append("\n"); + } + } + + private void writeResourceGroups(final StringBuilder sb, final ReportKey reportKey, + final StatusSummary summary, boolean noHosts) { + if (summary == null) { + sb.append(reportKey).append(": unavailable").append("\n"); + return; + } + + writeCounts(sb, summary); + + // skip host info if requested + if (noHosts) { + return; + } + if (!summary.getResourceGroups().isEmpty()) { + sb.append(I2).append("resource groups:\n"); + summary.getResourceGroups().forEach(g -> sb.append(I4).append(g).append("\n")); + + if (summary.getServiceCount() > 0) { + sb.append(I2).append("hosts (by group):\n"); + var hosts = summary.getServiceNames(); + hosts.forEach(h -> sb.append(I4).append(h).append("\n")); + } + } + } + + @Override + public String toString() { + return "ServiceStatusReport{reportTime='" + reportTime + '\'' + ", zkReadErrors=" + zkReadErrors + + ", noHosts=" + noHosts + ", status=" + summaryMap + '}'; + } + + public enum ReportKey { + COMPACTOR("Compactors"), + COORDINATOR("Coordinators"), + GC("Garbage Collectors"), + MANAGER("Managers"), + MONITOR("Monitors"), + S_SERVER("Scan Servers"), + T_SERVER("Tablet Servers"); + + private final String displayName; + + ReportKey(final String name) { + this.displayName = name; + } + + public String getDisplayName() { + return displayName; + } + } +} diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java new file mode 100644 index 00000000000..e3473d9d5e9 --- /dev/null +++ b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * https://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.apache.accumulo.server.util.serviceStatus; + +import java.util.Objects; +import java.util.Set; + +public class StatusSummary { + + private final ServiceStatusReport.ReportKey reportKey; + private final Set resourceGroups; + private final Set serviceNames; + private final int serviceCount; + private final int errorCount; + + public StatusSummary(ServiceStatusReport.ReportKey reportKey, final Set resourceGroups, + final Set serviceNames, final int errorCount) { + this.reportKey = reportKey; + this.resourceGroups = resourceGroups; + this.serviceNames = serviceNames; + this.serviceCount = serviceNames.size(); + this.errorCount = errorCount; + } + + public ServiceStatusReport.ReportKey getReportKey() { + return reportKey; + } + + public String getDisplayName() { + return reportKey.getDisplayName(); + } + + public Set getResourceGroups() { + return resourceGroups; + } + + public Set getServiceNames() { + return serviceNames; + } + + public int getServiceCount() { + return serviceCount; + } + + public int getErrorCount() { + return errorCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StatusSummary)) { + return false; + } + StatusSummary that = (StatusSummary) o; + return serviceCount == that.serviceCount && errorCount == that.errorCount + && reportKey == that.reportKey && Objects.equals(resourceGroups, that.resourceGroups) + && Objects.equals(serviceNames, that.serviceNames); + } + + @Override + public int hashCode() { + return Objects.hash(reportKey, resourceGroups, serviceCount, serviceNames, errorCount); + } + + @Override + public String toString() { + return "StatusSummary{serviceName=" + reportKey + ", resourceGroups=" + resourceGroups + + ", serviceCount=" + serviceCount + ", names=" + serviceNames + ", errorCount=" + + errorCount + '}'; + } +} diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java new file mode 100644 index 00000000000..524d0dcbc09 --- /dev/null +++ b/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java @@ -0,0 +1,388 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * https://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.apache.accumulo.server.util; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.accumulo.core.Constants.ZGC_LOCK; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +import org.apache.accumulo.core.Constants; +import org.apache.accumulo.core.data.InstanceId; +import org.apache.accumulo.core.fate.zookeeper.ZooReader; +import org.apache.accumulo.server.ServerContext; +import org.apache.accumulo.server.util.serviceStatus.ServiceStatusReport; +import org.apache.accumulo.server.util.serviceStatus.StatusSummary; +import org.apache.zookeeper.KeeperException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ServiceStatusCmdTest { + + private static final Logger LOG = LoggerFactory.getLogger(ServiceStatusCmdTest.class); + + private ServerContext context; + private String zRoot; + private ZooReader zooReader; + + @BeforeEach + public void populateContext() { + InstanceId iid = InstanceId.of(UUID.randomUUID()); + zRoot = "/accumulo/" + iid.canonical(); + context = createMock(ServerContext.class); + expect(context.getInstanceID()).andReturn(iid).anyTimes(); + expect(context.getZooKeeperRoot()).andReturn(zRoot).anyTimes(); + + zooReader = createMock(ZooReader.class); + + expect(context.getZooReader()).andReturn(zooReader).anyTimes(); + + replay(context); + } + + @AfterEach + public void validateMocks() { + verify(context, zooReader); + } + + @Test + void testManagerHosts() throws Exception { + String lock1Name = "zlock#" + UUID.randomUUID() + "#0000000001"; + String lock2Name = "zlock#" + UUID.randomUUID() + "#0000000002"; + String lock3Name = "zlock#" + UUID.randomUUID() + "#0000000003"; + + String host1 = "hostA:8080"; + String host2 = "hostB:9090"; + String host3 = "host1:9091"; + + String lockPath = zRoot + Constants.ZMANAGER_LOCK; + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of(lock1Name, lock2Name, lock3Name)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock1Name))).andReturn(host1.getBytes(UTF_8)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock2Name))).andReturn(host2.getBytes(UTF_8)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock3Name))).andReturn(host3.getBytes(UTF_8)) + .anyTimes(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getManagerStatus(zooReader, zRoot); + LOG.info("manager status data: {}", status); + + assertEquals(3, status.getServiceCount()); + + // expect sorted by name + Set hosts = new TreeSet<>(List.of(host1, host2, host3)); + StatusSummary expected = + new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, Set.of(), hosts, 0); + + assertEquals(expected.hashCode(), status.hashCode()); + assertEquals(expected.getDisplayName(), status.getDisplayName()); + assertEquals(expected.getResourceGroups(), status.getResourceGroups()); + assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceCount(), status.getServiceCount()); + assertEquals(expected.getErrorCount(), status.getErrorCount()); + assertEquals(expected, status); + } + + @Test + void testMonitorHosts() throws Exception { + String lock1Name = "zlock#" + UUID.randomUUID() + "#0000000001"; + String lock2Name = "zlock#" + UUID.randomUUID() + "#0000000002"; + + String host1 = "hostA:8080"; + String host2 = "host1:9091"; + + String lockPath = zRoot + Constants.ZMONITOR_LOCK; + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of(lock1Name, lock2Name)).anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock1Name))).andReturn(host1.getBytes(UTF_8)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock2Name))).andReturn(host2.getBytes(UTF_8)) + .anyTimes(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getMonitorStatus(zooReader, zRoot); + LOG.info("monitor status data: {}", status); + + assertEquals(2, status.getServiceCount()); + + // expect sorted by name + Set hosts = new TreeSet<>(List.of(host1, host2)); + StatusSummary expected = + new StatusSummary(ServiceStatusReport.ReportKey.MONITOR, Set.of(), hosts, 0); + + assertEquals(expected.hashCode(), status.hashCode()); + assertEquals(expected.getDisplayName(), status.getDisplayName()); + assertEquals(expected.getResourceGroups(), status.getResourceGroups()); + assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceCount(), status.getServiceCount()); + assertEquals(expected.getErrorCount(), status.getErrorCount()); + assertEquals(expected, status); + } + + @Test + void testTServerHosts() throws Exception { + String lock1Name = "zlock#" + UUID.randomUUID() + "#0000000001"; + String lock2Name = "zlock#" + UUID.randomUUID() + "#0000000002"; + String lock3Name = "zlock#" + UUID.randomUUID() + "#0000000003"; + + String host1 = "hostA:8080"; + String host2 = "hostB:9090"; + String host3 = "host1:9091"; + + String basePath = zRoot + Constants.ZTSERVERS; + expect(zooReader.getChildren(eq(basePath))).andReturn(List.of(host1, host2, host3)).anyTimes(); + + expect(zooReader.getChildren(eq(basePath + "/" + host1))).andReturn(List.of(lock1Name)).once(); + expect(zooReader.getData(eq(basePath + "/" + host1 + "/" + lock1Name))) + .andReturn(("TSERV_CLIENT=" + host1).getBytes(UTF_8)).anyTimes(); + + expect(zooReader.getChildren(eq(basePath + "/" + host2))).andReturn(List.of(lock2Name)).once(); + expect(zooReader.getData(eq(basePath + "/" + host2 + "/" + lock2Name))) + .andReturn(("TSERV_CLIENT=" + host2).getBytes(UTF_8)).anyTimes(); + + expect(zooReader.getChildren(eq(basePath + "/" + host3))).andReturn(List.of(lock3Name)).once(); + expect(zooReader.getData(eq(basePath + "/" + host3 + "/" + lock3Name))) + .andReturn(("TSERV_CLIENT=" + host3).getBytes(UTF_8)).anyTimes(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getTServerStatus(zooReader, zRoot); + LOG.info("tserver status data: {}", status); + + assertEquals(3, status.getServiceCount()); + + // expect sorted by name + Set hosts = new TreeSet<>(List.of(host1, host2, host3)); + StatusSummary expected = + new StatusSummary(ServiceStatusReport.ReportKey.T_SERVER, Set.of(), hosts, 0); + + assertEquals(expected.hashCode(), status.hashCode()); + assertEquals(expected.getDisplayName(), status.getDisplayName()); + assertEquals(expected.getResourceGroups(), status.getResourceGroups()); + assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceCount(), status.getServiceCount()); + assertEquals(expected.getErrorCount(), status.getErrorCount()); + assertEquals(expected, status); + } + + @Test + void testScanServerHosts() throws Exception { + UUID uuid1 = UUID.randomUUID(); + String lock1Name = "zlock#" + uuid1 + "#0000000001"; + UUID uuid2 = UUID.randomUUID(); + String lock2Name = "zlock#" + uuid2 + "#0000000022"; + UUID uuid3 = UUID.randomUUID(); + String lock3Name = "zlock#" + uuid3 + "#0000000033"; + String lock4Name = "zlock#" + uuid3 + "#0000000044"; + + // UUID uuidLock = UUID.randomUUID(); + + String host1 = "host1:8080"; + String host2 = "host2:9090"; + String host3 = "host3:9091"; + String host4 = "host4:9091"; + + String lockPath = zRoot + Constants.ZSSERVERS; + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of(host1, host2, host3, host4)) + .anyTimes(); + + expect(zooReader.getChildren(eq(lockPath + "/" + host1))).andReturn(List.of(lock1Name)).once(); + expect(zooReader.getData(eq(lockPath + "/" + host1 + "/" + lock1Name))) + .andReturn((UUID.randomUUID() + ",rg1").getBytes(UTF_8)).once(); + + expect(zooReader.getChildren(eq(lockPath + "/" + host2))).andReturn(List.of(lock2Name)).once(); + expect(zooReader.getData(eq(lockPath + "/" + host2 + "/" + lock2Name))) + .andReturn((UUID.randomUUID() + ",default").getBytes(UTF_8)).once(); + + expect(zooReader.getChildren(eq(lockPath + "/" + host3))).andReturn(List.of(lock3Name)).once(); + expect(zooReader.getData(eq(lockPath + "/" + host3 + "/" + lock3Name))) + .andReturn((UUID.randomUUID() + ",rg1").getBytes(UTF_8)).once(); + + expect(zooReader.getChildren(eq(lockPath + "/" + host4))).andReturn(List.of(lock4Name)).once(); + expect(zooReader.getData(eq(lockPath + "/" + host4 + "/" + lock4Name))) + .andReturn((UUID.randomUUID() + ",default").getBytes(UTF_8)).once(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getScanServerStatus(zooReader, zRoot); + assertEquals(4, status.getServiceCount()); + + Set hosts = + Set.of("default: host2:9090", "default: host4:9091", "rg1: host1:8080", "rg1: host3:9091"); + StatusSummary expected = new StatusSummary(ServiceStatusReport.ReportKey.S_SERVER, + Set.of("default", "rg1"), hosts, 0); + + assertEquals(expected, status); + + } + + @Test + void testCoordinatorHosts() throws Exception { + String lock1Name = "zlock#" + UUID.randomUUID() + "#0000000001"; + String lock2Name = "zlock#" + UUID.randomUUID() + "#0000000002"; + String lock3Name = "zlock#" + UUID.randomUUID() + "#0000000003"; + + String host1 = "hostA:8080"; + String host2 = "hostB:9090"; + String host3 = "host1:9091"; + + String lockPath = zRoot + Constants.ZCOORDINATOR_LOCK; + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of(lock1Name, lock2Name, lock3Name)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock1Name))).andReturn(host1.getBytes(UTF_8)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock2Name))).andReturn(host2.getBytes(UTF_8)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock3Name))).andReturn(host3.getBytes(UTF_8)) + .anyTimes(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getCoordinatorStatus(zooReader, zRoot); + LOG.info("tserver status data: {}", status); + + assertEquals(3, status.getServiceCount()); + + // expect sorted by name + Set hosts = new TreeSet<>(List.of(host1, host2, host3)); + StatusSummary expected = + new StatusSummary(ServiceStatusReport.ReportKey.COORDINATOR, Set.of(), hosts, 0); + + assertEquals(expected.hashCode(), status.hashCode()); + assertEquals(expected.getDisplayName(), status.getDisplayName()); + assertEquals(expected.getResourceGroups(), status.getResourceGroups()); + assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceCount(), status.getServiceCount()); + assertEquals(expected.getErrorCount(), status.getErrorCount()); + assertEquals(expected, status); + } + + @Test + public void testCompactorStatus() throws Exception { + String lockPath = zRoot + Constants.ZCOMPACTORS; + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of("q1", "q2")).once(); + + expect(zooReader.getChildren(eq(lockPath + "/q1"))) + .andReturn(List.of("hostA:8080", "hostC:8081")).once(); + expect(zooReader.getChildren(eq(lockPath + "/q2"))) + .andReturn(List.of("hostB:9090", "hostD:9091")).once(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getCompactorStatus(zooReader, zRoot); + LOG.info("compactor group counts: {}", status); + assertEquals(2, status.getResourceGroups().size()); + } + + @Test + public void testGcHosts() throws Exception { + + String lockPath = zRoot + ZGC_LOCK; + UUID uuid1 = UUID.randomUUID(); + String lock1Name = "zlock#" + uuid1 + "#0000000001"; + UUID uuid2 = UUID.randomUUID(); + String lock2Name = "zlock#" + uuid2 + "#0000000022"; + + String host1 = "host1:8080"; + String host2 = "host2:9090"; + + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of(lock1Name, lock2Name)).once(); + expect(zooReader.getData(eq(lockPath + "/" + lock1Name))) + .andReturn(("GC_CLIENT=" + host1).getBytes(UTF_8)).once(); + expect(zooReader.getData(eq(lockPath + "/" + lock2Name))) + .andReturn(("GC_CLIENT=" + host2).getBytes(UTF_8)).once(); + + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getGcStatus(zooReader, zRoot); + LOG.info("gc server counts: {}", status); + assertEquals(0, status.getResourceGroups().size()); + assertEquals(2, status.getServiceCount()); + assertEquals(0, status.getErrorCount()); + assertEquals(2, status.getServiceNames().size()); + assertEquals(new TreeSet<>(List.of(host1, host2)), status.getServiceNames()); + } + + /** + * Simulates node being deleted after lock list is read from ZooKeeper. Expect that the no node + * error is skipped and available hosts are returned. + */ + @Test + void zkNodeDeletedTest() throws Exception { + String lock1Name = "zlock#" + UUID.randomUUID() + "#0000000001"; + String lock2Name = "zlock#" + UUID.randomUUID() + "#0000000022"; + String lock3Name = "zlock#" + UUID.randomUUID() + "#0000000099"; + String host2 = "hostZ:8080"; + String host3 = "hostA:8080"; + + String lockPath = zRoot + Constants.ZMANAGER_LOCK; + expect(zooReader.getChildren(eq(lockPath))).andReturn(List.of(lock1Name, lock2Name, lock3Name)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock1Name))) + .andThrow(new KeeperException.NoNodeException("no node forced exception")).anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock2Name))).andReturn(host2.getBytes(UTF_8)) + .anyTimes(); + expect(zooReader.getData(eq(lockPath + "/" + lock3Name))).andReturn(host3.getBytes(UTF_8)) + .anyTimes(); + replay(zooReader); + + ServiceStatusCmd cmd = new ServiceStatusCmd(); + StatusSummary status = cmd.getManagerStatus(zooReader, zRoot); + LOG.info("manager status data: {}", status); + + assertEquals(2, status.getServiceNames().size()); + assertEquals(1, status.getErrorCount()); + + // host 1 missing - no node exception + Set sortedHosts = new TreeSet<>(List.of(host3, host2)); + assertEquals(sortedHosts, status.getServiceNames()); + } + + @Test + public void testServiceStatusCommandOpts() { + replay(zooReader); // needed for @AfterAll verify + ServiceStatusCmd.Opts opts = new ServiceStatusCmd.Opts(); + assertFalse(opts.json); + assertFalse(opts.noHosts); + } + +} diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java new file mode 100644 index 00000000000..eaf3053e422 --- /dev/null +++ b/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 + * + * https://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.apache.accumulo.server.util.serviceStatus; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ServiceStatusReportTest { + + private static final Logger LOG = LoggerFactory.getLogger(ServiceStatusReport.class); + + @Test + public void printOutputCountTest() { + final Map services = buildHostStatus(); + ServiceStatusReport report = new ServiceStatusReport(services, true); + StringBuilder sb = new StringBuilder(8192); + report.report(sb); + LOG.info("Report: \n{}", sb); + assertTrue(sb.length() > 0); + } + + @Test + public void printOutputHostTest() { + final Map services = buildHostStatus(); + ServiceStatusReport report = new ServiceStatusReport(services, false); + StringBuilder sb = new StringBuilder(8192); + report.report(sb); + LOG.info("Report: \n{}", sb); + assertTrue(sb.length() > 0); + } + + @Test + public void printJsonHostTest() { + final Map services = buildHostStatus(); + ServiceStatusReport report = new ServiceStatusReport(services, false); + var output = report.toJson(); + LOG.info("{}", output); + assertTrue(!output.isEmpty()); + } + + /** + * validate reduce / sum is correct + */ + @Test + public void sumTest() { + final Map services = buildHostStatus(); + int count = + services.values().stream().map(StatusSummary::getErrorCount).reduce(Integer::sum).orElse(0); + assertEquals(4, count); + } + + private Map buildHostStatus() { + final Map services = new TreeMap<>(); + + StatusSummary managerSummary = new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, + Set.of(), Set.of("host1:8080", "host2:9090"), 1); + services.put(ServiceStatusReport.ReportKey.MANAGER, managerSummary); + + StatusSummary monitorSummary = new StatusSummary(ServiceStatusReport.ReportKey.MONITOR, + Set.of(), Set.of("host1:8080", "host2:9090"), 0); + services.put(ServiceStatusReport.ReportKey.MONITOR, monitorSummary); + + StatusSummary gcSummary = new StatusSummary(ServiceStatusReport.ReportKey.GC, Set.of(), + Set.of("host1:8080", "host2:9090"), 0); + services.put(ServiceStatusReport.ReportKey.GC, gcSummary); + + StatusSummary tserverSummary = + new StatusSummary(ServiceStatusReport.ReportKey.T_SERVER, Set.of(), + new TreeSet<>(List.of("host2:9090", "host4:9091", "host1:8080", "host3:9091")), 1); + services.put(ServiceStatusReport.ReportKey.T_SERVER, tserverSummary); + + StatusSummary scanServerSummary = new StatusSummary(ServiceStatusReport.ReportKey.S_SERVER, + new TreeSet<>(List.of("default", "rg1", "rg2")), new TreeSet<>(List + .of("default, host2:9090", "rg2, host4:9091", "rg1, host1:8080", "rg1, host3:9091")), + 2); + services.put(ServiceStatusReport.ReportKey.S_SERVER, scanServerSummary); + + StatusSummary coordinatorSummary = new StatusSummary(ServiceStatusReport.ReportKey.COORDINATOR, + Set.of(), new TreeSet<>(List.of("host4:9090", "host2:9091")), 0); + services.put(ServiceStatusReport.ReportKey.COORDINATOR, coordinatorSummary); + + StatusSummary compactorSummary = new StatusSummary(ServiceStatusReport.ReportKey.COMPACTOR, + new TreeSet<>(List.of("q2", "q1")), new TreeSet<>(List.of("q2: host2:9090", + "q2: host4:9091", "q1: host3:8080", "q1: host1:9091", "q2: host5:8080")), + 0); + services.put(ServiceStatusReport.ReportKey.COMPACTOR, compactorSummary); + + return services; + } + +} From f5f92218bd01312ed5395180632d4bef8a856b55 Mon Sep 17 00:00:00 2001 From: Ed Coleman Date: Tue, 21 May 2024 17:33:27 +0000 Subject: [PATCH 2/3] Organize service status by resource group. This addresses PR comment to avoid displaying resource groups multiple times. It also changes the json output to use group name and a set of hosts. This makes it easier to pull out resources by resource group. Also includes some code clean-up --- .../server/util/ServiceStatusCmd.java | 146 +++++++++++------- .../serviceStatus/ServiceStatusReport.java | 61 ++++---- .../util/serviceStatus/StatusSummary.java | 36 +++-- .../server/util/ServiceStatusCmdTest.java | 52 +++++-- .../ServiceStatusReportTest.java | 51 ++++-- 5 files changed, 208 insertions(+), 138 deletions(-) diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java b/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java index 41186ef21ea..204e536c934 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/ServiceStatusCmd.java @@ -44,24 +44,14 @@ import com.google.common.annotations.VisibleForTesting; public class ServiceStatusCmd { + + // used when grouping by resource group when there is no group. + public static final String NO_GROUP_TAG = "NO_GROUP"; + private static final Logger LOG = LoggerFactory.getLogger(ServiceStatusCmd.class); public ServiceStatusCmd() {} - /** - * ServerServices writes lock data as SERVICE=host. This strips the SERVICE= from the string. - * - * @return a sort set of host names. - */ - private static Set stripServiceName(Set hostnames) { - return hostnames.stream().map(h -> { - if (h.contains(ServerServices.SEPARATOR_CHAR)) { - return h.substring(h.indexOf(ServerServices.SEPARATOR_CHAR) + 1); - } - return h; - }).collect(Collectors.toCollection(TreeSet::new)); - } - /** * Read the service statuses from ZooKeeper, build the status report and then output the report to * stdout. @@ -143,37 +133,35 @@ private StatusSummary getServerHostStatus(final ZooReader zooReader, String base ServiceStatusReport.ReportKey displayNames) { AtomicInteger errorSum = new AtomicInteger(0); - Set hostNames = new TreeSet<>(); + // Set hostNames = new TreeSet<>(); Set groupNames = new TreeSet<>(); + Map> hostsByGroups = new TreeMap<>(); var nodeNames = readNodeNames(zooReader, basePath); - nodeNames.getSecond().forEach(name -> { - var lock = readNodeNames(zooReader, basePath + "/" + name); - lock.getSecond().forEach(l -> { - var r = readNodeData(zooReader, basePath + "/" + name + "/" + l); - int err = r.getFirst(); + nodeNames.getHosts().forEach(host -> { + var lock = readNodeNames(zooReader, basePath + "/" + host); + lock.getHosts().forEach(l -> { + var nodeData = readNodeData(zooReader, basePath + "/" + host + "/" + l); + int err = nodeData.getErrorCount(); if (err > 0) { - errorSum.addAndGet(r.getFirst()); + errorSum.addAndGet(nodeData.getErrorCount()); } else { // process resource groups - var payload = r.getSecond(); - String[] tokens = payload.split(","); - String groupSeparator = ""; + String[] tokens = nodeData.getHosts().split(","); if (tokens.length == 2) { - groupNames.add(tokens[1]); - groupSeparator = tokens[1] + ": "; + String groupName = tokens[1]; + groupNames.add(groupName); + hostsByGroups.computeIfAbsent(groupName, s -> new TreeSet<>()).add(host); + } else { + hostsByGroups.computeIfAbsent(NO_GROUP_TAG, s -> new TreeSet<>()).add(host); } - hostNames.add(groupSeparator + name); } }); errorSum.addAndGet(lock.getFirst()); }); - - LOG.trace("Current data: {}", hostNames); - - return new StatusSummary(displayNames, groupNames, new TreeSet<>(hostNames), errorSum.get()); + return new StatusSummary(displayNames, groupNames, hostsByGroups, errorSum.get()); } /** @@ -185,12 +173,31 @@ StatusSummary getGcStatus(final ZooReader zooReader, String zRootPath) { String lockPath = zRootPath + Constants.ZGC_LOCK; var temp = getStatusSummary(ServiceStatusReport.ReportKey.GC, zooReader, lockPath); // remove GC_CLIENT= from displayed host:port - Set hosts = stripServiceName(temp.getServiceNames()); - return new StatusSummary(temp.getReportKey(), temp.getResourceGroups(), hosts, + Set hosts = + new TreeSet<>(stripServiceName(temp.getServiceByGroups().get(NO_GROUP_TAG))); + + Map> hostByGroup = new TreeMap<>(); + hostByGroup.put(NO_GROUP_TAG, hosts); + + return new StatusSummary(temp.getServiceType(), temp.getResourceGroups(), hostByGroup, temp.getErrorCount()); } + /** + * ServerServices writes lock data as [SERVICE]=host. This strips the [SERVICE]= from the string. + * + * @return a sort set of host names. + */ + private Set stripServiceName(Set hostnames) { + return hostnames.stream().map(h -> { + if (h.contains(ServerServices.SEPARATOR_CHAR)) { + return h.substring(h.indexOf(ServerServices.SEPARATOR_CHAR) + 1); + } + return h; + }).collect(Collectors.toCollection(TreeSet::new)); + } + /** * The coordinator paths in ZooKeeper are: {@code /accumulo/[IID]/coordinators/lock/zlock#[NUM]} * with the lock data providing host:port @@ -221,7 +228,9 @@ StatusSummary getCompactorStatus(final ZooReader zooReader, String zRootPath) { private StatusSummary getStatusSummary(ServiceStatusReport.ReportKey displayNames, ZooReader zooReader, String lockPath) { var result = readAllNodesData(zooReader, lockPath); - return new StatusSummary(displayNames, Set.of(), result.getSecond(), result.getFirst()); + Map> byGroup = new TreeMap<>(); + byGroup.put(NO_GROUP_TAG, result.getHosts()); + return new StatusSummary(displayNames, Set.of(), byGroup, result.getErrorCount()); } /** @@ -229,31 +238,34 @@ private StatusSummary getStatusSummary(ServiceStatusReport.ReportKey displayName */ private StatusSummary getCompactorHosts(final ZooReader zooReader, final String zRootPath) { final AtomicInteger errors = new AtomicInteger(0); - final Set hostAndQueue = new TreeSet<>(); - // get group names - Pair> r1 = readNodeNames(zooReader, zRootPath); - errors.addAndGet(r1.getFirst()); - Set queues = new TreeSet<>(r1.getSecond()); - queues.forEach(g -> { - var r2 = readNodeNames(zooReader, zRootPath + "/" + g); - errors.addAndGet(r2.getFirst()); - Collection hosts = r2.getSecond(); - hosts.forEach(h -> hostAndQueue.add(g + ": " + h)); + Map> hostsByGroups = new TreeMap<>(); + // get group names + Result> queueNodes = readNodeNames(zooReader, zRootPath); + errors.addAndGet(queueNodes.getErrorCount()); + Set queues = new TreeSet<>(queueNodes.getHosts()); + + queues.forEach(group -> { + var hostNames = readNodeNames(zooReader, zRootPath + "/" + group); + errors.addAndGet(hostNames.getErrorCount()); + Collection hosts = hostNames.getHosts(); + hosts.forEach(host -> { + hostsByGroups.computeIfAbsent(group, set -> new TreeSet<>()).add(host); + }); }); - return new StatusSummary(ServiceStatusReport.ReportKey.COMPACTOR, queues, hostAndQueue, + return new StatusSummary(ServiceStatusReport.ReportKey.COMPACTOR, queues, hostsByGroups, errors.get()); } /** * Read the node names from ZooKeeper. Exceptions are counted but ignored. * - * @return Pair with error count, Collection of the node names. + * @return Result with error count, Set of the node names. */ @VisibleForTesting - Pair> readNodeNames(final ZooReader zooReader, final String path) { + Result> readNodeNames(final ZooReader zooReader, final String path) { Set nodeNames = new TreeSet<>(); final AtomicInteger errorCount = new AtomicInteger(0); try { @@ -268,7 +280,7 @@ Pair> readNodeNames(final ZooReader zooReader, final } errorCount.incrementAndGet(); } - return new Pair<>(errorCount.get(), nodeNames); + return new Result<>(errorCount.get(), nodeNames); } /** @@ -278,17 +290,17 @@ Pair> readNodeNames(final ZooReader zooReader, final * @return Pair with error count, the node data as String. */ @VisibleForTesting - Pair readNodeData(final ZooReader zooReader, final String path) { + Result readNodeData(final ZooReader zooReader, final String path) { try { byte[] data = zooReader.getData(path); - return new Pair<>(0, new String(data, UTF_8)); + return new Result<>(0, new String(data, UTF_8)); } catch (KeeperException | InterruptedException ex) { if (Thread.currentThread().isInterrupted()) { Thread.currentThread().interrupt(); throw new IllegalStateException(ex); } LOG.info("Could not read locks from ZooKeeper for path {}", path, ex); - return new Pair<>(1, ""); + return new Result<>(1, ""); } } @@ -299,18 +311,18 @@ Pair readNodeData(final ZooReader zooReader, final String path) * @return Pair with error count, the data from each node as a String. */ @VisibleForTesting - Pair> readAllNodesData(final ZooReader zooReader, final String path) { + Result> readAllNodesData(final ZooReader zooReader, final String path) { Set hosts = new TreeSet<>(); final AtomicInteger errorCount = new AtomicInteger(0); try { var locks = zooReader.getChildren(path); locks.forEach(lock -> { - var r = readNodeData(zooReader, path + "/" + lock); - int err = r.getFirst(); + var nodeData = readNodeData(zooReader, path + "/" + lock); + int err = nodeData.getErrorCount(); if (err > 0) { - errorCount.addAndGet(r.getFirst()); + errorCount.addAndGet(nodeData.getErrorCount()); } else { - hosts.add(r.getSecond()); + hosts.add(nodeData.getHosts()); } }); } catch (KeeperException | InterruptedException ex) { @@ -321,7 +333,7 @@ Pair> readAllNodesData(final ZooReader zooReader, final Stri LOG.info("Could not read node names from ZooKeeper for path {}", path, ex); errorCount.incrementAndGet(); } - return new Pair<>(errorCount.get(), hosts); + return new Result<>(errorCount.get(), hosts); } @Parameters(commandDescription = "show service status") @@ -333,4 +345,24 @@ public static class Opts { boolean noHosts = false; } + /** + * Provides explicit method names instead of generic getFirst to get the error count and getSecond + * hosts information + * + * @param errorCount + * @param hosts + */ + private static class Result extends Pair { + public Result(A errorCount, B hosts) { + super(errorCount, hosts); + } + + public A getErrorCount() { + return getFirst(); + } + + public B getHosts() { + return getSecond(); + } + } } diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java index 0c9b1fd64e2..818ae495adf 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java @@ -41,26 +41,19 @@ public class ServiceStatusReport { DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); private static final String I2 = " "; private static final String I4 = " "; + private static final String I6 = " "; private final String reportTime; private final int zkReadErrors; private final boolean noHosts; - private final Map summaryMap; + private final Map summaries; - public ServiceStatusReport(final Map summaryMap, final boolean noHosts) { + public ServiceStatusReport(final Map summaries, final boolean noHosts) { reportTime = rptTimeFmt.format(ZonedDateTime.now(ZoneId.of("UTC"))); - zkReadErrors = summaryMap.values().stream().map(StatusSummary::getErrorCount) + zkReadErrors = summaries.values().stream().map(StatusSummary::getErrorCount) .reduce(Integer::sum).orElse(0); this.noHosts = noHosts; - this.summaryMap = summaryMap; - } - - public String getReportTime() { - return reportTime; - } - - public Map getSummaryMap() { - return summaryMap; + this.summaries = summaries; } public String toJson() { @@ -70,43 +63,43 @@ public String toJson() { public String report(final StringBuilder sb) { sb.append("Report time: ").append(rptTimeFmt.format(ZonedDateTime.now(ZoneId.of("UTC")))) .append("\n"); - int zkErrors = summaryMap.values().stream().map(StatusSummary::getErrorCount) + int zkErrors = summaries.values().stream().map(StatusSummary::getErrorCount) .reduce(Integer::sum).orElse(0); sb.append("ZooKeeper read errors: ").append(zkErrors).append("\n"); - writeServiceStatus(sb, ReportKey.MANAGER, summaryMap.get(ReportKey.MANAGER), noHosts); - writeServiceStatus(sb, ReportKey.MONITOR, summaryMap.get(ReportKey.MONITOR), noHosts); - writeServiceStatus(sb, ReportKey.GC, summaryMap.get(ReportKey.GC), noHosts); - writeServiceStatus(sb, ReportKey.T_SERVER, summaryMap.get(ReportKey.T_SERVER), noHosts); - writeResourceGroups(sb, ReportKey.S_SERVER, summaryMap.get(ReportKey.S_SERVER), noHosts); - writeServiceStatus(sb, ReportKey.COORDINATOR, summaryMap.get(ReportKey.COORDINATOR), noHosts); - writeResourceGroups(sb, ReportKey.COMPACTOR, summaryMap.get(ReportKey.COMPACTOR), noHosts); + fmtServiceStatus(sb, ReportKey.MANAGER, summaries.get(ReportKey.MANAGER), noHosts); + fmtServiceStatus(sb, ReportKey.MONITOR, summaries.get(ReportKey.MONITOR), noHosts); + fmtServiceStatus(sb, ReportKey.GC, summaries.get(ReportKey.GC), noHosts); + fmtServiceStatus(sb, ReportKey.T_SERVER, summaries.get(ReportKey.T_SERVER), noHosts); + fmtResourceGroups(sb, ReportKey.S_SERVER, summaries.get(ReportKey.S_SERVER), noHosts); + fmtServiceStatus(sb, ReportKey.COORDINATOR, summaries.get(ReportKey.COORDINATOR), noHosts); + fmtResourceGroups(sb, ReportKey.COMPACTOR, summaries.get(ReportKey.COMPACTOR), noHosts); sb.append("\n"); - LOG.trace("fmtStatus - with hosts: {}", summaryMap); + LOG.trace("fmtStatus - with hosts: {}", summaries); return sb.toString(); } - public void writeServiceStatus(final StringBuilder sb, final ReportKey displayNames, + private void fmtServiceStatus(final StringBuilder sb, final ReportKey displayNames, final StatusSummary summary, boolean noHosts) { if (summary == null) { sb.append(displayNames).append(": unavailable").append("\n"); return; } - writeCounts(sb, summary); + fmtCounts(sb, summary); // skip host info if requested if (noHosts) { return; } if (summary.getServiceCount() > 0) { - var hosts = summary.getServiceNames(); - hosts.forEach(h -> sb.append(I2).append(h).append("\n")); + var hosts = summary.getServiceByGroups(); + hosts.values().forEach(s -> s.forEach(h -> sb.append(I2).append(h).append("\n"))); } } - private void writeCounts(StringBuilder sb, StatusSummary summary) { + private void fmtCounts(StringBuilder sb, StatusSummary summary) { sb.append(summary.getDisplayName()).append(": count: ").append(summary.getServiceCount()); if (summary.getErrorCount() > 0) { sb.append(", (ZooKeeper errors: ").append(summary.getErrorCount()).append(")\n"); @@ -115,27 +108,33 @@ private void writeCounts(StringBuilder sb, StatusSummary summary) { } } - private void writeResourceGroups(final StringBuilder sb, final ReportKey reportKey, + private void fmtResourceGroups(final StringBuilder sb, final ReportKey reportKey, final StatusSummary summary, boolean noHosts) { if (summary == null) { sb.append(reportKey).append(": unavailable").append("\n"); return; } - writeCounts(sb, summary); + fmtCounts(sb, summary); // skip host info if requested if (noHosts) { return; } + if (!summary.getResourceGroups().isEmpty()) { sb.append(I2).append("resource groups:\n"); summary.getResourceGroups().forEach(g -> sb.append(I4).append(g).append("\n")); if (summary.getServiceCount() > 0) { sb.append(I2).append("hosts (by group):\n"); - var hosts = summary.getServiceNames(); - hosts.forEach(h -> sb.append(I4).append(h).append("\n")); + var groups = summary.getServiceByGroups(); + groups.forEach((g, h) -> { + sb.append(I4).append(g).append(" (").append(h.size()).append(")").append(":\n"); + h.forEach(n -> { + sb.append(I6).append(n).append("\n"); + }); + }); } } } @@ -143,7 +142,7 @@ private void writeResourceGroups(final StringBuilder sb, final ReportKey reportK @Override public String toString() { return "ServiceStatusReport{reportTime='" + reportTime + '\'' + ", zkReadErrors=" + zkReadErrors - + ", noHosts=" + noHosts + ", status=" + summaryMap + '}'; + + ", noHosts=" + noHosts + ", status=" + summaries + '}'; } public enum ReportKey { diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java index e3473d9d5e9..9ae9e87d5e9 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/StatusSummary.java @@ -18,40 +18,42 @@ */ package org.apache.accumulo.server.util.serviceStatus; +import java.util.Map; import java.util.Objects; import java.util.Set; public class StatusSummary { - private final ServiceStatusReport.ReportKey reportKey; + private final ServiceStatusReport.ReportKey serviceType; private final Set resourceGroups; - private final Set serviceNames; + private final Map> serviceByGroups; private final int serviceCount; private final int errorCount; - public StatusSummary(ServiceStatusReport.ReportKey reportKey, final Set resourceGroups, - final Set serviceNames, final int errorCount) { - this.reportKey = reportKey; + public StatusSummary(ServiceStatusReport.ReportKey serviceType, final Set resourceGroups, + final Map> serviceByGroups, final int errorCount) { + this.serviceType = serviceType; this.resourceGroups = resourceGroups; - this.serviceNames = serviceNames; - this.serviceCount = serviceNames.size(); + this.serviceByGroups = serviceByGroups; + this.serviceCount = + serviceByGroups.values().stream().map(Set::size).reduce(Integer::sum).orElse(0); this.errorCount = errorCount; } - public ServiceStatusReport.ReportKey getReportKey() { - return reportKey; + public ServiceStatusReport.ReportKey getServiceType() { + return serviceType; } public String getDisplayName() { - return reportKey.getDisplayName(); + return serviceType.getDisplayName(); } public Set getResourceGroups() { return resourceGroups; } - public Set getServiceNames() { - return serviceNames; + public Map> getServiceByGroups() { + return serviceByGroups; } public int getServiceCount() { @@ -72,19 +74,19 @@ public boolean equals(Object o) { } StatusSummary that = (StatusSummary) o; return serviceCount == that.serviceCount && errorCount == that.errorCount - && reportKey == that.reportKey && Objects.equals(resourceGroups, that.resourceGroups) - && Objects.equals(serviceNames, that.serviceNames); + && serviceType == that.serviceType && Objects.equals(resourceGroups, that.resourceGroups) + && Objects.equals(serviceByGroups, that.serviceByGroups); } @Override public int hashCode() { - return Objects.hash(reportKey, resourceGroups, serviceCount, serviceNames, errorCount); + return Objects.hash(serviceType, resourceGroups, serviceCount, serviceByGroups, errorCount); } @Override public String toString() { - return "StatusSummary{serviceName=" + reportKey + ", resourceGroups=" + resourceGroups - + ", serviceCount=" + serviceCount + ", names=" + serviceNames + ", errorCount=" + return "StatusSummary{serviceName=" + serviceType + ", resourceGroups=" + resourceGroups + + ", serviceCount=" + serviceCount + ", names=" + serviceByGroups + ", errorCount=" + errorCount + '}'; } } diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java index 524d0dcbc09..f4af7497986 100644 --- a/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java +++ b/server/base/src/test/java/org/apache/accumulo/server/util/ServiceStatusCmdTest.java @@ -20,6 +20,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.accumulo.core.Constants.ZGC_LOCK; +import static org.apache.accumulo.server.util.ServiceStatusCmd.NO_GROUP_TAG; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; @@ -29,7 +30,9 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.UUID; @@ -104,13 +107,16 @@ void testManagerHosts() throws Exception { // expect sorted by name Set hosts = new TreeSet<>(List.of(host1, host2, host3)); + Map> hostByGroup = new TreeMap<>(); + hostByGroup.put(NO_GROUP_TAG, hosts); + StatusSummary expected = - new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, Set.of(), hosts, 0); + new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, Set.of(), hostByGroup, 0); assertEquals(expected.hashCode(), status.hashCode()); assertEquals(expected.getDisplayName(), status.getDisplayName()); assertEquals(expected.getResourceGroups(), status.getResourceGroups()); - assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceByGroups(), status.getServiceByGroups()); assertEquals(expected.getServiceCount(), status.getServiceCount()); assertEquals(expected.getErrorCount(), status.getErrorCount()); assertEquals(expected, status); @@ -140,14 +146,16 @@ void testMonitorHosts() throws Exception { assertEquals(2, status.getServiceCount()); // expect sorted by name - Set hosts = new TreeSet<>(List.of(host1, host2)); + Map> hostByGroup = new TreeMap<>(); + hostByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of(host1, host2))); + StatusSummary expected = - new StatusSummary(ServiceStatusReport.ReportKey.MONITOR, Set.of(), hosts, 0); + new StatusSummary(ServiceStatusReport.ReportKey.MONITOR, Set.of(), hostByGroup, 0); assertEquals(expected.hashCode(), status.hashCode()); assertEquals(expected.getDisplayName(), status.getDisplayName()); assertEquals(expected.getResourceGroups(), status.getResourceGroups()); - assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceByGroups(), status.getServiceByGroups()); assertEquals(expected.getServiceCount(), status.getServiceCount()); assertEquals(expected.getErrorCount(), status.getErrorCount()); assertEquals(expected, status); @@ -187,14 +195,16 @@ void testTServerHosts() throws Exception { assertEquals(3, status.getServiceCount()); // expect sorted by name - Set hosts = new TreeSet<>(List.of(host1, host2, host3)); + Map> hostByGroup = new TreeMap<>(); + hostByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of(host1, host2, host3))); + StatusSummary expected = - new StatusSummary(ServiceStatusReport.ReportKey.T_SERVER, Set.of(), hosts, 0); + new StatusSummary(ServiceStatusReport.ReportKey.T_SERVER, Set.of(), hostByGroup, 0); assertEquals(expected.hashCode(), status.hashCode()); assertEquals(expected.getDisplayName(), status.getDisplayName()); assertEquals(expected.getResourceGroups(), status.getResourceGroups()); - assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceByGroups(), status.getServiceByGroups()); assertEquals(expected.getServiceCount(), status.getServiceCount()); assertEquals(expected.getErrorCount(), status.getErrorCount()); assertEquals(expected, status); @@ -243,10 +253,12 @@ void testScanServerHosts() throws Exception { StatusSummary status = cmd.getScanServerStatus(zooReader, zRoot); assertEquals(4, status.getServiceCount()); - Set hosts = - Set.of("default: host2:9090", "default: host4:9091", "rg1: host1:8080", "rg1: host3:9091"); + Map> hostByGroup = new TreeMap<>(); + hostByGroup.put("default", new TreeSet<>(List.of("host2:9090", "host4:9091"))); + hostByGroup.put("rg1", new TreeSet<>(List.of("host1:8080", "host3:9091"))); + StatusSummary expected = new StatusSummary(ServiceStatusReport.ReportKey.S_SERVER, - Set.of("default", "rg1"), hosts, 0); + Set.of("default", "rg1"), hostByGroup, 0); assertEquals(expected, status); @@ -282,13 +294,16 @@ void testCoordinatorHosts() throws Exception { // expect sorted by name Set hosts = new TreeSet<>(List.of(host1, host2, host3)); + Map> hostByGroup = new TreeMap<>(); + hostByGroup.put(NO_GROUP_TAG, hosts); + StatusSummary expected = - new StatusSummary(ServiceStatusReport.ReportKey.COORDINATOR, Set.of(), hosts, 0); + new StatusSummary(ServiceStatusReport.ReportKey.COORDINATOR, Set.of(), hostByGroup, 0); assertEquals(expected.hashCode(), status.hashCode()); assertEquals(expected.getDisplayName(), status.getDisplayName()); assertEquals(expected.getResourceGroups(), status.getResourceGroups()); - assertEquals(expected.getServiceNames(), status.getServiceNames()); + assertEquals(expected.getServiceByGroups(), status.getServiceByGroups()); assertEquals(expected.getServiceCount(), status.getServiceCount()); assertEquals(expected.getErrorCount(), status.getErrorCount()); assertEquals(expected, status); @@ -338,8 +353,10 @@ public void testGcHosts() throws Exception { assertEquals(0, status.getResourceGroups().size()); assertEquals(2, status.getServiceCount()); assertEquals(0, status.getErrorCount()); - assertEquals(2, status.getServiceNames().size()); - assertEquals(new TreeSet<>(List.of(host1, host2)), status.getServiceNames()); + assertEquals(1, status.getServiceByGroups().size()); + assertEquals(2, status.getServiceByGroups().get(NO_GROUP_TAG).size()); + assertEquals(new TreeSet<>(List.of(host1, host2)), + status.getServiceByGroups().get(NO_GROUP_TAG)); } /** @@ -369,12 +386,13 @@ void zkNodeDeletedTest() throws Exception { StatusSummary status = cmd.getManagerStatus(zooReader, zRoot); LOG.info("manager status data: {}", status); - assertEquals(2, status.getServiceNames().size()); + assertEquals(1, status.getServiceByGroups().size()); + assertEquals(2, status.getServiceByGroups().get(NO_GROUP_TAG).size()); assertEquals(1, status.getErrorCount()); // host 1 missing - no node exception Set sortedHosts = new TreeSet<>(List.of(host3, host2)); - assertEquals(sortedHosts, status.getServiceNames()); + assertEquals(sortedHosts, status.getServiceByGroups().get(NO_GROUP_TAG)); } @Test diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java index eaf3053e422..ff4b5de1bc5 100644 --- a/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java +++ b/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java @@ -18,7 +18,9 @@ */ package org.apache.accumulo.server.util.serviceStatus; +import static org.apache.accumulo.server.util.ServiceStatusCmd.NO_GROUP_TAG; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -61,7 +63,7 @@ public void printJsonHostTest() { ServiceStatusReport report = new ServiceStatusReport(services, false); var output = report.toJson(); LOG.info("{}", output); - assertTrue(!output.isEmpty()); + assertFalse(output.isEmpty()); } /** @@ -78,37 +80,54 @@ public void sumTest() { private Map buildHostStatus() { final Map services = new TreeMap<>(); - StatusSummary managerSummary = new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, - Set.of(), Set.of("host1:8080", "host2:9090"), 1); + Map> managerByGroup = new TreeMap<>(); + managerByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("host1:8080", "host2:9090"))); + StatusSummary managerSummary = + new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, Set.of(), managerByGroup, 1); services.put(ServiceStatusReport.ReportKey.MANAGER, managerSummary); - StatusSummary monitorSummary = new StatusSummary(ServiceStatusReport.ReportKey.MONITOR, - Set.of(), Set.of("host1:8080", "host2:9090"), 0); + Map> monitorByGroup = new TreeMap<>(); + monitorByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("host1:8080", "host2:9090"))); + StatusSummary monitorSummary = + new StatusSummary(ServiceStatusReport.ReportKey.MONITOR, Set.of(), monitorByGroup, 0); services.put(ServiceStatusReport.ReportKey.MONITOR, monitorSummary); - StatusSummary gcSummary = new StatusSummary(ServiceStatusReport.ReportKey.GC, Set.of(), - Set.of("host1:8080", "host2:9090"), 0); + Map> gcByGroup = new TreeMap<>(); + gcByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("host1:8080", "host2:9090"))); + + StatusSummary gcSummary = + new StatusSummary(ServiceStatusReport.ReportKey.GC, Set.of(), gcByGroup, 0); services.put(ServiceStatusReport.ReportKey.GC, gcSummary); + Map> tserverByGroup = new TreeMap<>(); + tserverByGroup.put(NO_GROUP_TAG, + new TreeSet<>(List.of("host2:9090", "host4:9091", "host1:8080", "host3:9091"))); + StatusSummary tserverSummary = - new StatusSummary(ServiceStatusReport.ReportKey.T_SERVER, Set.of(), - new TreeSet<>(List.of("host2:9090", "host4:9091", "host1:8080", "host3:9091")), 1); + new StatusSummary(ServiceStatusReport.ReportKey.T_SERVER, Set.of(), tserverByGroup, 1); services.put(ServiceStatusReport.ReportKey.T_SERVER, tserverSummary); + Map> sserverByGroup = new TreeMap<>(); + sserverByGroup.put("default", new TreeSet<>(List.of("host2:9090"))); + sserverByGroup.put("rg1", new TreeSet<>(List.of("host1:8080", "host3:9091"))); + sserverByGroup.put("rg2", new TreeSet<>(List.of("host4:9091"))); + StatusSummary scanServerSummary = new StatusSummary(ServiceStatusReport.ReportKey.S_SERVER, - new TreeSet<>(List.of("default", "rg1", "rg2")), new TreeSet<>(List - .of("default, host2:9090", "rg2, host4:9091", "rg1, host1:8080", "rg1, host3:9091")), - 2); + new TreeSet<>(List.of("default", "rg1", "rg2")), sserverByGroup, 2); services.put(ServiceStatusReport.ReportKey.S_SERVER, scanServerSummary); + Map> coordinatorByGroup = new TreeMap<>(); + coordinatorByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("host4:9090", "host2:9091"))); StatusSummary coordinatorSummary = new StatusSummary(ServiceStatusReport.ReportKey.COORDINATOR, - Set.of(), new TreeSet<>(List.of("host4:9090", "host2:9091")), 0); + Set.of(), coordinatorByGroup, 0); services.put(ServiceStatusReport.ReportKey.COORDINATOR, coordinatorSummary); + Map> compactorByGroup = new TreeMap<>(); + compactorByGroup.put("q2", new TreeSet<>(List.of("host5:8080", "host2:9090", "host4:9091"))); + compactorByGroup.put("q1", new TreeSet<>(List.of("host3:8080", "host1:9091"))); + StatusSummary compactorSummary = new StatusSummary(ServiceStatusReport.ReportKey.COMPACTOR, - new TreeSet<>(List.of("q2", "q1")), new TreeSet<>(List.of("q2: host2:9090", - "q2: host4:9091", "q1: host3:8080", "q1: host1:9091", "q2: host5:8080")), - 0); + new TreeSet<>(List.of("q2", "q1")), compactorByGroup, 0); services.put(ServiceStatusReport.ReportKey.COMPACTOR, compactorSummary); return services; From 647a6cbd476b7b32d84796c799ef0e86b9c0335a Mon Sep 17 00:00:00 2001 From: Ed Coleman Date: Wed, 22 May 2024 12:47:54 +0000 Subject: [PATCH 3/3] Add report getters and json round trip test --- .../serviceStatus/ServiceStatusReport.java | 16 +++++++++++ .../ServiceStatusReportTest.java | 27 ++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java index 818ae495adf..09518310282 100644 --- a/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java +++ b/server/base/src/main/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReport.java @@ -56,10 +56,26 @@ public ServiceStatusReport(final Map summaries, final b this.summaries = summaries; } + public String getReportTime() { + return reportTime; + } + + public int getTotalZkReadErrors() { + return zkReadErrors; + } + + public Map getSummaries() { + return summaries; + } + public String toJson() { return gson.toJson(this, ServiceStatusReport.class); } + public static ServiceStatusReport fromJson(final String json) { + return gson.fromJson(json, ServiceStatusReport.class); + } + public String report(final StringBuilder sb) { sb.append("Report time: ").append(rptTimeFmt.format(ZonedDateTime.now(ZoneId.of("UTC")))) .append("\n"); diff --git a/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java b/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java index ff4b5de1bc5..b90e2c8378b 100644 --- a/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java +++ b/server/base/src/test/java/org/apache/accumulo/server/util/serviceStatus/ServiceStatusReportTest.java @@ -19,8 +19,10 @@ package org.apache.accumulo.server.util.serviceStatus; import static org.apache.accumulo.server.util.ServiceStatusCmd.NO_GROUP_TAG; +import static org.apache.accumulo.server.util.serviceStatus.ServiceStatusReport.ReportKey.MANAGER; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.List; @@ -66,6 +68,26 @@ public void printJsonHostTest() { assertFalse(output.isEmpty()); } + @Test + public void jsonRoundTripTest() { + final Map services = new TreeMap<>(); + + Map> managerByGroup = new TreeMap<>(); + managerByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("hostZ:8080", "hostA:9090"))); + StatusSummary managerSummary = new StatusSummary(MANAGER, Set.of(), managerByGroup, 1); + services.put(MANAGER, managerSummary); + ServiceStatusReport report = new ServiceStatusReport(services, false); + var encoded = report.toJson(); + + ServiceStatusReport decoded = ServiceStatusReport.fromJson(encoded); + assertNotNull(decoded.getReportTime()); + assertEquals(1, decoded.getTotalZkReadErrors()); + assertEquals(1, report.getSummaries().size()); + + var byGroup = report.getSummaries().get(MANAGER).getServiceByGroups(); + assertEquals(new TreeSet<>(List.of("hostZ:8080", "hostA:9090")), byGroup.get(NO_GROUP_TAG)); + } + /** * validate reduce / sum is correct */ @@ -82,9 +104,8 @@ private Map buildHostStatus() { Map> managerByGroup = new TreeMap<>(); managerByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("host1:8080", "host2:9090"))); - StatusSummary managerSummary = - new StatusSummary(ServiceStatusReport.ReportKey.MANAGER, Set.of(), managerByGroup, 1); - services.put(ServiceStatusReport.ReportKey.MANAGER, managerSummary); + StatusSummary managerSummary = new StatusSummary(MANAGER, Set.of(), managerByGroup, 1); + services.put(MANAGER, managerSummary); Map> monitorByGroup = new TreeMap<>(); monitorByGroup.put(NO_GROUP_TAG, new TreeSet<>(List.of("host1:8080", "host2:9090")));