Skip to content

Commit e115560

Browse files
committed
Resource leakage WIP
1 parent 713be6c commit e115560

10 files changed

+452
-5
lines changed

spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/SmartDirtiesContextTestExecutionListener.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static com.github.seregamorph.testsmartcontext.SmartDirtiesTestsSupport.isInnerClass;
44

5+
import com.github.seregamorph.testsmartcontext.leakage.ResourceLeakageManager;
56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
78
import org.springframework.test.context.TestContext;
@@ -28,28 +29,38 @@ public class SmartDirtiesContextTestExecutionListener extends AbstractTestExecut
2829

2930
@Override
3031
public int getOrder() {
31-
// DirtiesContextTestExecutionListener.getOrder() + 1
32+
// DirtiesContextTestExecutionListener.getOrder() + 10
3233
//noinspection MagicNumber
33-
return 3001;
34+
return 3010;
3435
}
3536

3637
@Override
3738
public void beforeTestClass(TestContext testContext) {
39+
Class<?> testClass = testContext.getTestClass();
3840
// stack Nested classes
3941
CurrentTestContext.pushCurrentTestClass(testContext.getTestClass());
40-
Class<?> testClass = testContext.getTestClass();
4142
if (isInnerClass(testClass)) {
4243
SmartDirtiesTestsSupport.verifyInnerClass(testClass);
4344
}
45+
46+
ResourceLeakageManager leakageManager = ResourceLeakageManager.getInstance();
47+
if (SmartDirtiesTestsSupport.isFirstClassPerConfig(testClass)) {
48+
logger.info("firstClassPerConfig {}", testClass.getName());
49+
leakageManager.handleBeforeClassGroup();
50+
}
51+
leakageManager.handleBeforeClass(testClass);
4452
}
4553

