This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch 1.X
in repository https://gitbox.apache.org/repos/asf/commons-beanutils.git


The following commit(s) were added to refs/heads/1.X by this push:
     new 9541e18d Make WrapDynaClassTest tolerate gc eviction of weak cache 
entries (1.X) (#425)
9541e18d is described below

commit 9541e18dfc99e1a61578dcf3719649e47ef5faaa
Author: Naveed Khan <[email protected]>
AuthorDate: Tue Jul 21 17:38:24 2026 +0000

    Make WrapDynaClassTest tolerate gc eviction of weak cache entries (1.X) 
(#425)
    
    The per-classloader cache holds its CacheKey weakly and nothing else
    references it, so any GC that runs between two createDynaClass calls can
    evict the entry and the next caller legitimately builds a second
    instance. testConcurrentCreateDynaClassReturnsSameInstance asserted
    exactly one instance per round regardless, so a GC landing inside a
    round failed it on CI even though the synchronized get/create/put from
    PR 419 is correct. Reproducible without any concurrency: create, call
    System.gc(), create again, and the second call returns a new instance.
    
    Guard each round with a WeakReference canary: only assert the
    single-instance property when the canary shows no GC ran during the
    round, and skip rounds a GC invalidated. Apply the same guard to
    testCreateDynaClassIsCached, which had the same sensitivity in a smaller
    window. The test still fails on the unsynchronized pre-419 code and now
    passes under a System.gc() hammer thread that previously made it fail
    immediately with the same error as the CI run.
    
    Signed-off-by: Naveed Khan <[email protected]>
---
 .../commons/beanutils/WrapDynaClassTest.java       | 24 +++++++++++++++++++---
 1 file changed, 21 insertions(+), 3 deletions(-)

diff --git a/src/test/java/org/apache/commons/beanutils/WrapDynaClassTest.java 
b/src/test/java/org/apache/commons/beanutils/WrapDynaClassTest.java
index 623d4dbe..5b7cca8c 100644
--- a/src/test/java/org/apache/commons/beanutils/WrapDynaClassTest.java
+++ b/src/test/java/org/apache/commons/beanutils/WrapDynaClassTest.java
@@ -20,6 +20,7 @@ package org.apache.commons.beanutils;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
 
+import java.lang.ref.WeakReference;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Set;
@@ -56,6 +57,10 @@ class WrapDynaClassTest {
      * The cache key is one bean class, so {@code createDynaClass} must hand 
back a single instance no matter how many threads race to populate the
      * per-classloader cache. With a plain {@code WeakHashMap} and an 
unsynchronized get/create/put sequence, concurrent callers could build and 
return
      * distinct instances for one key; this drives that race and fails if more 
than one instance escapes.
+     * <p>
+     * The cache holds its keys weakly and nothing else references them, so a 
GC that runs while a round is in flight may evict the freshly cached entry
+     * and a late thread then builds a second instance without any race. Each 
round therefore only asserts when a GC canary shows no collection happened
+     * during the round; rounds invalidated by a GC are skipped.
      */
     @Test
     void testConcurrentCreateDynaClassReturnsSameInstance() throws Exception {
@@ -65,6 +70,8 @@ class WrapDynaClassTest {
         try {
             for (int r = 0; r < rounds; r++) {
                 WrapDynaClass.clear();
+                // Cleared only if a GC ran after this point, which is the 
only way a cache entry can disappear mid-round.
+                final WeakReference<Object> gcCanary = new WeakReference<>(new 
Object());
                 final CyclicBarrier barrier = new CyclicBarrier(threads);
                 final Set<WrapDynaClass> results = 
Collections.newSetFromMap(new IdentityHashMap<>());
                 final Future<?>[] futures = new Future<?>[threads];
@@ -81,6 +88,9 @@ class WrapDynaClassTest {
                 for (final Future<?> f : futures) {
                     f.get();
                 }
+                if (results.size() > 1 && gcCanary.get() == null) {
+                    continue;
+                }
                 assertEquals(1, results.size(), "createDynaClass returned more 
than one instance for one key");
             }
         } finally {
@@ -90,13 +100,21 @@ class WrapDynaClassTest {
     }
 
     /**
-     * The single-threaded cache contract: repeated calls for one bean class 
return the same instance until the cache is cleared.
+     * The single-threaded cache contract: repeated calls for one bean class 
return the same instance until the cache is cleared. A GC between the two
+     * calls may legitimately evict the weakly held cache entry, so such 
attempts are retried.
      */
     @Test
     void testCreateDynaClassIsCached() {
         WrapDynaClass.clear();
-        final WrapDynaClass first = 
WrapDynaClass.createDynaClass(ConcurrentBean.class);
-        assertSame(first, WrapDynaClass.createDynaClass(ConcurrentBean.class));
+        WeakReference<Object> gcCanary;
+        WrapDynaClass first;
+        WrapDynaClass second;
+        do {
+            gcCanary = new WeakReference<>(new Object());
+            first = WrapDynaClass.createDynaClass(ConcurrentBean.class);
+            second = WrapDynaClass.createDynaClass(ConcurrentBean.class);
+        } while (first != second && gcCanary.get() == null);
+        assertSame(first, second);
         WrapDynaClass.clear();
     }
 }

Reply via email to