/**
* Copyright (c) 2016 Evolveum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.evolveum.midpoint.test.util;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.testng.AssertJUnit;
import com.evolveum.midpoint.util.DebugDumpable;
import com.evolveum.midpoint.util.DebugUtil;
import com.evolveum.midpoint.util.logging.Trace;
import com.evolveum.midpoint.util.logging.TraceManager;
/**
* WARNING! Only works on Linux.
*
* @author semancik
*/
public class Lsof implements DebugDumpable {
private static final Trace LOGGER = TraceManager.getTrace(Lsof.class);
private int pid;
private int toleranceUp = 2;
private int toleranceDown = 10;
private String lsofOutput;
private int totalFds;
private Map<String, Integer> typeMap;
private Map<String, Integer> miscMap;
private Map<String, String> nodeMap;
private String baselineLsofOutput;
private int baselineTotalFds;
private Map<String, Integer> baselineTypeMap;
private Map<String, Integer> baselineMiscMap;
private Map<String, String> baselineNodeMap;
public Lsof(int pid) {
super();
this.pid = pid;
}
public int getToleranceUp() {
return toleranceUp;
}
public void setToleranceUp(int tolerance) {
this.toleranceUp = tolerance;
}
public int getToleranceDown() {
return toleranceDown;
}
public void setToleranceDown(int toleranceDown) {
this.toleranceDown = toleranceDown;
}
public int rememberBaseline() throws NumberFormatException, IOException, InterruptedException {
baselineTotalFds = count();
baselineLsofOutput = lsofOutput;
baselineTypeMap = typeMap;
baselineMiscMap = miscMap;
baselineNodeMap = nodeMap;
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Baseline LSOF output:\n{}", baselineLsofOutput);
}
return baselineTotalFds;
}
public int count() throws NumberFormatException, IOException, InterruptedException {
lsofOutput = execLsof(pid);
// if (LOGGER.isTraceEnabled()) {
// LOGGER.trace("LSOF output:\n{}", lsofOutput);
// }
String[] lines = lsofOutput.split("\n");
Pattern fdPattern = Pattern.compile("(\\d+)(\\S*)");
Pattern namePatternJar = Pattern.compile("/.+\\.jar");
Pattern namePatternFile = Pattern.compile("/.*");
Pattern namePatternPipe = Pattern.compile("pipe");
Pattern namePatternEventpoll = Pattern.compile("\\[eventpoll\\]");
typeMap = new HashMap<>();
miscMap = new HashMap<>();
nodeMap = new HashMap<>();
totalFds = 0;
for (int lineNum = 1; lineNum < lines.length; lineNum++) {
String line = lines[lineNum];
String[] columns = line.split("\\s+");
String pidCol = columns[1];
if (Integer.parseInt(pidCol) != pid) {
throw new IllegalStateException("Unexpected pid in line "+lineNum+", expected "+pid+"\n"+line);
}
String fd = columns[3];
Matcher fdMatcher = fdPattern.matcher(fd);
// if (!fdMatcher.matches()) {
// LOGGER.trace("SKIP fd {}", fd);
// continue;
// }
totalFds++;
String type = columns[4];
increment(typeMap, type);
String node = columns[7];
String nodeKey = node;
if (!StringUtils.isNumeric(nodeKey)) {
nodeKey = nodeKey + ":" + fd;
}
nodeMap.put(nodeKey, line);
String name = columns[8];
if (namePatternJar.matcher(name).matches()) {
increment(miscMap, "jar");
} else if (namePatternFile.matcher(name).matches()) {
increment(miscMap, "file");
} else if (namePatternPipe.matcher(name).matches()) {
increment(miscMap, "pipe");
} else if (namePatternEventpoll.matcher(name).matches()) {
increment(miscMap, "eventpoll");
} else if ("TCP".equals(node)) {
increment(miscMap, "TCP");
} else {
increment(miscMap, "other");
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("lsof counts:\n{}", debugDump(1));
}
return totalFds;
}
private void increment(Map<String, Integer> map, String key) {
Integer typeCount = map.get(key);
if (typeCount == null) {
typeCount = 0;
}
typeCount++;
map.put(key, typeCount);
}
private String execLsof(int pid) throws IOException, InterruptedException {
Process process = null;
String output = null;
try {
process = Runtime.getRuntime().exec(new String[]{ "lsof", "-p", Integer.toString(pid) });
InputStream inputStream = process.getInputStream();
output = IOUtils.toString(inputStream, "UTF-8");
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IllegalStateException("Lsof process ended with error ("+exitCode+")");
}
} finally {
if (process != null) {
try {
process.getInputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
try {
process.getOutputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
try {
process.getErrorStream().close();
} catch (IOException e) {
e.printStackTrace();
}
process.destroy();
}
}
return output;
}
public void assertStable() throws NumberFormatException, IOException, InterruptedException {
count();
if (!checkWithinTolerance(baselineTotalFds, totalFds)) {
LOGGER.debug("FD situation UNSTABLE ({} -> {}):\n{}", baselineTotalFds, totalFds, debugDump(1));
logFailDump();
AssertJUnit.fail("Unexpected number of open FDs, expected: "+baselineTotalFds+", but was "+totalFds+" (tolerance +"+toleranceUp+"/-"+toleranceDown+")");
} else {
LOGGER.debug("FD situation stable (total {})", totalFds);
}
}
public void assertFdIncrease(int increase) throws NumberFormatException, IOException, InterruptedException {
count();
if (!checkWithinTolerance(baselineTotalFds + increase, totalFds)) {
LOGGER.debug("Unexpected FD number increase {} ({} -> {}):\n{}", (totalFds - baselineTotalFds), baselineTotalFds, totalFds, debugDump(1));
logFailDump();
AssertJUnit.fail("Unexpected FD number increase, expected increase " + increase + " ("+ (baselineTotalFds + increase) +"), but was "
+ (totalFds - baselineTotalFds) + " (" + totalFds + ")"+" (tolerance +"+toleranceUp+"/-"+toleranceDown+")");
} else {
LOGGER.debug("Expected increase of {} FDs (total {})", increase, totalFds);
}
}
private boolean checkWithinTolerance(int expected, int was) {
return (was <= (expected + toleranceUp)) && (was >= (expected - toleranceDown));
}
private void logFailDump() {
LOGGER.debug("types:\n{}", diffMap(baselineTypeMap, typeMap));
LOGGER.debug("misc:\n{}", diffMap(baselineMiscMap, miscMap));
LOGGER.debug("nodes:\n{}", diffNodeMap());
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("LSOF output:\n{}", lsofOutput);
}
}
private String diffNodeMap() {
StringBuilder sb = new StringBuilder();
for (Entry<String, String> baselineEntry: baselineNodeMap.entrySet()) {
if (nodeMap.get(baselineEntry.getKey()) == null) {
sb.append("- ").append(baselineEntry.getValue()).append("\n");
}
}
for (Entry<String, String> currentEntry: nodeMap.entrySet()) {
if (baselineNodeMap.get(currentEntry.getKey()) == null) {
sb.append("+ ").append(currentEntry.getValue()).append("\n");
}
}
return sb.toString();
}
private String diffMap(Map<String, Integer> baselineMap, Map<String, Integer> currentMap) {
StringBuilder sb = new StringBuilder();
for (Entry<String, Integer> currentEntry: currentMap.entrySet()) {
Integer currentValue = currentEntry.getValue();
Integer baselineValue = baselineMap.get(currentEntry.getKey());
diff(sb, currentEntry.getKey(), baselineValue, currentValue);
}
for (Entry<String, Integer> baselineEntry: baselineMap.entrySet()) {
Integer currentValue = currentMap.get(baselineEntry.getKey());
if (currentValue == null) {
diff(sb, baselineEntry.getKey(), baselineEntry.getValue(), currentValue);
}
}
return sb.toString();
}
private void diff(StringBuilder sb, String key, Integer baselineValue, Integer currentValue) {
if (baselineValue == null) {
baselineValue = 0;
}
if (currentValue == null) {
currentValue = 0;
}
if (baselineValue.equals(currentValue)) {
return;
}
sb.append(key).append(": ");
int diff = currentValue - baselineValue;
if (diff > 0) {
sb.append("+").append(diff);
} else {
sb.append(diff);
}
sb.append("\n");
}
@Override
public String debugDump() {
return debugDump(0);
}
@Override
public String debugDump(int indent) {
StringBuilder sb = new StringBuilder();
DebugUtil.indentDebugDump(sb, indent);
sb.append("Lsof(pid=").append(pid).append(")\n");
DebugUtil.debugDumpWithLabelLn(sb, "baselineTotalFds", baselineTotalFds, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "totalFds", totalFds, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "typeMap", typeMap, indent + 1);
DebugUtil.debugDumpWithLabelLn(sb, "miscMap", miscMap, indent + 1);
// Do not display output and nodemap, that is too much
return sb.toString();
}
}