Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 65 additions & 19 deletions src/main/java/groovy/concurrent/AwaitableAdapterRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.apache.groovy.runtime.async.FlowPublisherAdapter;
import org.apache.groovy.runtime.async.GroovyPromise;

import java.lang.ref.SoftReference;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
Expand Down Expand Up @@ -51,7 +52,31 @@ public final class AwaitableAdapterRegistry {

private static final List<AwaitableAdapter> adapters = new CopyOnWriteArrayList<>();

private static volatile ClassValue<AwaitableAdapter> awaitableCache = buildAwaitableCache();
/**
* Marks a type no adapter supports, so that a cache entry whose {@link SoftReference}
* was cleared can be told apart from a type that resolved to no adapter.
*/
private static final AwaitableAdapter NO_ADAPTER = new AwaitableAdapter() {
@Override
public boolean supportsAwaitable(Class<?> type) {
return false;
}

@Override
public <T> Awaitable<T> toAwaitable(Object source) {
throw new IllegalStateException("NO_ADAPTER cannot adapt");
}
};

/**
* Adapter lookups cached per source class. A {@code ClassValue} association lives as
* long as its key class, and the common keys here are platform classes such as
* {@link CompletableFuture}, so the value is held through a {@link SoftReference}:
* the association then strongly reaches only {@code java.base} objects and never pins
* the adapter's class loader (GROOVY-12280). A cleared reference is re-resolved from
* {@link #adapters} on the next lookup.
*/
private static volatile ClassValue<SoftReference<AwaitableAdapter>> awaitableCache = buildAwaitableCache();

static {
// Load SPI adapters
Expand Down Expand Up @@ -104,7 +129,7 @@ static <T> Awaitable<T> toAwaitable(Object source) {
}
if (source instanceof Awaitable) return (Awaitable<T>) source;
Class<?> type = source.getClass();
AwaitableAdapter adapter = awaitableCache.get(type);
AwaitableAdapter adapter = adapterFor(type);
if (adapter != null) {
return adapter.toAwaitable(source);
}
Expand All @@ -113,6 +138,38 @@ static <T> Awaitable<T> toAwaitable(Object source) {
+ ". Register an AwaitableAdapter via ServiceLoader or AwaitableAdapterRegistry.register().");
}

/**
* The adapter for the given type, or {@code null} when none supports it.
* <p>
* A cleared cache reference is removed and recomputed once; should the fresh
* reference already be cleared as well, the answer comes from an uncached scan,
* so the lookup terminates under any memory pressure.
*/
private static AwaitableAdapter adapterFor(Class<?> type) {
ClassValue<SoftReference<AwaitableAdapter>> cache = awaitableCache;
AwaitableAdapter adapter = cache.get(type).get();
if (adapter == null) {
cache.remove(type);
adapter = cache.get(type).get();
if (adapter == null) {
adapter = resolveAdapter(type);
}
}
return adapter == NO_ADAPTER ? null : adapter;
}

/**
* Resolves the first adapter supporting the supplied type, or {@link #NO_ADAPTER}.
*/
private static AwaitableAdapter resolveAdapter(Class<?> type) {
for (AwaitableAdapter adapter : adapters) {
if (adapter.supportsAwaitable(type)) {
return adapter;
}
}
return NO_ADAPTER;
}

/**
* Converts the given source to an {@link Iterable} for {@code for await}.
*/
Expand All @@ -132,25 +189,14 @@ public static <T> Iterable<T> toIterable(Object source) {
+ ". Register an AwaitableAdapter via ServiceLoader or AwaitableAdapterRegistry.register().");
}

private static ClassValue<AwaitableAdapter> buildAwaitableCache() {
/**
* Cache of awaitable adapters by source type.
*/
// Keep this method static and the ClassValue below free of any enclosing-instance reference:
// capturing one would let the association hold this registry, its class and its loader, undoing
// the loader-unloading this class exists to allow (GROOVY-12280).
private static ClassValue<SoftReference<AwaitableAdapter>> buildAwaitableCache() {
return new ClassValue<>() {
Comment thread
paulk-asert marked this conversation as resolved.
/**
* Resolves the first adapter supporting the supplied type.
*
* @param type the source type
* @return the matching adapter, or {@code null} if none match
*/
@Override
protected AwaitableAdapter computeValue(Class<?> type) {
for (AwaitableAdapter adapter : adapters) {
if (adapter.supportsAwaitable(type)) {
return adapter;
}
}
return null;
protected SoftReference<AwaitableAdapter> computeValue(Class<?> type) {
return new SoftReference<>(resolveAdapter(type));
}
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 groovy.concurrent

import org.junit.jupiter.api.Test

import java.lang.ref.SoftReference
import java.util.concurrent.CompletableFuture

import static groovy.test.GroovyAssert.shouldFail

/**
* GROOVY-12280: the adapter cache's {@code ClassValue} associations live as long as their
* key classes, and the common keys are platform classes, so the cached adapter is held
* through a {@link SoftReference} and re-resolved when the reference has been cleared.
* These tests drive the cleared-reference protocol deterministically by clearing the
* references directly rather than simulating memory pressure.
*/
final class AwaitableAdapterRegistryCacheTest {

private static ClassValue<SoftReference<?>> cache() {
def field = AwaitableAdapterRegistry.getDeclaredField('awaitableCache')
field.accessible = true
field.get(null) as ClassValue<SoftReference<?>>
}

@Test
void testCachedValueIsHeldThroughASoftReference() {
AwaitableAdapterRegistry.toAwaitable(CompletableFuture.completedFuture(1))
def entry = cache().get(CompletableFuture)
assert entry instanceof SoftReference
assert entry.get() instanceof AwaitableAdapter
}

@Test
void testClearedReferenceIsReResolved() {
def future = CompletableFuture.completedFuture(42)
assert AwaitableAdapterRegistry.toAwaitable(future).get() == 42

cache().get(CompletableFuture).clear()

assert AwaitableAdapterRegistry.toAwaitable(future).get() == 42
}

@Test
void testUnsupportedTypeStillFailsAfterItsReferenceIsCleared() {
// "no adapter" must not be confused with "reference cleared": both before and after
// clearing, an unsupported type resolves to no adapter and fails the same way
def err = shouldFail(IllegalArgumentException) {
AwaitableAdapterRegistry.toAwaitable(new Object())
}
assert err.message.contains('No Awaitable adapter found')

cache().get(Object).clear()

err = shouldFail(IllegalArgumentException) {
AwaitableAdapterRegistry.toAwaitable(new Object())
}
assert err.message.contains('No Awaitable adapter found')
}

@Test
void testRegistrationStillRebuildsTheCache() {
def marker = new AwaitableAdapter() {
@Override
boolean supportsAwaitable(Class<?> type) { type == StringBuilder }

@Override
def <T> Awaitable<T> toAwaitable(Object source) { Awaitable.of(source.toString()) }
}
try {
AwaitableAdapterRegistry.register(marker)
assert AwaitableAdapterRegistry.toAwaitable(new StringBuilder('sb')).get() == 'sb'
} finally {
AwaitableAdapterRegistry.unregister(marker)
}
shouldFail(IllegalArgumentException) {
AwaitableAdapterRegistry.toAwaitable(new StringBuilder('sb'))
}
}
}
Loading