aboutsummaryrefslogtreecommitdiffstats
path: root/tests/bugs1921
diff options
context:
space:
mode:
authorAlexander Kriegisch <Alexander@Kriegisch.name>2024-03-06 15:27:50 +0100
committerAlexander Kriegisch <Alexander@Kriegisch.name>2024-03-12 08:21:38 +0100
commit966397e3bfe0d38aeca28c6aec0f543d62a0e2ea (patch)
tree1e682471439a1f176868a211f2249341677724d8 /tests/bugs1921
parent512d3fc281d2e56227e487dbf8ccee28cb63e589 (diff)
downloadaspectj-966397e3bfe0d38aeca28c6aec0f543d62a0e2ea.tar.gz
aspectj-966397e3bfe0d38aeca28c6aec0f543d62a0e2ea.zip
Add regression tests for inheritable thread-local memory leak
Leak was introduced in commit 3c80a36527, fixing #128, but introducing #288 instead, which was the lesser of two evils, but still bad for some users unwilling to use native AspectJ syntax for their aspects, avoiding the problem. Relates to #288. Signed-off-by: Alexander Kriegisch <Alexander@Kriegisch.name>
Diffstat (limited to 'tests/bugs1921')
-rw-r--r--tests/bugs1921/github_288/FirstAspect.aj12
-rw-r--r--tests/bugs1921/github_288/MemoryHog.java18
-rw-r--r--tests/bugs1921/github_288/NestedAroundClosureMemoryLeakTest.java104
-rw-r--r--tests/bugs1921/github_288/SecondAspect.aj12
4 files changed, 146 insertions, 0 deletions
diff --git a/tests/bugs1921/github_288/FirstAspect.aj b/tests/bugs1921/github_288/FirstAspect.aj
new file mode 100644
index 000000000..63ef73ff9
--- /dev/null
+++ b/tests/bugs1921/github_288/FirstAspect.aj
@@ -0,0 +1,12 @@
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+@Aspect
+public class FirstAspect {
+ @Around("execution(* doSomething())")
+ public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+ System.out.println(getClass().getSimpleName());
+ return joinPoint.proceed();
+ }
+}
diff --git a/tests/bugs1921/github_288/MemoryHog.java b/tests/bugs1921/github_288/MemoryHog.java
new file mode 100644
index 000000000..f81e90a55
--- /dev/null
+++ b/tests/bugs1921/github_288/MemoryHog.java
@@ -0,0 +1,18 @@
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class MemoryHog {
+ final ExecutorService taskManager;
+ // Use 128 MB of data, then run with -Xmx1G for 10 threads or -Xmx512M for 5 threads
+ final byte[] someBigData = new byte[1024 * 1024 * 128];
+
+ public MemoryHog(final ExecutorService executorService) {
+ taskManager = executorService;
+ }
+
+ public void doSomething() throws ExecutionException, InterruptedException {
+ Future<?> future = taskManager.submit(() -> System.out.println("Executing task"));
+ future.get();
+ }
+}
diff --git a/tests/bugs1921/github_288/NestedAroundClosureMemoryLeakTest.java b/tests/bugs1921/github_288/NestedAroundClosureMemoryLeakTest.java
new file mode 100644
index 000000000..54a5eb1cc
--- /dev/null
+++ b/tests/bugs1921/github_288/NestedAroundClosureMemoryLeakTest.java
@@ -0,0 +1,104 @@
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class NestedAroundClosureMemoryLeakTest {
+
+ public static void main(String[] args) throws Exception {
+ if (args.length > 0 && "oom".equals(args[0]))
+ testNoMemoryLeak_SystemShouldNotRunOutOfMemory();
+ else
+ testNoMemoryLeak_InheritableThreadLocalCleared();
+ }
+
+ /**
+ * Tests that the inheritable thread-locals of the spawned threads are either null or contain all null elements
+ */
+ public static void testNoMemoryLeak_InheritableThreadLocalCleared() throws Exception {
+ int numThreadPools = 1;
+ List<ExecutorService> executorServices = createExecutorServicesWithFixedThreadPools(numThreadPools);
+ try {
+ executeTasksAndGC(executorServices);
+
+ Field mapField = Thread.class.getDeclaredField("inheritableThreadLocals");
+ mapField.setAccessible(true);
+ Set<Thread> threads = Thread.getAllStackTraces().keySet();
+
+ threads.stream()
+ .filter(thread -> thread.getName().contains("pool"))
+ .forEach(thread -> {
+ try {
+ Object inheritableThreadLocals = mapField.get(thread);
+ if (inheritableThreadLocals != null) {
+ Field tableField = inheritableThreadLocals.getClass().getDeclaredField("table");
+ tableField.setAccessible(true);
+ Object[] inheritableThreadLocalTable = (Object[]) tableField.get(inheritableThreadLocals);
+ if (inheritableThreadLocalTable != null) {
+ for (Object entry : inheritableThreadLocalTable)
+ assert entry == null : "All inheritable thread-locals should be null after GC";
+ }
+ }
+ }
+ catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ System.out.println("Test passed - all inheritable thread-locals are null after GC");
+ }
+ finally {
+ for (ExecutorService executorService : executorServices)
+ executorService.shutdown();
+ }
+ }
+
+ /**
+ * Executes tasks in multiple threads, using one executor service with a fixed thread pool of size 1 per task. This
+ * ensures that each spawned thread gets initialised for the first time and allocates memory for inheritable
+ * thread-locals, exposing possible memory leaks when running @AspectJ aspects with non-inlined, nested around advices
+ * in multi-thread situations.
+ * <p>
+ * If each thread allocates memory for a stack of around closures (memory leak case) according to
+ * <a href="https://github.com/eclipse-aspectj/aspectj/issues/288">GitHub issue 288</a>, the program will run out of
+ * memory. When fixed, this should no longer happen.
+ * <p>
+ * Run this test e.g. with {@code -Xmx1G} for 10 x 128 MB memory consumption to ensure an out of memory error in the
+ * leak case. Any other appropriate combination of number of threads and memory limit is also OK.
+ */
+ public static void testNoMemoryLeak_SystemShouldNotRunOutOfMemory() throws Exception {
+ int numThreadPools = 5;
+ List<ExecutorService> executorServices = createExecutorServicesWithFixedThreadPools(numThreadPools);
+ try {
+ executeTasksAndGC(executorServices);
+ System.out.println("Test passed - no OutOfMemoryError due to inheritable thread-locals memory leak");
+ }
+ finally {
+ for (ExecutorService executorService : executorServices)
+ executorService.shutdown();
+ }
+ }
+
+ private static List<ExecutorService> createExecutorServicesWithFixedThreadPools(int count) {
+ List<ExecutorService> executorServiceList = new ArrayList<>(count);
+ for (int i = 0; i < count; i++)
+ executorServiceList.add(Executors.newFixedThreadPool(1));
+ return executorServiceList;
+ }
+
+ private static void executeTasksAndGC(List<ExecutorService> executorServices) throws Exception {
+ for (ExecutorService executorService : executorServices)
+ new MemoryHog(executorService).doSomething();
+ System.out.println("Finished executing tasks");
+
+ // Best effort GC
+ System.gc();
+ System.out.println("Finished executing GC");
+
+ // Sleep to take a memory dump
+ // Thread.sleep(500000);
+ }
+
+}
diff --git a/tests/bugs1921/github_288/SecondAspect.aj b/tests/bugs1921/github_288/SecondAspect.aj
new file mode 100644
index 000000000..ef405e4ee
--- /dev/null
+++ b/tests/bugs1921/github_288/SecondAspect.aj
@@ -0,0 +1,12 @@
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+
+@Aspect
+public class SecondAspect {
+ @Around("execution(* doSomething())")
+ public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
+ System.out.println(getClass().getSimpleName());
+ return joinPoint.proceed();
+ }
+}