4654
@Override
4755
public void afterTestClass(TestContext testContext) {
4856
try {
4957
Class<?> testClass = testContext.getTestClass();
58+
ResourceLeakageManager leakageManager = ResourceLeakageManager.getInstance();
59+
leakageManager.handleAfterClass(testClass);
5060
if (SmartDirtiesTestsSupport.isLastClassPerConfig(testClass)) {
5161
logger.info("markDirty (closing context) after {}", testClass.getName());
5262
testContext.markApplicationContextDirty(null);
63+
leakageManager.handleAfterClassGroup(testClass);
5364
} else {
5465
logger.debug("Reusing context after {}", testClass.getName());
5566
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.github.seregamorph.testsmartcontext.jdbc;
2+
3+
import java.io.Closeable;
4+
import java.io.IOException;
5+
import javax.sql.DataSource;
6+
import org.springframework.jdbc.datasource.DelegatingDataSource;
7+
8+
public class CloseableDelegatingDataSource extends DelegatingDataSource implements Closeable {
9+
10+
public CloseableDelegatingDataSource() {
11+
// for lazy initialization
12+
}
13+
14+
public CloseableDelegatingDataSource(DataSource targetDataSource) {
15+
// eagerly initialized DataSource should be closeable
16+
super(requireCloseable(targetDataSource));
17+
}
18+
19+
private static DataSource requireCloseable(DataSource targetDataSource) {
20+
if (targetDataSource == null) {
21+
throw new IllegalArgumentException("targetDataSource is null");
22+
}
23+
if (!(targetDataSource instanceof Closeable)) {
24+
throw new IllegalArgumentException("targetDataSource is not closeable");
25+
}
26+
return targetDataSource;
27+
}
28+
29+
@Override
30+
public void close() throws IOException {
31+
DataSource targetDataSource = getTargetDataSource();
32+
// condition may be false for lazily initialized DataSource
33+
if (targetDataSource instanceof Closeable) {
34+
((Closeable) targetDataSource).close();
35+
}
36+
}
37+
}

spring-test-smart-context/src/main/java/com/github/seregamorph/testsmartcontext/jdbc/LateInitDataSource.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.util.function.Supplier;
44
import javax.sql.DataSource;
5-
import org.springframework.jdbc.datasource.DelegatingDataSource;
65
import org.springframework.lang.Nullable;
76

87
/**
@@ -27,7 +26,7 @@
2726
*
2827
* @author Sergey Chernov
2928
*/
30-
public class LateInitDataSource extends DelegatingDataSource {
29+
public class LateInitDataSource extends CloseableDelegatingDataSource {
3130

3231
@Nullable
3332
private final String name;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.lang.management.ManagementFactory;
4+
import java.lang.management.MemoryMXBean;
5+
import java.util.Arrays;
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
public class HeapResourceLeakageDetector extends ResourceLeakageDetector {
10+
11+
private final MemoryMXBean memoryMXBean;
12+
13+
public HeapResourceLeakageDetector() {
14+
super(Arrays.asList("committed", "used"));
15+
this.memoryMXBean = ManagementFactory.getMemoryMXBean();
16+
}
17+
18+
@Override
19+
public Map<String, Long> getIndicators() {
20+
Map<String, Long> map = new HashMap<>();
21+
map.put("committed", memoryMXBean.getHeapMemoryUsage().getCommitted());
22+
map.put("used", memoryMXBean.getHeapMemoryUsage().getUsed());
23+
// map.put("init", memoryMXBean.getHeapMemoryUsage().getInit());
24+
// map.put("max", memoryMXBean.getHeapMemoryUsage().getMax());
25+
return map;
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import static java.nio.charset.StandardCharsets.UTF_8;
4+
5+
import java.io.File;
6+
import java.io.FileNotFoundException;
7+
import java.io.FileOutputStream;
8+
import java.io.OutputStreamWriter;
9+
import java.io.PrintWriter;
10+
import java.io.UncheckedIOException;
11+
import java.util.ArrayList;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
public class ResourceLeakageCsvLogWriter extends ResourceLeakageLogWriter {
16+
17+
private final PrintWriter out;
18+
private final List<String> headers;
19+
20+
public ResourceLeakageCsvLogWriter(File outputFile, List<String> headers) {
21+
try {
22+
FileOutputStream fileOutputStream = new FileOutputStream(outputFile, false);
23+
out = new PrintWriter(new OutputStreamWriter(fileOutputStream, UTF_8), true);
24+
} catch (FileNotFoundException e) {
25+
throw new UncheckedIOException(e);
26+
}
27+
28+
this.headers = new ArrayList<>(headers);
29+
StringBuilder fileHeader = new StringBuilder("Timestamp,testClass,event,testGroup,test");
30+
for (String header : headers) {
31+
fileHeader.append(",").append(header);
32+
}
33+
out.println(fileHeader);
34+
}
35+
36+
@Override
37+
public void write(
38+
Map<String, Long> indicators,
39+
Class<?> testClass,
40+
String event,
41+
int testGroupNumber,
42+
int testNumber
43+
) {
44+
String timestamp = getTimestamp();
45+
StringBuilder line = new StringBuilder(timestamp)
46+
.append(",").append(testClass.getSimpleName())
47+
.append(",").append(event)
48+
.append(",").append(testGroupNumber)
49+
.append(",").append(testNumber);
50+
for (String header : headers) {
51+
Long value = indicators.get(header);
52+
if (value == null) {
53+
line.append(",");
54+
} else {
55+
line.append(",").append(value);
56+
}
57+
}
58+
out.println(line);
59+
}
60+
61+
@Override
62+
public void close() {
63+
out.close();
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
/*
9+
threads
10+
docker containers
11+
heap memory
12+
opened files
13+
opened sockets
14+
loaded classes
15+
CPU history
16+
*/
17+
public abstract class ResourceLeakageDetector {
18+
19+
private final List<String> indicatorKeys;
20+
21+
List<Class<?>> testClasses;
22+
23+
protected ResourceLeakageDetector(List<String> indicatorKeys) {
24+
this.indicatorKeys = Collections.unmodifiableList(new ArrayList<>(indicatorKeys));
25+
}
26+
27+
public final List<String> getIndicatorKeys() {
28+
return indicatorKeys;
29+
}
30+
31+
public abstract Map<String, Long> getIndicators();
32+
33+
public void handleBeforeClassGroup() {
34+
this.testClasses = new ArrayList<>();
35+
}
36+
37+
public void handleAfterClass(Class<?> testClass) {
38+
// testClasses can be null in case if a single test is executed
39+
if (this.testClasses != null) {
40+
this.testClasses.add(testClass);
41+
}
42+
}
43+
44+
public void handleAfterClassGroup() {
45+
this.testClasses = null;
46+
}
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.io.Closeable;
4+
import java.util.Map;
5+
import java.util.concurrent.TimeUnit;
6+
7+
public abstract class ResourceLeakageLogWriter implements Closeable {
8+
9+
private final long startNanoTime = System.nanoTime();
10+
11+
public abstract void write(
12+
Map<String, Long> indicators,
13+
Class<?> testClass,
14+
String event,
15+
int testGroupNumber,
16+
int testNumber
17+
);
18+
19+
protected String getTimestamp() {
20+
long now = System.nanoTime();
21+
long totalSeconds = TimeUnit.NANOSECONDS.toSeconds(now - startNanoTime);
22+
23+
return formatTimestamp(totalSeconds);
24+
}
25+
26+
static String formatTimestamp(long totalSeconds) {
27+
long seconds = totalSeconds % 60;
28+
long minutes = (totalSeconds = totalSeconds / 60) % 60;
29+
long hours = totalSeconds / 60;
30+
return hours + ":" + String.format("%02d", minutes) + ":" + String.format("%02d", seconds);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.io.File;
4+
import java.util.Arrays;
5+
import java.util.HashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
import java.util.concurrent.atomic.AtomicInteger;
9+
import java.util.stream.Collectors;
10+
import org.springframework.lang.Nullable;
11+
12+
public class ResourceLeakageManager {
13+
14+
private final AtomicInteger testGroupNumber = new AtomicInteger();
15+
private final AtomicInteger testNumber = new AtomicInteger();
16+
17+
private final List<ResourceLeakageDetector> resourceLeakageDetectors;
18+
@Nullable
19+
private final ResourceLeakageLogWriter resourceLeakageLogWriter;
20+
21+
private static final ResourceLeakageManager instance = initInstance();
22+
23+
private static ResourceLeakageManager initInstance() {
24+
return new ResourceLeakageManager();
25+
}
26+
27+
private ResourceLeakageManager() {
28+
// todo service discovery with priority
29+
this(Arrays.asList(
30+
new ThreadsResourceLeakageDetector(),
31+
new HeapResourceLeakageDetector()
32+
));
33+
}
34+
35+
private ResourceLeakageManager(List<ResourceLeakageDetector> resourceLeakageDetectors) {
36+
/*@Nullable*/
37+
File reportsBaseDir = ResourceLeakageUtils.getReportsBaseDir();
38+
39+
this.resourceLeakageDetectors = resourceLeakageDetectors;
40+
if (reportsBaseDir == null) {
41+
this.resourceLeakageLogWriter = null;
42+
} else {
43+
File outputFile = new File(reportsBaseDir, "report.csv");
44+
List<String> headers = resourceLeakageDetectors.stream()
45+
.flatMap(detector -> detector.getIndicatorKeys().stream())
46+
.collect(Collectors.toList());
47+
resourceLeakageLogWriter = new ResourceLeakageCsvLogWriter(outputFile, headers);
48+
}
49+
}
50+
51+
public static ResourceLeakageManager getInstance() {
52+
return instance;
53+
}
54+
55+
public void handleBeforeClassGroup() {
56+
testGroupNumber.incrementAndGet();
57+
resourceLeakageDetectors.forEach(ResourceLeakageDetector::handleBeforeClassGroup);
58+
}
59+
60+
public void handleBeforeClass(Class<?> testClass) {
61+
testNumber.incrementAndGet();
62+
logIndicators(testClass, "BC");
63+
}
64+
65+
public void handleAfterClass(Class<?> testClass) {
66+
logIndicators(testClass, "AC");
67+
for (ResourceLeakageDetector resourceLeakageDetector : resourceLeakageDetectors) {
68+
resourceLeakageDetector.handleAfterClass(testClass);
69+
}
70+
}
71+
72+
public void handleAfterClassGroup(Class<?> testClass) {
73+
if (Boolean.getBoolean("testsmartcontext.handleAfterClassGroup.gc")) {
74+
System.gc();
75+
}
76+
logIndicators(testClass, "ACG");
77+
for (ResourceLeakageDetector resourceLeakageDetector : resourceLeakageDetectors) {
78+
resourceLeakageDetector.handleAfterClassGroup();
79+
}
80+
}
81+
82+
private void logIndicators(Class<?> testClass, String event) {
83+
if (resourceLeakageLogWriter != null) {
84+
Map<String, Long> indicators = new HashMap<>();
85+
resourceLeakageDetectors.forEach(detector -> indicators.putAll(detector.getIndicators()));
86+
resourceLeakageLogWriter.write(indicators, testClass, event,
87+
testGroupNumber.get(), testNumber.get());
88+
}
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.github.seregamorph.testsmartcontext.leakage;
2+
3+
import java.io.File;
4+
import org.apache.commons.logging.Log;
5+
import org.apache.commons.logging.LogFactory;
6+
import org.springframework.lang.Nullable;
7+
8+
final class ResourceLeakageUtils {
9+
10+
private static final Log log = LogFactory.getLog(ResourceLeakageUtils.class);
11+
12+
@Nullable
13+
static File getReportsBaseDir() {
14+
// todo target
15+
// "basedir" is provided by Maven
16+
String basedirProperty = System.getProperty("basedir");
17+
if (basedirProperty == null) {
18+
return null;
19+
}
20+
21+
File basedir = new File(basedirProperty, "leakage-detector");
22+
if ((basedir.mkdir() || basedir.exists()) && basedir.isDirectory()) {
23+
return basedir;
24+
}
25+
log.warn("Failed to create " + basedir);
26+
return null;
27+
}
28+
}

0 commit comments

Comments
 (0)