diff --git a/orbmain/src/main/java/com/sun/corba/ee/impl/io/ObjectStreamClass.java b/orbmain/src/main/java/com/sun/corba/ee/impl/io/ObjectStreamClass.java index 748b8eb0ec..d9fb3b798d 100644 --- a/orbmain/src/main/java/com/sun/corba/ee/impl/io/ObjectStreamClass.java +++ b/orbmain/src/main/java/com/sun/corba/ee/impl/io/ObjectStreamClass.java @@ -22,6 +22,7 @@ package com.sun.corba.ee.impl.io; import com.sun.corba.ee.impl.misc.ClassInfoCache; +import com.sun.corba.ee.impl.misc.ConcurrentSoftCache; import com.sun.corba.ee.impl.util.RepositoryId; import com.sun.corba.ee.spi.trace.TraceValueHandler; @@ -46,8 +47,8 @@ import java.util.Comparator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ConcurrentMap; -import org.glassfish.pfl.basic.concurrent.SoftCache; import org.glassfish.pfl.basic.reflection.Bridge; import org.omg.CORBA.ValueMember; @@ -99,13 +100,39 @@ static final ObjectStreamClass lookup(Class cl) { */ static ObjectStreamClass lookupInternal(Class cl) { + /* A descriptor that is present and fully initialized can be returned + * without any lock at all, and in a running system that is very nearly + * every lookup: descriptors are created once per class and then read + * for the life of the process. + * + * The lock below is a single process wide monitor which every + * marshalled and unmarshalled object had to pass through, so removing + * it from the steady state path is the point of this fast path. It + * also has to be a fast path rather than a rewrite of what follows: + * the comment on init() records that moving initialization out of this + * monitor was tried and reverted because it deadlocks (bug 5104239), + * so the slow path is deliberately left exactly as it was. + * + * Correctness rests on two things. 'initialized' is volatile and is + * assigned last in init(), so seeing it true means every field written + * during initialization is visible. And a descriptor is published into + * the cache by the constructor, before init() runs, precisely so that + * recursive lookups find it - which is why it is not enough for the + * entry to exist, it must also report itself initialized. + */ + ObjectStreamClass cached = descriptorFor.get(cl); + if (cached != null && cached.initialized) { + return cached; + } + /* Synchronize on the hashtable so no two threads will do * this at the same time. */ ObjectStreamClass desc = null; synchronized (descriptorFor) { + descriptorFor.purge(); /* Find the matching descriptor if it already known */ - desc = (ObjectStreamClass)descriptorFor.get( cl ) ; + desc = descriptorFor.get( cl ) ; if (desc == null) { /* Check if it's serializable */ ClassInfoCache.ClassInfo cinfo = ClassInfoCache.get( cl ) ; @@ -641,7 +668,15 @@ public final String getRMIIIOPOptionalDataRepId() { superclass = null; } - public static final synchronized ObjectStreamField[] translateFields( + /** + * @param fields the fields to translate + * @return the translated fields + */ + // Not synchronized: PersistentFieldsValue.translateFields allocates a new + // array and reads only its argument, so the monitor this used to take - + // on the ObjectStreamClass class object, shared with every other static + // synchronized member - guarded nothing. + public static final ObjectStreamField[] translateFields( java.io.ObjectStreamField fields[]) { return PersistentFieldsValue.translateFields(fields); } @@ -1194,11 +1229,18 @@ static String getSignature(Constructor cons) { return sb.toString(); } - /* - * Cache of Class -> ClassDescriptor Mappings. + /** + * Cache of Class to ObjectStreamClass mappings. + * + *

This used to be {@code org.glassfish.pfl.basic.concurrent.SoftCache}, + * a bare HashMap that was safe only because {@link #lookupInternal} took a + * process wide monitor around every access. Reads have to be lock free for + * that method's fast path to exist. The values stay soft: an + * ObjectStreamClass holds its Class, so a strong map would form a key to + * value to key cycle pinning the application class loader. */ - static private final SoftCache,ObjectStreamClass> descriptorFor = - new SoftCache,ObjectStreamClass>() ; + private static final ConcurrentSoftCache, ObjectStreamClass> descriptorFor = + new ConcurrentSoftCache<>(); /* * The name of this descriptor @@ -1258,7 +1300,10 @@ static String getSignature(Constructor cons) { * try to fix bug 4373844. Working to move to * reusing java.io.ObjectStreamClass for JDK 1.5. */ - private boolean initialized = false; + // Read without holding any lock by lookupInternal's fast path, and + // assigned last by init(), so it doubles as the publication fence for + // every other field this descriptor computes. + private volatile boolean initialized = false; /* Internal lock object. */ private final Object lock = new Object(); diff --git a/orbmain/src/main/java/com/sun/corba/ee/impl/misc/ConcurrentSoftCache.java b/orbmain/src/main/java/com/sun/corba/ee/impl/misc/ConcurrentSoftCache.java new file mode 100644 index 0000000000..ef0dad46f0 --- /dev/null +++ b/orbmain/src/main/java/com/sun/corba/ee/impl/misc/ConcurrentSoftCache.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License v2.0 + * w/Classpath exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause OR GPL-2.0 WITH + * Classpath-exception-2.0 + */ + +package com.sun.corba.ee.impl.misc; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * A concurrent cache whose values are held softly. + * + *

It exists to replace two uses of + * {@code org.glassfish.pfl.basic.concurrent.SoftCache}, which despite its + * package name is a bare {@code HashMap} with no synchronization of its own. + * One of those uses was safe only because every caller happened to hold an + * external monitor; the other was read with no lock at all while other + * threads wrote it, which is a data race on a plain HashMap - and since that + * class also mutates its map inside {@code get}, even two concurrent readers + * were enough to corrupt it. + * + *

Values are soft rather than strong because both caches map to, or from, + * {@code Class} objects. A strong reference from a process wide static map to + * an application class keeps its class loader alive forever, which in an + * application server is a redeployment leak. + * + *

Reads take no lock. Cleared entries are evicted by {@link #purge()}, + * which callers are expected to invoke from whatever slow path they already + * have, rather than on every read. + * + * @param key type + * @param value type + */ +public final class ConcurrentSoftCache { + + private final ConcurrentMap> map = new ConcurrentHashMap<>(); + private final ReferenceQueue cleared = new ReferenceQueue<>(); + + /** A soft reference that remembers its key, so a cleared one can be evicted. */ + private static final class Entry extends SoftReference { + + private final K key; + + Entry(K key, V value, ReferenceQueue queue) { + super(value, queue); + this.key = key; + } + } + + /** + * @param key the key to look up + * @return the value, or null if absent or already collected + */ + public V get(K key) { + Entry entry = map.get(key); + return entry == null ? null : entry.get(); + } + + /** + * @param key the key to store under + * @param value the value to hold softly + */ + public void put(K key, V value) { + map.put(key, new Entry<>(key, value, cleared)); + } + + /** + * Drops the entries whose value has been collected. + */ + public void purge() { + for (Reference ref; (ref = cleared.poll()) != null; ) { + @SuppressWarnings("unchecked") + Entry entry = (Entry) ref; + // Two argument remove, so an entry that a later put reinstated + // under the same key is never evicted by this one's death. + map.remove(entry.key, entry); + } + } + + /** + * @return the number of entries, including any whose value has been + * collected but not yet purged + */ + public int size() { + return map.size(); + } +} diff --git a/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryId.java b/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryId.java index ad18c7f728..cd4f7d8f35 100644 --- a/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryId.java +++ b/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryId.java @@ -21,6 +21,8 @@ package com.sun.corba.ee.impl.util; +import com.sun.corba.ee.impl.misc.ConcurrentSoftCache; + import com.sun.corba.ee.impl.io.ObjectStreamClass; import com.sun.corba.ee.impl.javax.rmi.CORBA.Util; import com.sun.corba.ee.impl.misc.ClassInfoCache ; @@ -34,7 +36,6 @@ import java.util.Map; import java.util.WeakHashMap; -import org.glassfish.pfl.basic.concurrent.SoftCache; public class RepositoryId { @@ -77,7 +78,14 @@ public class RepositoryId { private static final Map, String> classSeqToRepStr = new WeakHashMap<>(); private static final Map repStrToByteArray = new IdentityHashMap<>(); - private static final Map> repStrToClass = new SoftCache<>(); + /* + * Read by getAnyClassFromType with no lock at all, while the writes below + * happen under the classToRepStr monitor. With the previous SoftCache - + * a bare HashMap that mutates itself even inside get() - that was a data + * race, and two concurrent readers were enough to corrupt it. + */ + private static final ConcurrentSoftCache> repStrToClass = + new ConcurrentSoftCache<>(); private String repId = null; private boolean isSupportedFormat = true; @@ -779,6 +787,7 @@ public static String createForJavaType(java.io.Serializable ser) createHashString(clazz); classToRepStr.put(clazz, repid); + repStrToClass.purge(); repStrToClass.put(repid, clazz); return repid; } @@ -815,6 +824,7 @@ public static String createForJavaType(Class clz, ClassInfoCache.ClassInfo ci createHashString(clz); classToRepStr.put(clz, repid); + repStrToClass.purge(); repStrToClass.put(repid, clz); return repid; } diff --git a/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryIdCache.java b/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryIdCache.java index a2a7bebc9d..eb3c5756f7 100644 --- a/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryIdCache.java +++ b/orbmain/src/main/java/com/sun/corba/ee/impl/util/RepositoryIdCache.java @@ -20,18 +20,35 @@ package com.sun.corba.ee.impl.util; -import java.util.Hashtable; +import java.util.concurrent.ConcurrentHashMap; -public class RepositoryIdCache extends Hashtable { - public final synchronized RepositoryId getId(String key) { - RepositoryId repId = super.get(key); +/** + * Interns {@link RepositoryId} instances by their string form. + * + *

This was a {@code Hashtable} with a {@code synchronized getId} on top, + * held in a static field on {@link RepositoryId}, so every value type + * marshalled anywhere in the process took the same monitor twice - once for + * the method and once inside the Hashtable itself. The map is now concurrent + * and the hit path takes no lock. + */ +public class RepositoryIdCache extends ConcurrentHashMap { + private static final long serialVersionUID = 1L; + + /** + * @param key the repository id string + * @return the interned RepositoryId for that string, creating one if this + * is the first time it has been seen + */ + public final RepositoryId getId(String key) { + // Look before computing: computeIfAbsent locks a bin even when the key + // is present, and present is what almost every call finds. + RepositoryId repId = get(key); if (repId != null) { return repId; - } else { - repId = new RepositoryId(key); - put(key, repId); - return repId; } + // RepositoryId's constructor only parses the string it is given, so it + // cannot re-enter this map - which computeIfAbsent would not tolerate. + return computeIfAbsent(key, RepositoryId::new); } } diff --git a/orbmain/src/test/java/com/sun/corba/ee/impl/io/DescriptorLookupContentionTest.java b/orbmain/src/test/java/com/sun/corba/ee/impl/io/DescriptorLookupContentionTest.java new file mode 100644 index 0000000000..942130de5d --- /dev/null +++ b/orbmain/src/test/java/com/sun/corba/ee/impl/io/DescriptorLookupContentionTest.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License v2.0 + * w/Classpath exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause OR GPL-2.0 WITH + * Classpath-exception-2.0 + */ + +package com.sun.corba.ee.impl.io; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import com.sun.corba.ee.impl.util.RepositoryId; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Test; + +/** + * Asserts that looking up a class descriptor no longer serializes every + * thread in the process behind one monitor. + * + *

Throughput would be the obvious way to show this and the wrong way to + * assert it: a timing threshold is a flaky test on a shared build machine. + * What is checked instead is the property itself - that the fast path does + * not need the lock - by holding that lock and watching a lookup succeed + * anyway. Before the change the same test blocks until the timeout. + */ +public class DescriptorLookupContentionTest { + + private static final long TIMEOUT_SECONDS = 10; + + /** A class of our own, so no other test can have warmed it up first. */ + private static final class Marshalled implements Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + private int field; + } + + private static Object descriptorCache() throws Exception { + Field field = ObjectStreamClass.class.getDeclaredField("descriptorFor"); + field.setAccessible(true); + return field.get(null); + } + + @Test + public void aLookupOfAnInitializedDescriptorDoesNotNeedTheGlobalLock() throws Exception { + // Warm up: after this the descriptor is cached and initialized, which + // is the state every class reaches within moments of a server starting. + ObjectStreamClass warmed = ObjectStreamClass.lookup(Marshalled.class); + assertNotNull(warmed); + + final Object cache = descriptorCache(); + final CountDownLatch lockHeld = new CountDownLatch(1); + final CountDownLatch lookupDone = new CountDownLatch(1); + final AtomicReference found = new AtomicReference<>(); + + Thread reader = new Thread(new Runnable() { + @Override + public void run() { + try { + lockHeld.await(TIMEOUT_SECONDS, TimeUnit.SECONDS); + found.set(ObjectStreamClass.lookup(Marshalled.class)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + lookupDone.countDown(); + } + } + }, "descriptor-lookup"); + reader.setDaemon(true); + reader.start(); + + synchronized (cache) { + lockHeld.countDown(); + assertTrue("a lookup of an already initialized descriptor blocked on the" + + " cache monitor; the lock free fast path is not being taken", + lookupDone.await(TIMEOUT_SECONDS, TimeUnit.SECONDS)); + } + + reader.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS)); + assertSame("the fast path must return the same descriptor as the slow path", + warmed, found.get()); + } + + @Test + public void concurrentLookupsAgreeOnOneDescriptorPerClass() throws Exception { + final Class[] classes = { + Marshalled.class, String.class, Integer.class, java.util.ArrayList.class, + java.util.HashMap.class, java.math.BigDecimal.class, java.util.Date.class, + }; + + int threads = Math.max(4, Runtime.getRuntime().availableProcessors()); + ExecutorService pool = Executors.newFixedThreadPool(threads); + try { + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + tasks.add(new Callable() { + @Override + public ObjectStreamClass[] call() { + ObjectStreamClass[] seen = new ObjectStreamClass[classes.length]; + for (int round = 0; round < 200; round++) { + for (int i = 0; i < classes.length; i++) { + seen[i] = ObjectStreamClass.lookup(classes[i]); + } + } + return seen; + } + }); + } + + ObjectStreamClass[] reference = null; + for (Future result : pool.invokeAll(tasks, 60, TimeUnit.SECONDS)) { + ObjectStreamClass[] seen = result.get(); + if (reference == null) { + reference = seen; + } else { + for (int i = 0; i < seen.length; i++) { + assertSame("two threads got different descriptors for " + classes[i], + reference[i], seen[i]); + } + } + } + } finally { + pool.shutdownNow(); + } + } + + @Test + public void concurrentRepositoryIdLookupsIntern() throws Exception { + final String[] ids = new String[64]; + for (int i = 0; i < ids.length; i++) { + ids[i] = "RMI:com.acme.Type" + i + ":0123456789ABCDEF"; + } + + int threads = Math.max(4, Runtime.getRuntime().availableProcessors()); + ExecutorService pool = Executors.newFixedThreadPool(threads); + try { + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + tasks.add(new Callable() { + @Override + public RepositoryId[] call() { + RepositoryId[] seen = new RepositoryId[ids.length]; + for (int round = 0; round < 200; round++) { + for (int i = 0; i < ids.length; i++) { + seen[i] = RepositoryId.cache.getId(ids[i]); + } + } + return seen; + } + }); + } + + RepositoryId[] reference = null; + for (Future result : pool.invokeAll(tasks, 60, TimeUnit.SECONDS)) { + RepositoryId[] seen = result.get(); + assertEquals(ids.length, seen.length); + if (reference == null) { + reference = seen; + } else { + for (int i = 0; i < seen.length; i++) { + assertSame("the cache handed out two instances for " + ids[i], + reference[i], seen[i]); + } + } + } + } finally { + pool.shutdownNow(); + } + } +} diff --git a/orbmain/src/test/java/com/sun/corba/ee/impl/io/DescriptorLookupThroughput.java b/orbmain/src/test/java/com/sun/corba/ee/impl/io/DescriptorLookupThroughput.java new file mode 100644 index 0000000000..3dea432bb5 --- /dev/null +++ b/orbmain/src/test/java/com/sun/corba/ee/impl/io/DescriptorLookupThroughput.java @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License v2.0 + * w/Classpath exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause OR GPL-2.0 WITH + * Classpath-exception-2.0 + */ + +package com.sun.corba.ee.impl.io; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * Measures how descriptor lookup scales with thread count. + * + *

Deliberately a {@code main} and not a test: the number it reports is the + * point, and a number is not something to assert on a shared build machine. + * The correctness properties are asserted in + * {@link DescriptorLookupContentionTest}; this is here so that a claim about + * throughput in a review can be reproduced rather than believed. + * + *

+ * mvn -pl orbmain test-compile
+ * java -cp orbmain/target/classes:orbmain/target/test-classes:$(deps) \
+ *      com.sun.corba.ee.impl.io.DescriptorLookupThroughput
+ * 
+ * + *

What to look for is not the absolute rate but the shape: with a global + * monitor on the lookup path the total rate is flat as threads are added, + * because the threads are queueing. Without it the rate should climb roughly + * with core count until memory bandwidth or the allocator becomes the limit. + */ +public final class DescriptorLookupThroughput { + + /** Iterations per thread, each doing one pass over CLASSES. */ + private static final int ROUNDS = 2_000_000; + private static final int WARMUP_ROUNDS = 200_000; + + private DescriptorLookupThroughput() { + } + + private static final class Sample implements Serializable { + + private static final long serialVersionUID = 1L; + + @SuppressWarnings("unused") + private int a; + @SuppressWarnings("unused") + private String b; + } + + private static final Class[] CLASSES = { + Sample.class, String.class, Integer.class, Long.class, + java.util.ArrayList.class, java.util.HashMap.class, + java.math.BigDecimal.class, java.util.Date.class, + }; + + public static void main(String[] args) throws Exception { + for (Class cl : CLASSES) { + ObjectStreamClass.lookup(cl); + } + run(1, WARMUP_ROUNDS); + run(2, WARMUP_ROUNDS); + + int cores = Runtime.getRuntime().availableProcessors(); + System.out.println("cores: " + cores); + System.out.printf("%8s %16s %12s%n", "threads", "lookups/sec", "vs 1 thread"); + + double single = 0; + for (int threads = 1; threads <= cores * 2; threads *= 2) { + // Best of three: the interesting quantity is the ceiling, and a + // shared machine only ever adds noise downwards. + double best = 0; + for (int attempt = 0; attempt < 3; attempt++) { + best = Math.max(best, run(threads, ROUNDS)); + } + if (threads == 1) { + single = best; + } + System.out.printf("%8d %16.0f %11.2fx%n", threads, best, best / single); + } + } + + /** + * Runs a fixed amount of work per thread and times the whole thing once. + * + *

A deadline checked inside the loop would be the obvious shape and is + * wrong here: System.nanoTime is not necessarily a cheap userspace read, + * and when it is not, every thread queues on the same clock source. That + * turns the harness itself into the contended resource and reports the + * same flat curve whatever the code under test does. + * + * @param threads number of threads to run + * @param rounds passes over CLASSES per thread + * @return lookups per second across all threads + */ + private static double run(int threads, final int rounds) throws Exception { + ExecutorService pool = Executors.newFixedThreadPool(threads); + try { + final java.util.concurrent.CyclicBarrier start = + new java.util.concurrent.CyclicBarrier(threads); + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + tasks.add(new Callable() { + @Override + public Long call() throws Exception { + start.await(); + long seen = 0; + for (int round = 0; round < rounds; round++) { + for (int i = 0; i < CLASSES.length; i++) { + if (ObjectStreamClass.lookup(CLASSES[i]) != null) { + seen++; + } + } + } + return seen; + } + }); + } + + long began = System.nanoTime(); + long total = 0; + for (Future result : pool.invokeAll(tasks)) { + total += result.get(); + } + long elapsed = System.nanoTime() - began; + return total / (elapsed / 1_000_000_000.0); + } finally { + pool.shutdownNow(); + } + } +} diff --git a/orbmain/src/test/java/com/sun/corba/ee/impl/misc/ConcurrentSoftCacheTest.java b/orbmain/src/test/java/com/sun/corba/ee/impl/misc/ConcurrentSoftCacheTest.java new file mode 100644 index 0000000000..d56d733571 --- /dev/null +++ b/orbmain/src/test/java/com/sun/corba/ee/impl/misc/ConcurrentSoftCacheTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, or the Eclipse Distribution License + * v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the Eclipse + * Public License v. 2.0 are satisfied: GNU General Public License v2.0 + * w/Classpath exception which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause OR GPL-2.0 WITH + * Classpath-exception-2.0 + */ + +package com.sun.corba.ee.impl.misc; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public class ConcurrentSoftCacheTest { + + @Test + public void storesAndReturnsValues() { + ConcurrentSoftCache cache = new ConcurrentSoftCache<>(); + assertNull(cache.get("absent")); + + String value = "a value"; + cache.put("key", value); + assertSame(value, cache.get("key")); + assertEquals(1, cache.size()); + } + + @Test + public void aLaterPutReplacesTheEarlierOne() { + ConcurrentSoftCache cache = new ConcurrentSoftCache<>(); + cache.put("key", "first"); + cache.put("key", "second"); + + assertEquals("second", cache.get("key")); + assertEquals(1, cache.size()); + } + + @Test + public void purgeLeavesLiveEntriesAlone() { + ConcurrentSoftCache cache = new ConcurrentSoftCache<>(); + String held = "still referenced"; + cache.put("key", held); + + cache.purge(); + + assertSame(held, cache.get("key")); + } + + /** + * The cache is read with no lock while other threads write it, which is + * exactly what its predecessor could not survive. + */ + @Test + public void concurrentReadersAndWritersAgree() throws Exception { + final ConcurrentSoftCache cache = new ConcurrentSoftCache<>(); + final int keys = 200; + final List values = new ArrayList<>(); + for (int i = 0; i < keys; i++) { + values.add("value " + i); + } + + int threads = Math.max(4, Runtime.getRuntime().availableProcessors()); + ExecutorService pool = Executors.newFixedThreadPool(threads); + try { + List> tasks = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + tasks.add(new Callable() { + @Override + public Boolean call() { + for (int round = 0; round < 500; round++) { + for (int i = 0; i < keys; i++) { + cache.put(i, values.get(i)); + String seen = cache.get(i); + // Never a value belonging to another key. + if (seen != null && seen != values.get(i)) { + return false; + } + } + } + return true; + } + }); + } + for (Future result : pool.invokeAll(tasks, 60, TimeUnit.SECONDS)) { + assertTrue("a reader saw a value that did not belong to its key", result.get()); + } + assertEquals(keys, cache.size()); + } finally { + pool.shutdownNow(); + } + } +}