diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..f2605b7a7e --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,14 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v1 diff --git a/build.gradle b/build.gradle index e166a74189..d6ccf87353 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,8 @@ allprojects { group = 'com.github.ben-manes.caffeine' version.with { major = 3 // incompatible API changes - minor = 0 // backwards-compatible additions - patch = 7 // backwards-compatible bug fixes + minor = 1 // backwards-compatible additions + patch = 0 // backwards-compatible bug fixes releaseBuild = rootProject.hasProperty('release') } } diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeFactoryGenerator.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeFactoryGenerator.java index bd811db946..70c736da0d 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeFactoryGenerator.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeFactoryGenerator.java @@ -30,7 +30,7 @@ import static com.github.benmanes.caffeine.cache.Specifications.keySpec; import static com.github.benmanes.caffeine.cache.Specifications.lookupKeyType; import static com.github.benmanes.caffeine.cache.Specifications.rawReferenceKeyType; -import static com.github.benmanes.caffeine.cache.Specifications.referenceKeyType; +import static com.github.benmanes.caffeine.cache.Specifications.referenceType; import static com.github.benmanes.caffeine.cache.Specifications.vTypeVar; import static com.github.benmanes.caffeine.cache.Specifications.valueRefQueueSpec; import static com.github.benmanes.caffeine.cache.Specifications.valueSpec; @@ -246,7 +246,7 @@ private MethodSpec newReferenceKeyMethod() { return MethodSpec.methodBuilder("newReferenceKey") .addJavadoc("Returns a key suitable for inserting into the cache. If the cache holds " + "keys strongly then\nthe key is returned. If the cache holds keys weakly " - + "then a {@link $T}\nholding the key argument is returned.\n", referenceKeyType) + + "then a {@link $T}\nholding the key argument is returned.\n", referenceType) .addModifiers(Modifier.PUBLIC, Modifier.DEFAULT) .addParameter(kTypeVar, "key") .addParameter(kRefQueueType, "referenceQueue") diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java index 6eeee8c24b..3af6fac606 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/NodeSelectorCode.java @@ -32,6 +32,9 @@ public final class NodeSelectorCode { private NodeSelectorCode() { block = CodeBlock.builder() + .beginControlFlow("if (builder.interner)") + .addStatement("return new Interned<>()") + .endControlFlow() .addStatement("$1T sb = new $1T(\"$2N.\")", StringBuilder.class, NODE_FACTORY.rawType.packageName()); } diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/Specifications.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/Specifications.java index 378fc8fc85..236d22c804 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/Specifications.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/Specifications.java @@ -17,6 +17,7 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; +import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import com.squareup.javapoet.ClassName; @@ -46,6 +47,8 @@ public final class Specifications { public static final ClassName nodeType = ClassName.get(PACKAGE_NAME, "Node"); public static final TypeName lookupKeyType = ClassName.get(PACKAGE_NAME + ".References", "LookupKeyReference"); + public static final TypeName referenceType = ParameterizedTypeName.get( + ClassName.get(Reference.class), kTypeVar); public static final TypeName referenceKeyType = ParameterizedTypeName.get( ClassName.get(PACKAGE_NAME + ".References", "WeakKeyReference"), kTypeVar); public static final TypeName rawReferenceKeyType = ParameterizedTypeName.get( diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddHealth.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddHealth.java index a9a5308409..16514ba320 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddHealth.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddHealth.java @@ -19,7 +19,7 @@ import static com.github.benmanes.caffeine.cache.Specifications.DEAD_WEAK_KEY; import static com.github.benmanes.caffeine.cache.Specifications.RETIRED_STRONG_KEY; import static com.github.benmanes.caffeine.cache.Specifications.RETIRED_WEAK_KEY; -import static com.github.benmanes.caffeine.cache.Specifications.referenceKeyType; +import static com.github.benmanes.caffeine.cache.Specifications.referenceType; import com.squareup.javapoet.MethodSpec; @@ -67,6 +67,9 @@ private void addState(String checkName, String actionName, String arg, boolean f var action = MethodSpec.methodBuilder(actionName) .addModifiers(context.publicFinalModifiers()); if (valueStrength() == Strength.STRONG) { + if (keyStrength() != Strength.STRONG) { + action.addStatement("key.clear()"); + } // Set the value to null only when dead, as otherwise the explicit removal of an expired async // value will be notified as explicit rather than expired due to the isComputingAsync() check if (finalized) { @@ -77,7 +80,7 @@ private void addState(String checkName, String actionName, String arg, boolean f action.addStatement("$1T valueRef = ($1T) $2L.get(this)", valueReferenceType(), varHandleName("value")); if (keyStrength() != Strength.STRONG) { - action.addStatement("$1T keyRef = ($1T) valueRef.getKeyReference()", referenceKeyType); + action.addStatement("$1T keyRef = ($1T) valueRef.getKeyReference()", referenceType); action.addStatement("keyRef.clear()"); } action.addStatement("valueRef.setKeyReference($N)", arg); diff --git a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java index 66803b0be8..be249eb0a2 100644 --- a/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java +++ b/caffeine/src/javaPoet/java/com/github/benmanes/caffeine/cache/node/AddKey.java @@ -16,7 +16,7 @@ package com.github.benmanes.caffeine.cache.node; import static com.github.benmanes.caffeine.cache.Specifications.kTypeVar; -import static com.github.benmanes.caffeine.cache.Specifications.referenceKeyType; +import static com.github.benmanes.caffeine.cache.Specifications.referenceType; import java.util.List; @@ -78,7 +78,7 @@ private void addIfCollectedValue() { if (isStrongKeys()) { getKey.addStatement("return ($T) valueRef.getKeyReference()", kTypeVar); } else { - getKey.addStatement("$1T keyRef = ($1T) valueRef.getKeyReference()", referenceKeyType); + getKey.addStatement("$1T keyRef = ($1T) valueRef.getKeyReference()", referenceType); getKey.addStatement("return keyRef.get()"); } context.nodeSubtype.addMethod(getKey.build()); diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 3aeff1631e..12029f36f1 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -2109,6 +2109,26 @@ public boolean containsValue(Object value) { return value; } + /** + * Returns the key associated with the mapping in this cache, or {@code null} if there is none. + * + * @param key the key whose canonical instance is to be returned + * @return the key used by the mapping, or {@code null} if this cache does not contain a mapping + * for the key + * @throws NullPointerException if the specified key is null + */ + public @Nullable K getKey(K key) { + Node node = data.get(nodeFactory.newLookupKey(key)); + if (node == null) { + if (drainStatus() == REQUIRED) { + scheduleDrainBuffers(); + } + return null; + } + afterRead(node, /* now */ 0L, /* recordStats */ false); + return node.getKey(); + } + @Override public Map getAllPresent(Iterable keys) { var result = new LinkedHashMap(); diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java index acc3108892..a60d211e2d 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Cache.java @@ -46,8 +46,8 @@ public interface Cache { * cached value for the {@code key}. * * @param key the key whose associated value is to be returned - * @return the value to which the specified key is mapped, or {@code null} if this cache contains - * no mapping for the key + * @return the value to which the specified key is mapped, or {@code null} if this cache does not + * contain a mapping for the key * @throws NullPointerException if the specified key is null */ @Nullable diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java index 7174765a87..5761ae34fd 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Caffeine.java @@ -147,6 +147,7 @@ enum Strength { WEAK, SOFT } static final int DEFAULT_REFRESH_NANOS = 0; boolean strictParsing = true; + boolean interner; long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; @@ -226,6 +227,14 @@ public static Caffeine newBuilder() { return new Caffeine<>(); } + /** Returns a cache that is optimized for weak reference interning (see {@link Interner}). */ + @CheckReturnValue + static BoundedLocalCache newWeakInterner() { + var builder = new Caffeine().executor(Runnable::run).weakKeys(); + builder.interner = true; + return LocalCacheFactory.newBoundedLocalCache(builder, /* loader */ null, /* async */ false); + } + /** * Constructs a new {@code Caffeine} instance with the settings specified in {@code spec}. * diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Interner.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Interner.java new file mode 100644 index 0000000000..b1b5168f52 --- /dev/null +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/Interner.java @@ -0,0 +1,176 @@ +/* + * Copyright 2022 Ben Manes. All Rights Reserved. + * + * Licensed 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 com.github.benmanes.caffeine.cache; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.github.benmanes.caffeine.cache.References.LookupKeyEqualsReference; +import com.github.benmanes.caffeine.cache.References.WeakKeyEqualsReference; + +/** + * Provides similar behavior to {@link String#intern} for any immutable type. + *

+ * Note that {@code String.intern()} has some well-known performance limitations, and should + * generally be avoided. Prefer {@link Interner#newWeakInterner} or another {@code Interner} + * implementation even for {@code String} interning. + * + * @param the type of elements + * @author ben.manes@gmail.com (Ben Manes) + */ +@FunctionalInterface +public interface Interner { + + /** + * Chooses and returns the representative instance for any of a collection of instances that are + * equal to each other. If two {@linkplain Object#equals equal} inputs are given to this method, + * both calls will return the same instance. That is, {@code intern(a).equals(a)} always holds, + * and {@code intern(a) == intern(b)} if and only if {@code a.equals(b)}. Note that {@code + * intern(a)} is permitted to return one instance now and a different instance later if the + * original interned instance was garbage-collected. + *

+ * Warning: do not use with mutable objects. + * + * @param sample the element to add if absent + * @return the representative instance, possibly the {@code sample} if absent + * @throws NullPointerException if {@code sample} is null + */ + E intern(E sample); + + /** + * Returns a new thread-safe interner which retains a strong reference to each instance it has + * interned, thus preventing these instances from being garbage-collected. + * + * @param the type of elements + * @return an interner for retrieving the canonical instance + */ + static Interner newStrongInterner() { + return new StrongInterner<>(); + } + + /** + * Returns a new thread-safe interner which retains a weak reference to each instance it has + * interned, and so does not prevent these instances from being garbage-collected. + * + * @param the type of elements + * @return an interner for retrieving the canonical instance + */ + static Interner newWeakInterner() { + return new WeakInterner<>(); + } +} + +final class StrongInterner implements Interner { + final ConcurrentMap map; + + StrongInterner() { + map = new ConcurrentHashMap<>(); + } + @Override public E intern(E sample) { + E canonical = map.get(sample); + if (canonical != null) { + return canonical; + } + + var value = map.putIfAbsent(sample, sample); + if (value == null) { + return sample; + } + return value; + } +} + +final class WeakInterner implements Interner { + final BoundedLocalCache cache; + + WeakInterner() { + cache = Caffeine.newWeakInterner(); + } + @Override public E intern(E sample) { + for (;;) { + E canonical = cache.getKey(sample); + if (canonical != null) { + return canonical; + } + + var value = cache.putIfAbsent(sample, Boolean.TRUE); + if (value == null) { + return sample; + } + } + } +} + +@SuppressWarnings({"unchecked", "NullAway"}) +final class Interned extends Node implements NodeFactory { + volatile Reference keyReference; + + Interned() {} + + Interned(Reference keyReference) { + this.keyReference = keyReference; + } + @Override public K getKey() { + return (K) keyReference.get(); + } + @Override public Object getKeyReference() { + return keyReference; + } + @Override public V getValue() { + return (V) Boolean.TRUE; + } + @Override public V getValueReference() { + return (V) Boolean.TRUE; + } + @Override public void setValue(V value, ReferenceQueue referenceQueue) {} + @Override public boolean containsValue(Object value) { + return Objects.equals(value, getValue()); + } + @Override public Node newNode(K key, ReferenceQueue keyReferenceQueue, + V value, ReferenceQueue valueReferenceQueue, int weight, long now) { + return new Interned<>(new WeakKeyEqualsReference<>(key, keyReferenceQueue)); + } + @Override public Node newNode(Object keyReference, V value, + ReferenceQueue valueReferenceQueue, int weight, long now) { + return new Interned<>((Reference) keyReference); + } + @Override public Object newLookupKey(Object key) { + return new LookupKeyEqualsReference<>(key); + } + @Override public Object newReferenceKey(K key, ReferenceQueue referenceQueue) { + return new WeakKeyEqualsReference(key, referenceQueue); + } + @Override public boolean isAlive() { + Object keyRef = keyReference; + return (keyRef != RETIRED_WEAK_KEY) && (keyRef != DEAD_WEAK_KEY); + } + @Override public boolean isRetired() { + return (keyReference == RETIRED_WEAK_KEY); + } + @Override public void retire() { + keyReference = RETIRED_WEAK_KEY; + } + @Override public boolean isDead() { + return (keyReference == DEAD_WEAK_KEY); + } + @Override public void die() { + keyReference.clear(); + keyReference = DEAD_WEAK_KEY; + } +} diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/References.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/References.java index 8b17c89bba..2f009f6ab2 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/References.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/References.java @@ -20,6 +20,7 @@ import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; +import java.util.Objects; import org.checkerframework.checker.nullness.qual.Nullable; @@ -56,7 +57,7 @@ interface InternalReference { Object getKeyReference(); /** - * Returns {@code true} if the arguments is an {@linkplain InternalReference} that holds the + * Returns {@code true} if the arguments is a {@linkplain InternalReference} that holds the * same element. A weakly or softly held element is compared using identity equality. * * @param object the reference object with which to compare @@ -71,6 +72,24 @@ default boolean referenceEquals(@Nullable Object object) { } return false; } + + /** + * Returns {@code true} if the arguments is a {@linkplain InternalReference} that holds an + * equivalent element as determined by {@link Object#equals}. + * + * @param object the reference object with which to compare + * @return {@code true} if this object is equivalent by {@link Object#equals} as the argument; + * {@code false} otherwise + */ + default boolean objectEquals(Object object) { + if (object == this) { + return true; + } else if (object instanceof InternalReference) { + InternalReference referent = (InternalReference) object; + return Objects.equals(get(), referent.get()); + } + return false; + } } /** @@ -78,18 +97,18 @@ default boolean referenceEquals(@Nullable Object object) { * This {@linkplain InternalReference} implementation is not suitable for storing in the cache as * the key is strongly held. */ - static final class LookupKeyReference implements InternalReference { + static final class LookupKeyReference implements InternalReference { private final int hashCode; - private final E e; + private final K key; - public LookupKeyReference(E e) { - this.hashCode = System.identityHashCode(e); - this.e = requireNonNull(e); + public LookupKeyReference(K key) { + this.hashCode = System.identityHashCode(key); + this.key = requireNonNull(key); } @Override - public E get() { - return e; + public K get() { + return key; } @Override @@ -109,7 +128,47 @@ public int hashCode() { @Override public String toString() { - return String.format("%s{e=%s, hashCode=%d}", getClass().getSimpleName(), e, hashCode); + return String.format("%s{key=%s, hashCode=%d}", getClass().getSimpleName(), get(), hashCode); + } + } + + /** + * A short-lived adapter used for looking up an entry in the cache where the keys are weakly held. + * This {@linkplain InternalReference} implementation is not suitable for storing in the cache as + * the key is strongly held. + */ + static final class LookupKeyEqualsReference implements InternalReference { + private final int hashCode; + private final K key; + + public LookupKeyEqualsReference(K key) { + this.hashCode = key.hashCode(); + this.key = requireNonNull(key); + } + + @Override + public K get() { + return key; + } + + @Override + public Object getKeyReference() { + return this; + } + + @Override + public boolean equals(Object object) { + return objectEquals(object); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return String.format("%s{key=%s, hashCode=%d}", getClass().getSimpleName(), get(), hashCode); } } @@ -118,7 +177,7 @@ public String toString() { * the advent that the key is reclaimed so that the entry can be removed from the cache in * constant time. */ - static class WeakKeyReference extends WeakReference implements InternalReference { + static final class WeakKeyReference extends WeakReference implements InternalReference { private final int hashCode; public WeakKeyReference(@Nullable K key, @Nullable ReferenceQueue queue) { @@ -143,7 +202,42 @@ public int hashCode() { @Override public String toString() { - return String.format("%s{hashCode=%d}", getClass().getSimpleName(), hashCode); + return String.format("%s{key=%s, hashCode=%d}", getClass().getSimpleName(), get(), hashCode); + } + } + + /** + * The key in a cache that holds the key weakly and uses equals equivalence. This class retains + * the key's hash code in the advent that the key is reclaimed so that the entry can be removed + * from the cache in constant time. + */ + static final class WeakKeyEqualsReference + extends WeakReference implements InternalReference { + private final int hashCode; + + public WeakKeyEqualsReference(K key, @Nullable ReferenceQueue queue) { + super(key, queue); + hashCode = key.hashCode(); + } + + @Override + public Object getKeyReference() { + return this; + } + + @Override + public boolean equals(Object object) { + return objectEquals(object); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return String.format("%s{key=%s, hashCode=%d}", getClass().getSimpleName(), get(), hashCode); } } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/InternerTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/InternerTest.java new file mode 100644 index 0000000000..791b975c84 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/InternerTest.java @@ -0,0 +1,140 @@ +/* + * Copyright 2022 Ben Manes. All Rights Reserved. + * + * Licensed 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 com.github.benmanes.caffeine.cache; + +import static com.github.benmanes.caffeine.cache.LocalCacheSubject.mapLocal; +import static com.github.benmanes.caffeine.cache.testing.CacheSubject.assertThat; +import static com.github.benmanes.caffeine.testing.MapSubject.assertThat; +import static com.google.common.truth.Truth.assertAbout; +import static com.google.common.truth.Truth.assertThat; + +import java.lang.ref.WeakReference; + +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import com.github.benmanes.caffeine.testing.Int; +import com.google.common.testing.GcFinalization; +import com.google.common.testing.NullPointerTester; + +/** + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class InternerTest { + + @Test(dataProvider = "interners", expectedExceptions = NullPointerException.class) + public void intern_null(Interner interner) { + interner.intern(null); + } + + @Test(dataProvider = "interners") + public void intern(Interner interner) { + var canonical = new Int(1); + var other = new Int(1); + + assertThat(interner.intern(canonical)).isSameInstanceAs(canonical); + assertThat(interner.intern(other)).isSameInstanceAs(canonical); + checkSize(interner, 1); + + var next = new Int(2); + assertThat(interner.intern(next)).isSameInstanceAs(next); + checkSize(interner, 2); + checkState(interner); + } + + @Test + public void intern_weak_replace() { + var canonical = new Int(1); + var other = new Int(1); + + Interner interner = Interner.newWeakInterner(); + assertThat(interner.intern(canonical)).isSameInstanceAs(canonical); + + var signal = new WeakReference<>(canonical); + canonical = null; + + GcFinalization.awaitClear(signal); + assertThat(interner.intern(other)).isSameInstanceAs(other); + checkSize(interner, 1); + checkState(interner); + } + + @Test + public void intern_weak_remove() { + var canonical = new Int(1); + var next = new Int(2); + + Interner interner = Interner.newWeakInterner(); + assertThat(interner.intern(canonical)).isSameInstanceAs(canonical); + + var signal = new WeakReference<>(canonical); + canonical = null; + + GcFinalization.awaitClear(signal); + assertThat(interner.intern(next)).isSameInstanceAs(next); + checkSize(interner, 1); + checkState(interner); + } + + @Test + public void intern_weak_cleanup() { + var interner = (WeakInterner) Interner.newWeakInterner(); + interner.cache.drainStatus = BoundedLocalCache.REQUIRED; + + var canonical = new Int(1); + interner.intern(canonical); + assertThat(interner.cache.drainStatus).isEqualTo(BoundedLocalCache.IDLE); + + interner.cache.drainStatus = BoundedLocalCache.REQUIRED; + interner.intern(canonical); + assertThat(interner.cache.drainStatus).isEqualTo(BoundedLocalCache.IDLE); + } + + @Test + public void nullPointerExceptions() { + new NullPointerTester().testAllPublicStaticMethods(Interner.class); + } + + private void checkSize(Interner interner, int size) { + if (interner instanceof StrongInterner) { + assertThat(((StrongInterner) interner).map).hasSize(size); + } else if (interner instanceof WeakInterner) { + var cache = new LocalManualCache() { + @Override public LocalCache cache() { + return ((WeakInterner) interner).cache; + } + @Override public Policy policy() { + throw new UnsupportedOperationException(); + } + }; + assertThat(cache).whenCleanedUp().hasSize(size); + } else { + Assert.fail(); + } + } + + private void checkState(Interner interner) { + if (interner instanceof WeakInterner) { + assertAbout(mapLocal()).that(((WeakInterner) interner).cache).isValid(); + } + } + + @DataProvider(name = "interners") + Object[] providesInterners() { + return new Object[] { Interner.newStrongInterner(), Interner.newWeakInterner() }; + } +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LocalCacheSubject.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LocalCacheSubject.java index b3a3c35db2..de60d1d7c4 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LocalCacheSubject.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/LocalCacheSubject.java @@ -30,6 +30,7 @@ import com.github.benmanes.caffeine.cache.BoundedLocalCache.BoundedLocalAsyncLoadingCache; import com.github.benmanes.caffeine.cache.BoundedLocalCache.BoundedLocalManualCache; import com.github.benmanes.caffeine.cache.LocalAsyncLoadingCache.LoadingCacheView; +import com.github.benmanes.caffeine.cache.References.WeakKeyEqualsReference; import com.github.benmanes.caffeine.cache.References.WeakKeyReference; import com.github.benmanes.caffeine.cache.TimerWheel.Sentinel; import com.github.benmanes.caffeine.cache.UnboundedLocalCache.UnboundedLocalAsyncCache; @@ -341,7 +342,8 @@ private void checkKey(BoundedLocalCache bounded, if ((key != null) && (value != null)) { check("bounded").that(bounded).containsKey(key); } - check("keyReference").that(node.getKeyReference()).isInstanceOf(WeakKeyReference.class); + var clazz = node instanceof Interned ? WeakKeyEqualsReference.class : WeakKeyReference.class; + check("keyReference").that(node.getKeyReference()).isInstanceOf(clazz); } else { check("key").that(key).isNotNull(); } diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 74eebb0afb..63f379944c 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -90,7 +90,7 @@ ext { coveralls: '2.12.0', dependencyCheck: '7.0.4.1', errorprone: '2.0.2', - findsecbugs: '1.11.0', + findsecbugs: '1.12.0', jacoco: '0.8.7', jmh: '0.6.6', jmhReport: '0.9.0', @@ -195,7 +195,7 @@ ext { testng: [ "org.testng:testng:${testVersions.testng}", "com.google.inject:guice:${testVersions.guice}", - 'org.ow2.asm:asm:9.2', + 'org.ow2.asm:asm:9.3', ], truth: [ "com.google.truth:truth:${testVersions.truth}",