From 1f921a09ad2837df76b75e273af5d43f46c3a9af Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 9 Apr 2022 20:52:54 -0700 Subject: [PATCH] Add Interner type for weakKey equality caching (fixes #344) A weak keyed cache uses identity equivalence, as this is the only sensible approach when caching a value. If Objects.equals was used then the user would not be inclined to hold a reference to the canonical key and expect any equivalent key to cause the mapping to be retained. As that is an impossible expectation, the cache would seemingly discard prematurely. Therefore we disallow this case to hint that users should be more thoughtful about their desired behavior. An interner is a Set-based cache where the key is the only item of interest. This allows usages to resolve to a canonical instance, discard duplicates as determined by Object.equals, and thereby reduce memory usage. A weak interner allows the garbage collector to discard the canonical instance when all usages have been reclaimed. This special case of a weak keyed cache with Object.equals behavior is now supported, but hidden through an interface to make the behavior explicit and avoid misuses. For cases that want to cache by a canonical weak key to a value, the two type should be used in conjunction. Instead of incorrectly trying to combine into a single cache, use intern the key and use that for the cache lookup. --- build.gradle | 4 +- .../caffeine/cache/NodeFactoryGenerator.java | 4 +- .../caffeine/cache/NodeSelectorCode.java | 3 + .../caffeine/cache/Specifications.java | 3 + .../caffeine/cache/node/AddHealth.java | 7 +- .../benmanes/caffeine/cache/node/AddKey.java | 4 +- .../caffeine/cache/BoundedLocalCache.java | 20 ++ .../github/benmanes/caffeine/cache/Cache.java | 4 +- .../benmanes/caffeine/cache/Caffeine.java | 7 + .../benmanes/caffeine/cache/Interner.java | 176 ++++++++++++++++++ .../benmanes/caffeine/cache/References.java | 116 ++++++++++-- .../benmanes/caffeine/cache/InternerTest.java | 121 ++++++++++++ .../caffeine/cache/LocalCacheSubject.java | 4 +- gradle/dependencies.gradle | 4 +- 14 files changed, 453 insertions(+), 24 deletions(-) create mode 100644 caffeine/src/main/java/com/github/benmanes/caffeine/cache/Interner.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/cache/InternerTest.java 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..7dfe190210 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,12 @@ public static Caffeine newBuilder() { return new Caffeine<>(); } + 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..4bcdc68ce2 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/InternerTest.java @@ -0,0 +1,121 @@ +/* + * 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.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 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 = ((WeakInterner) interner).cache; + if (cache.size() != size) { + cache.cleanUp(); + } + assertThat(cache).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}",