Enhance WeakKeySet to auto evict keys and avoid calling toString on Keys.
This should fix https://code.google.com/p/google-guice/issues/detail?id=756.
-------------
Created by MOE: http://code.google.com/p/moe-java
MOE_MIGRATED_REVID=64083354
diff --git a/build.xml b/build.xml
index 203d504..09acb0d 100644
--- a/build.xml
+++ b/build.xml
@@ -105,6 +105,7 @@
<pathelement location="lib/javax.inject.jar"/>
<pathelement location="lib/aopalliance.jar"/>
<pathelement location="lib/guava-16.0.1.jar"/>
+ <pathelement location="lib/build/guava-testlib-16.0.1.jar"/>
<pathelement location="lib/build/junit.jar"/>
<pathelement location="lib/build/servlet-api-2.5.jar"/>
<pathelement location="lib/build/easymock.jar"/>
diff --git a/core/pom.xml b/core/pom.xml
index e420035..ffb78ab 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -29,6 +29,12 @@
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-testlib</artifactId>
+ <version>16.0.1</version>
+ <scope>test</scope>
+ </dependency>
<!--
| CGLIB is embedded by default by the JarJar build profile
-->
diff --git a/core/src/com/google/inject/internal/AbstractBindingProcessor.java b/core/src/com/google/inject/internal/AbstractBindingProcessor.java
index 72dae2b..e1a0bba 100644
--- a/core/src/com/google/inject/internal/AbstractBindingProcessor.java
+++ b/core/src/com/google/inject/internal/AbstractBindingProcessor.java
@@ -98,7 +98,7 @@
}
// prevent the parent from creating a JIT binding for this key
- injector.state.parent().blacklist(key, binding.getSource());
+ injector.state.parent().blacklist(key, injector.state, binding.getSource());
injector.state.putBinding(key, binding);
}
diff --git a/core/src/com/google/inject/internal/InheritingState.java b/core/src/com/google/inject/internal/InheritingState.java
index 2d694cb..06e0ec4 100644
--- a/core/src/com/google/inject/internal/InheritingState.java
+++ b/core/src/com/google/inject/internal/InheritingState.java
@@ -56,12 +56,13 @@
/*end[AOP]*/
private final List<TypeListenerBinding> typeListenerBindings = Lists.newArrayList();
private final List<ProvisionListenerBinding> provisionListenerBindings = Lists.newArrayList();
- private final WeakKeySet blacklistedKeys = new WeakKeySet();
+ private final WeakKeySet blacklistedKeys;
private final Object lock;
InheritingState(State parent) {
this.parent = checkNotNull(parent, "parent");
this.lock = (parent == State.NONE) ? this : parent.lock();
+ this.blacklistedKeys = new WeakKeySet(lock);
}
public State parent() {
@@ -154,9 +155,9 @@
return result;
}
- public void blacklist(Key<?> key, Object source) {
- parent.blacklist(key, source);
- blacklistedKeys.add(key, source);
+ public void blacklist(Key<?> key, State state, Object source) {
+ parent.blacklist(key, state, source);
+ blacklistedKeys.add(key, state, source);
}
public boolean isBlacklisted(Key<?> key) {
diff --git a/core/src/com/google/inject/internal/InjectorImpl.java b/core/src/com/google/inject/internal/InjectorImpl.java
index 6faab97..10bd9be 100644
--- a/core/src/com/google/inject/internal/InjectorImpl.java
+++ b/core/src/com/google/inject/internal/InjectorImpl.java
@@ -791,7 +791,7 @@
}
BindingImpl<T> binding = createJustInTimeBinding(key, errors, jitDisabled, jitType);
- state.parent().blacklist(key, binding.getSource());
+ state.parent().blacklist(key, state, binding.getSource());
jitBindings.put(key, binding);
return binding;
}
diff --git a/core/src/com/google/inject/internal/State.java b/core/src/com/google/inject/internal/State.java
index d824cc1..3c9e081 100644
--- a/core/src/com/google/inject/internal/State.java
+++ b/core/src/com/google/inject/internal/State.java
@@ -105,7 +105,7 @@
return ImmutableList.of();
}
- public void blacklist(Key<?> key, Object source) {
+ public void blacklist(Key<?> key, State state, Object source) {
}
public boolean isBlacklisted(Key<?> key) {
@@ -165,9 +165,9 @@
/**
* Forbids the corresponding injector from creating a binding to {@code key}. Child injectors
* blacklist their bound keys on their parent injectors to prevent just-in-time bindings on the
- * parent injector that would conflict.
+ * parent injector that would conflict and pass along their state to control the lifetimes.
*/
- void blacklist(Key<?> key, Object source);
+ void blacklist(Key<?> key, State state, Object source);
/**
* Returns true if {@code key} is forbidden from being bound in this injector. This indicates that
diff --git a/core/src/com/google/inject/internal/WeakKeySet.java b/core/src/com/google/inject/internal/WeakKeySet.java
index 761cd35..022aa4f 100644
--- a/core/src/com/google/inject/internal/WeakKeySet.java
+++ b/core/src/com/google/inject/internal/WeakKeySet.java
@@ -16,37 +16,86 @@
package com.google.inject.internal;
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalCause;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.collect.LinkedHashMultiset;
import com.google.common.collect.Maps;
+import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.inject.Key;
import com.google.inject.internal.util.SourceProvider;
+import java.lang.annotation.Annotation;
import java.util.Map;
import java.util.Set;
/**
* Minimal set that doesn't hold strong references to the contained keys.
*
- * @author [email protected] (Jesse Wilson)
+ * @author [email protected] (Daniel Weis)
*/
final class WeakKeySet {
- // TODO(user): Instead of relying on Key#toString(), consider maintaining a set of custom weak
- // references for child injectors. On some actions, we can poll the ReferenceQueue and clear the
- // relevant blacklisted values. This would allow us to avoid forcing the toString memoization on
- // Key and fix the memory leak referenced in
- // https://code.google.com/p/google-guice/issues/detail?id=756.
+ /**
+ * The key for the Map is {@link Key} for most bindings, String for multibindings.
+ * <p>
+ * Reason being that multibinding Key's annotations hold a reference to their injector, implying
+ * we'd leak memory.
+ */
+ private Map<Object, Multiset<Object>> backingSet;
+
+ /**
+ * This is already locked externally on add and getSources but we need it to handle clean up in
+ * the evictionCache's RemovalListener.
+ */
+ private final Object lock;
/**
- * We store strings rather than keys so we don't hold strong references.
- *
- * <p>One potential problem with this approach is that parent and child injectors cannot define
- * keys whose class names are equal but class loaders are different. This shouldn't be an issue
- * in practice.
+ * Tracks child injector lifetimes and evicts blacklisted keys/sources after the child injector is
+ * garbage collected.
*/
- private Map<String, Set<Object>> backingSet;
+ private final Cache<State, Set<KeyAndSource>> evictionCache = CacheBuilder.newBuilder()
+ .weakKeys()
+ .removalListener(
+ new RemovalListener<State, Set<KeyAndSource>>() {
+ @Override
+ public void onRemoval(RemovalNotification<State, Set<KeyAndSource>> notification) {
+ Preconditions.checkState(RemovalCause.COLLECTED.equals(notification.getCause()));
- public void add(Key<?> key, Object source) {
+ cleanUpForCollectedState(notification.getValue());
+ }
+ })
+ .build();
+
+ /**
+ * There may be multiple child injectors blacklisting a certain key so only remove the source
+ * that's relevant.
+ */
+ private void cleanUpForCollectedState(Set<KeyAndSource> keysAndSources) {
+ synchronized (lock) {
+
+ for (KeyAndSource keyAndSource : keysAndSources) {
+ Multiset<Object> set = backingSet.get(keyAndSource.mapKey);
+ if (set != null) {
+ set.remove(keyAndSource.source);
+ if (set.isEmpty()) {
+ backingSet.remove(keyAndSource.mapKey);
+ }
+ }
+ }
+ }
+ }
+
+ WeakKeySet(Object lock) {
+ this.lock = lock;
+ }
+
+ public void add(Key<?> key, State state, Object source) {
if (backingSet == null) {
backingSet = Maps.newHashMap();
}
@@ -55,22 +104,74 @@
if (source instanceof Class || source == SourceProvider.UNKNOWN_SOURCE) {
source = null;
}
- String k = key.toString();
- Set<Object> sources = backingSet.get(k);
+ Object mapKey = toMapKey(key);
+ Multiset<Object> sources = backingSet.get(mapKey);
if (sources == null) {
- sources = Sets.newLinkedHashSet();
- backingSet.put(k, sources);
+ sources = LinkedHashMultiset.create();
+ backingSet.put(mapKey, sources);
}
- sources.add(Errors.convert(source));
+ Object convertedSource = Errors.convert(source);
+ sources.add(convertedSource);
+
+ // Avoid all the extra work if we can.
+ if (state.parent() != State.NONE) {
+ Set<KeyAndSource> keyAndSources = evictionCache.getIfPresent(state);
+ if (keyAndSources == null) {
+ evictionCache.put(state, keyAndSources = Sets.newHashSet());
+ }
+ keyAndSources.add(new KeyAndSource(mapKey, convertedSource));
+ }
}
public boolean contains(Key<?> key) {
- // avoid calling key.toString() if the backing set is empty. toString is expensive in aggregate,
- // and most WeakKeySets are empty in practice (because they're used by top-level injectors)
- return backingSet != null && backingSet.containsKey(key.toString());
+ evictionCache.cleanUp();
+ return backingSet != null && backingSet.containsKey(key);
}
public Set<Object> getSources(Key<?> key) {
- return backingSet.get(key.toString());
+ evictionCache.cleanUp();
+ Multiset<Object> sources = backingSet.get(key);
+ return (sources == null) ? null : sources.elementSet();
+ }
+
+ private static Object toMapKey(Key<?> key) {
+ Annotation annotation = key.getAnnotation();
+ if (annotation != null
+ // HACK: See comment on backingSet for more info. This is tested in MultibinderTest,
+ // MapBinderTest, and OptionalBinderTest in the multibinder test suite.
+ && "com.google.inject.multibindings.RealElement".equals(annotation.getClass().getName())) {
+ return key.toString();
+ }
+ return key;
+ }
+
+ private static final class KeyAndSource {
+ final Object mapKey;
+ final Object source;
+
+ KeyAndSource(Object mapKey, Object source) {
+ this.mapKey = mapKey;
+ this.source = source;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(mapKey, source);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+
+ if (!(obj instanceof KeyAndSource)) {
+ return false;
+ }
+
+ KeyAndSource other = (KeyAndSource) obj;
+ return Objects.equal(mapKey, other.mapKey)
+ && Objects.equal(source, other.source);
+ }
}
}
diff --git a/core/test/com/google/inject/AllTests.java b/core/test/com/google/inject/AllTests.java
index 54f4dea..c0fd13f 100644
--- a/core/test/com/google/inject/AllTests.java
+++ b/core/test/com/google/inject/AllTests.java
@@ -20,6 +20,7 @@
import com.google.inject.internal.MoreTypesTest;
import com.google.inject.internal.RehashableKeysTest;
import com.google.inject.internal.UniqueAnnotationsTest;
+import com.google.inject.internal.WeakKeySetTest;
import com.google.inject.internal.util.LineNumbersTest;
import com.google.inject.matcher.MatcherTest;
import com.google.inject.name.NamedEquivalanceTest;
@@ -106,6 +107,7 @@
suite.addTestSuite(TypeLiteralInjectionTest.class);
suite.addTestSuite(TypeLiteralTest.class);
suite.addTestSuite(TypeLiteralTypeResolutionTest.class);
+ suite.addTestSuite(WeakKeySetTest.class);
// internal
suite.addTestSuite(LineNumbersTest.class);
diff --git a/core/test/com/google/inject/internal/WeakKeySetTest.java b/core/test/com/google/inject/internal/WeakKeySetTest.java
new file mode 100644
index 0000000..c3b2df8
--- /dev/null
+++ b/core/test/com/google/inject/internal/WeakKeySetTest.java
@@ -0,0 +1,578 @@
+/**
+ * Copyright (C) 2014 Google Inc.
+ *
+ * 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.google.inject.internal;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.testing.GcFinalization;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Scope;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.BindingImpl;
+import com.google.inject.internal.Errors;
+/*if[AOP]*/
+import com.google.inject.internal.MethodAspect;
+/*end[AOP]*/
+import com.google.inject.internal.State;
+import com.google.inject.internal.WeakKeySet;
+import com.google.inject.spi.ProvisionListenerBinding;
+import com.google.inject.spi.ScopeBinding;
+import com.google.inject.spi.TypeConverterBinding;
+import com.google.inject.spi.TypeListenerBinding;
+
+import junit.framework.TestCase;
+
+import java.lang.annotation.Annotation;
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tests for {@link WeakKeySet}.
+ * <p>
+ * Multibinding specific tests can be found in MultibinderTest and MapBinderTest.
+ *
+ * @author [email protected] (Daniel Weis)
+ */
+public class WeakKeySetTest extends TestCase {
+
+ private WeakKeySet set;
+
+ @Override
+ protected void setUp() throws Exception {
+ set = new WeakKeySet(new Object());
+ }
+
+ public void testEviction() {
+ TestState state = new TestState();
+ Key<Integer> key = Key.get(Integer.class);
+ Object source = new Object();
+
+ WeakReference<Key<Integer>> weakKeyRef = new WeakReference<Key<Integer>>(key);
+
+ set.add(key, state, source);
+ assertTrue(set.contains(key));
+ assertEquals(1, set.getSources(key).size());
+ assertTrue(set.getSources(key).contains(source));
+
+ state = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertFalse(set.contains(Key.get(Integer.class)));
+ assertNull(set.getSources(Key.get(Integer.class)));
+
+ // Ensure there are no hanging references.
+ key = null;
+ GcFinalization.awaitClear(weakKeyRef);
+ }
+
+ public void testEviction_nullSource() {
+ TestState state = new TestState();
+ Key<Integer> key = Key.get(Integer.class);
+ Object source = null;
+
+ WeakReference<Key<Integer>> weakKeyRef = new WeakReference<Key<Integer>>(key);
+
+ set.add(key, state, source);
+ assertTrue(set.contains(key));
+ assertEquals(1, set.getSources(key).size());
+ assertTrue(set.getSources(key).contains(source));
+
+ state = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertFalse(set.contains(Key.get(Integer.class)));
+ assertNull(set.getSources(Key.get(Integer.class)));
+
+ // Ensure there are no hanging references.
+ key = null;
+ GcFinalization.awaitClear(weakKeyRef);
+ }
+
+ public void testEviction_keyOverlap_2x() {
+ TestState state1 = new TestState();
+ TestState state2 = new TestState();
+ Key<Integer> key1 = Key.get(Integer.class);
+ Key<Integer> key2 = Key.get(Integer.class);
+ Object source1 = new Object();
+ Object source2 = new Object();
+
+ set.add(key1, state1, source1);
+ assertTrue(set.contains(key1));
+ assertEquals(1, set.getSources(key1).size());
+ assertTrue(set.getSources(key1).contains(source1));
+
+ set.add(key2, state2, source2);
+ assertTrue(set.contains(key2));
+ assertEquals(2, set.getSources(key2).size());
+ assertTrue(set.getSources(key2).containsAll(Arrays.asList(source1, source2)));
+
+ WeakReference<Key<Integer>> weakKey1Ref = new WeakReference<Key<Integer>>(key1);
+ WeakReference<Key<Integer>> weakKey2Ref = new WeakReference<Key<Integer>>(key2);
+ WeakReference<Object> weakSource1Ref = new WeakReference<Object>(source1);
+ WeakReference<Object> weakSource2Ref = new WeakReference<Object>(source2);
+
+ Key<Integer> key = key1 = key2 = Key.get(Integer.class);
+ state1 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertTrue(set.contains(key));
+ assertEquals(1, set.getSources(key).size());
+ assertTrue(set.getSources(key).contains(source2));
+ assertFalse(set.getSources(key).contains(source1));
+
+ source1 = source2 = null;
+
+ GcFinalization.awaitClear(weakSource1Ref);
+ // Key1 will be referenced as the key in the sources backingSet and won't be
+ // GC'd.
+
+ // Should not be GC'd until state2 goes away.
+ assertNotNull(weakSource2Ref.get());
+
+ state2 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertFalse(set.contains(key));
+ assertNull(set.getSources(key));
+
+ GcFinalization.awaitClear(weakKey2Ref);
+ GcFinalization.awaitClear(weakSource2Ref);
+ // Now that the backing set is emptied, key1 is released.
+ GcFinalization.awaitClear(weakKey1Ref);
+ }
+
+ public void testNoEviction_keyOverlap_2x() {
+ TestState state1 = new TestState();
+ TestState state2 = new TestState();
+ Key<Integer> key1 = Key.get(Integer.class);
+ Key<Integer> key2 = Key.get(Integer.class);
+ Object source1 = new Object();
+ Object source2 = new Object();
+
+ set.add(key1, state1, source1);
+ assertTrue(set.contains(key1));
+ assertEquals(1, set.getSources(key1).size());
+ assertTrue(set.getSources(key1).contains(source1));
+
+ set.add(key2, state2, source2);
+ assertTrue(set.contains(key2));
+ assertEquals(2, set.getSources(key2).size());
+ assertTrue(set.getSources(key1).containsAll(Arrays.asList(source1, source2)));
+
+ WeakReference<Key<Integer>> weakKey1Ref = new WeakReference<Key<Integer>>(key1);
+ WeakReference<Key<Integer>> weakKey2Ref = new WeakReference<Key<Integer>>(key2);
+
+ Key<Integer> key = key1 = key2 = Key.get(Integer.class);
+
+ GcFinalization.awaitFullGc();
+
+ assertTrue(set.contains(key));
+ assertEquals(2, set.getSources(key).size());
+ assertTrue(set.getSources(key1).containsAll(Arrays.asList(source1, source2)));
+
+ // Ensure the keys don't get GC'd when states are still referenced. key1 will be present in the
+ // as the map key but key2 could be GC'd if the implementation does something wrong.
+ assertNotNull(weakKey1Ref.get());
+ assertNotNull(weakKey2Ref.get());
+ }
+
+ public void testEviction_keyAndSourceOverlap_null() {
+ TestState state1 = new TestState();
+ TestState state2 = new TestState();
+ Key<Integer> key1 = Key.get(Integer.class);
+ Key<Integer> key2 = Key.get(Integer.class);
+ Object source = null;
+
+ set.add(key1, state1, source);
+ assertTrue(set.contains(key1));
+ assertEquals(1, set.getSources(key1).size());
+ assertTrue(set.getSources(key1).contains(source));
+
+ set.add(key2, state2, source);
+ assertTrue(set.contains(key2));
+
+ // Same source so still only one value.
+ assertEquals(1, set.getSources(key2).size());
+ assertTrue(set.getSources(key1).contains(source));
+
+ WeakReference<Key<Integer>> weakKey1Ref = new WeakReference<Key<Integer>>(key1);
+ WeakReference<Key<Integer>> weakKey2Ref = new WeakReference<Key<Integer>>(key2);
+ WeakReference<Object> weakSourceRef = new WeakReference<Object>(source);
+
+ Key<Integer> key = key1 = key2 = Key.get(Integer.class);
+ state1 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertTrue(set.contains(key));
+
+ // Should still have a single source.
+ assertEquals(1, set.getSources(key).size());
+ assertTrue(set.getSources(key1).contains(source));
+
+ source = null;
+
+ GcFinalization.awaitClear(weakSourceRef);
+ // Key1 will be referenced as the key in the sources backingSet and won't be
+ // GC'd.
+
+ state2 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertFalse(set.contains(key));
+ assertNull(set.getSources(key));
+
+ GcFinalization.awaitClear(weakKey2Ref);
+ GcFinalization.awaitClear(weakSourceRef);
+ // Now that the backing set is emptied, key1 is released.
+ GcFinalization.awaitClear(weakKey1Ref);
+ }
+
+ public void testEviction_keyAndSourceOverlap_nonNull() {
+ TestState state1 = new TestState();
+ TestState state2 = new TestState();
+ Key<Integer> key1 = Key.get(Integer.class);
+ Key<Integer> key2 = Key.get(Integer.class);
+ Object source = new Object();
+
+ set.add(key1, state1, source);
+ assertTrue(set.contains(key1));
+ assertEquals(1, set.getSources(key1).size());
+ assertTrue(set.getSources(key1).contains(source));
+
+ set.add(key2, state2, source);
+ assertTrue(set.contains(key2));
+
+ // Same source so still only one value.
+ assertEquals(1, set.getSources(key2).size());
+ assertTrue(set.getSources(key1).contains(source));
+
+ WeakReference<Key<Integer>> weakKey1Ref = new WeakReference<Key<Integer>>(key1);
+ WeakReference<Key<Integer>> weakKey2Ref = new WeakReference<Key<Integer>>(key2);
+ WeakReference<Object> weakSourceRef = new WeakReference<Object>(source);
+
+ Key<Integer> key = key1 = key2 = Key.get(Integer.class);
+ state1 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertTrue(set.contains(key));
+
+ // Should still have a single source.
+ assertEquals(1, set.getSources(key).size());
+ assertTrue(set.getSources(key1).contains(source));
+
+ source = null;
+
+ GcFinalization.awaitFullGc();
+ assertNotNull(weakSourceRef.get());
+ // Key1 will be referenced as the key in the sources backingSet and won't be
+ // GC'd.
+
+ state2 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertFalse(set.contains(key));
+ assertNull(set.getSources(key));
+
+ GcFinalization.awaitClear(weakKey2Ref);
+ GcFinalization.awaitClear(weakSourceRef);
+ // Now that the backing set is emptied, key1 is released.
+ GcFinalization.awaitClear(weakKey1Ref);
+ }
+
+ public void testEviction_keyOverlap_3x() {
+ TestState state1 = new TestState();
+ TestState state2 = new TestState();
+ TestState state3 = new TestState();
+ Key<Integer> key1 = Key.get(Integer.class);
+ Key<Integer> key2 = Key.get(Integer.class);
+ Key<Integer> key3 = Key.get(Integer.class);
+ Object source1 = new Object();
+ Object source2 = new Object();
+ Object source3 = new Object();
+
+ set.add(key1, state1, source1);
+ assertTrue(set.contains(key1));
+ assertEquals(1, set.getSources(key1).size());
+ assertTrue(set.getSources(key1).contains(source1));
+
+ set.add(key2, state2, source2);
+ assertTrue(set.contains(key2));
+ assertEquals(2, set.getSources(key2).size());
+ assertTrue(set.getSources(key1).containsAll(Arrays.asList(source1, source2)));
+
+ set.add(key3, state3, source3);
+ assertTrue(set.contains(key3));
+ assertEquals(3, set.getSources(key3).size());
+ assertTrue(set.getSources(key1).containsAll(Arrays.asList(source1, source2, source3)));
+
+ WeakReference<Key<Integer>> weakKey1Ref = new WeakReference<Key<Integer>>(key1);
+ WeakReference<Key<Integer>> weakKey2Ref = new WeakReference<Key<Integer>>(key2);
+ WeakReference<Key<Integer>> weakKey3Ref = new WeakReference<Key<Integer>>(key3);
+ WeakReference<Object> weakSource1Ref = new WeakReference<Object>(source1);
+ WeakReference<Object> weakSource2Ref = new WeakReference<Object>(source2);
+ WeakReference<Object> weakSource3Ref = new WeakReference<Object>(source3);
+
+ Key<Integer> key = key1 = key2 = key3 = Key.get(Integer.class);
+ state1 = null;
+
+ GcFinalization.awaitFullGc();
+
+ assertTrue(set.contains(key));
+ assertEquals(2, set.getSources(key).size());
+ assertTrue(set.getSources(key).containsAll(Arrays.asList(source2, source3)));
+
+ source1 = null;
+ // Key1 will be referenced as the key in the sources backingSet and won't be
+ // GC'd.
+ GcFinalization.awaitClear(weakSource1Ref);
+
+ state2 = null;
+ GcFinalization.awaitFullGc();
+
+ assertTrue(set.contains(key));
+ assertEquals(1, set.getSources(key).size());
+ assertTrue(set.getSources(key).contains(source3));
+
+ GcFinalization.awaitClear(weakKey2Ref);
+
+ source2 = null;
+ GcFinalization.awaitClear(weakSource2Ref);
+ // Key1 will be referenced as the key in the sources backingSet and won't be
+ // GC'd.
+
+ state3 = null;
+ GcFinalization.awaitFullGc();
+
+ assertFalse(set.contains(Key.get(Integer.class)));
+ assertNull(set.getSources(Key.get(Integer.class)));
+
+ GcFinalization.awaitClear(weakKey3Ref);
+ source3 = null;
+ GcFinalization.awaitClear(weakSource3Ref);
+ // Now that the backing set is emptied, key1 is released.
+ GcFinalization.awaitClear(weakKey1Ref);
+ }
+
+ public void testWeakKeySet_integration() {
+ Injector parentInjector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(Integer.class).toInstance(4);
+ }
+ });
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+
+ Injector childInjector = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("bar");
+ }
+ });
+ WeakReference<Injector> weakRef = new WeakReference<Injector>(childInjector);
+ assertBlacklisted(parentInjector, Key.get(String.class));
+
+ // Clear the ref, GC, and ensure that we are no longer blacklisting.
+ childInjector = null;
+ GcFinalization.awaitClear(weakRef);
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+ }
+
+ public void testWeakKeySet_integration_multipleChildren() {
+ Injector parentInjector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(Integer.class).toInstance(4);
+ }
+ });
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+ assertNotBlacklisted(parentInjector, Key.get(Long.class));
+
+ Injector childInjector1 = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("foo");
+ }
+ });
+ WeakReference<Injector> weakRef1 = new WeakReference<Injector>(childInjector1);
+ assertBlacklisted(parentInjector, Key.get(String.class));
+ assertNotBlacklisted(parentInjector, Key.get(Long.class));
+
+ Injector childInjector2 = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(Long.class).toInstance(6L);
+ }
+ });
+ WeakReference<Injector> weakRef2 = new WeakReference<Injector>(childInjector2);
+ assertBlacklisted(parentInjector, Key.get(String.class));
+ assertBlacklisted(parentInjector, Key.get(Long.class));
+
+ // Clear ref1, GC, and ensure that we still blacklist.
+ childInjector1 = null;
+ GcFinalization.awaitClear(weakRef1);
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+ assertBlacklisted(parentInjector, Key.get(Long.class));
+
+ // Clear the ref, GC, and ensure that we are no longer blacklisting.
+ childInjector2 = null;
+ GcFinalization.awaitClear(weakRef2);
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+ assertNotBlacklisted(parentInjector, Key.get(Long.class));
+ }
+
+ public void testWeakKeySet_integration_multipleChildren_overlappingKeys() {
+ Injector parentInjector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(Integer.class).toInstance(4);
+ }
+ });
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+
+ Injector childInjector1 = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("foo");
+ }
+ });
+ WeakReference<Injector> weakRef1 = new WeakReference<Injector>(childInjector1);
+ assertBlacklisted(parentInjector, Key.get(String.class));
+
+ Injector childInjector2 = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("bar");
+ }
+ });
+ WeakReference<Injector> weakRef2 = new WeakReference<Injector>(childInjector2);
+ assertBlacklisted(parentInjector, Key.get(String.class));
+
+ // Clear ref1, GC, and ensure that we still blacklist.
+ childInjector1 = null;
+ GcFinalization.awaitClear(weakRef1);
+ assertBlacklisted(parentInjector, Key.get(String.class));
+
+ // Clear the ref, GC, and ensure that we are no longer blacklisting.
+ childInjector2 = null;
+ GcFinalization.awaitClear(weakRef2);
+ assertNotBlacklisted(parentInjector, Key.get(String.class));
+ }
+
+ private static void assertBlacklisted(Injector injector, Key<?> key) {
+ assertBlacklistState(injector, key, true);
+ }
+
+ private static void assertNotBlacklisted(Injector injector, Key<?> key) {
+ assertBlacklistState(injector, key, false);
+ }
+
+ private static void assertBlacklistState(Injector injector, Key<?> key, boolean isBlacklisted) {
+ assertEquals(isBlacklisted, ((InjectorImpl) injector).state.isBlacklisted(key));
+ }
+
+ private static class TestState implements State {
+ public State parent() {
+ return new TestState();
+ }
+
+ public <T> BindingImpl<T> getExplicitBinding(Key<T> key) {
+ return null;
+ }
+
+ public Map<Key<?>, Binding<?>> getExplicitBindingsThisLevel() {
+ throw new UnsupportedOperationException();
+ }
+
+ public void putBinding(Key<?> key, BindingImpl<?> binding) {
+ throw new UnsupportedOperationException();
+ }
+
+ public ScopeBinding getScopeBinding(Class<? extends Annotation> scopingAnnotation) {
+ return null;
+ }
+
+ public void putScopeBinding(Class<? extends Annotation> annotationType, ScopeBinding scope) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void addConverter(TypeConverterBinding typeConverterBinding) {
+ throw new UnsupportedOperationException();
+ }
+
+ public TypeConverterBinding getConverter(String stringValue, TypeLiteral<?> type, Errors errors,
+ Object source) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Iterable<TypeConverterBinding> getConvertersThisLevel() {
+ return ImmutableSet.of();
+ }
+
+ /*if[AOP]*/
+ public void addMethodAspect(MethodAspect methodAspect) {
+ throw new UnsupportedOperationException();
+ }
+
+ public ImmutableList<MethodAspect> getMethodAspects() {
+ return ImmutableList.of();
+ }
+ /*end[AOP]*/
+
+ public void addTypeListener(TypeListenerBinding typeListenerBinding) {
+ throw new UnsupportedOperationException();
+ }
+
+ public List<TypeListenerBinding> getTypeListenerBindings() {
+ return ImmutableList.of();
+ }
+
+ public void addProvisionListener(ProvisionListenerBinding provisionListenerBinding) {
+ throw new UnsupportedOperationException();
+ }
+
+ public List<ProvisionListenerBinding> getProvisionListenerBindings() {
+ return ImmutableList.of();
+ }
+
+ public void blacklist(Key<?> key, State state, Object source) {
+ }
+
+ public boolean isBlacklisted(Key<?> key) {
+ return true;
+ }
+
+ public Set<Object> getSourcesForBlacklistedKey(Key<?> key) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Object lock() {
+ throw new UnsupportedOperationException();
+ }
+
+ public Map<Class<? extends Annotation>, Scope> getScopes() {
+ return ImmutableMap.of();
+ }
+ }
+}
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java b/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
index ba0671d..70be342 100644
--- a/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
+++ b/extensions/multibindings/test/com/google/inject/multibindings/MapBinderTest.java
@@ -31,6 +31,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
+import com.google.common.testing.GcFinalization;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.BindingAnnotation;
@@ -60,6 +61,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
@@ -871,7 +873,7 @@
expected.put(1, 1);
expected.put(2, 2);
assertEquals(expected, s1);
- }
+ }
public void testTwoMapBindersAreDistinct() {
Injector injector = Guice.createInjector(new AbstractModule() {
@@ -908,4 +910,32 @@
assertFalse(map2Binding.containsElement(a));
assertTrue(map2Binding.containsElement(b));
}
+
+ // Tests for com.google.inject.internal.WeakKeySet not leaking memory.
+ public void testWeakKeySet_integration_mapbinder() {
+ Key<Map<String, String>> mapKey = Key.get(new TypeLiteral<Map<String, String>>() {});
+
+ Injector parentInjector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("hi");
+ }
+ });
+ WeakKeySetUtils.assertNotBlacklisted(parentInjector, mapKey);
+
+ Injector childInjector = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ MapBinder<String, String> binder =
+ MapBinder.newMapBinder(binder(), String.class, String.class);
+ binder.addBinding("bar").toInstance("foo");
+ }
+ });
+ WeakReference<Injector> weakRef = new WeakReference<Injector>(childInjector);
+ WeakKeySetUtils.assertBlacklisted(parentInjector, mapKey);
+
+ // Clear the ref, GC, and ensure that we are no longer blacklisting.
+ childInjector = null;
+
+ GcFinalization.awaitClear(weakRef);
+ WeakKeySetUtils.assertNotBlacklisted(parentInjector, mapKey);
+ }
}
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java b/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java
index 9494e5e..d317525 100644
--- a/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java
+++ b/extensions/multibindings/test/com/google/inject/multibindings/MultibinderTest.java
@@ -34,6 +34,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import com.google.common.testing.GcFinalization;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.BindingAnnotation;
@@ -72,6 +73,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;
@@ -1129,4 +1131,31 @@
assertFalse(collector.optionalbinding.containsElement(b));
assertTrue(collector.optionalbinding.containsElement(c));
}
+
+ // Tests for com.google.inject.internal.WeakKeySet not leaking memory.
+ public void testWeakKeySet_integration_multibinder() {
+ Key<Set<String>> setKey = Key.get(new TypeLiteral<Set<String>>() {});
+
+ Injector parentInjector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("hi");
+ }
+ });
+ WeakKeySetUtils.assertNotBlacklisted(parentInjector, setKey);
+
+ Injector childInjector = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ Multibinder<String> binder = Multibinder.newSetBinder(binder(), String.class);
+ binder.addBinding().toInstance("foo");
+ }
+ });
+ WeakReference<Injector> weakRef = new WeakReference<Injector>(childInjector);
+ WeakKeySetUtils.assertBlacklisted(parentInjector, setKey);
+
+ // Clear the ref, GC, and ensure that we are no longer blacklisting.
+ childInjector = null;
+
+ GcFinalization.awaitClear(weakRef);
+ WeakKeySetUtils.assertNotBlacklisted(parentInjector, setKey);
+ }
}
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/OptionalBinderTest.java b/extensions/multibindings/test/com/google/inject/multibindings/OptionalBinderTest.java
index 1393e0c..246c7a3 100644
--- a/extensions/multibindings/test/com/google/inject/multibindings/OptionalBinderTest.java
+++ b/extensions/multibindings/test/com/google/inject/multibindings/OptionalBinderTest.java
@@ -29,6 +29,7 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
+import com.google.common.testing.GcFinalization;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.BindingAnnotation;
@@ -59,6 +60,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map.Entry;
@@ -911,6 +913,30 @@
assertEquals(2, i2.intValue());
}
+ // Tests for com.google.inject.internal.WeakKeySet not leaking memory.
+ public void testWeakKeySet_integration() {
+ Injector parentInjector = Guice.createInjector(new AbstractModule() {
+ @Override protected void configure() {
+ bind(String.class).toInstance("hi");
+ }
+ });
+ WeakKeySetUtils.assertNotBlacklisted(parentInjector, Key.get(Integer.class));
+
+ Injector childInjector = parentInjector.createChildInjector(new AbstractModule() {
+ @Override protected void configure() {
+ OptionalBinder.newOptionalBinder(binder(), Integer.class).setDefault().toInstance(4);
+ }
+ });
+ WeakReference<Injector> weakRef = new WeakReference<Injector>(childInjector);
+ WeakKeySetUtils.assertBlacklisted(parentInjector, Key.get(Integer.class));
+
+ // Clear the ref, GC, and ensure that we are no longer blacklisting.
+ childInjector = null;
+
+ GcFinalization.awaitClear(weakRef);
+ WeakKeySetUtils.assertNotBlacklisted(parentInjector, Key.get(Integer.class));
+ }
+
@SuppressWarnings("unchecked")
private <V> Set<V> setOf(V... elements) {
return ImmutableSet.copyOf(elements);
diff --git a/extensions/multibindings/test/com/google/inject/multibindings/WeakKeySetUtils.java b/extensions/multibindings/test/com/google/inject/multibindings/WeakKeySetUtils.java
new file mode 100644
index 0000000..0dc5a8e
--- /dev/null
+++ b/extensions/multibindings/test/com/google/inject/multibindings/WeakKeySetUtils.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (C) 2014 Google Inc.
+ *
+ * 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.google.inject.multibindings;
+
+import com.google.inject.Injector;
+import com.google.inject.Key;
+
+import junit.framework.TestCase;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+/**
+ * Utilities for verifying com.google.inject.internal.WeakKeySet is not leaking memory.
+ *
+ * @author [email protected] (Daniel Weis)
+ */
+final class WeakKeySetUtils {
+
+ private WeakKeySetUtils() {}
+
+ static void assertBlacklisted(Injector injector, Key<?> key) {
+ assertBlacklistState(injector, key, true);
+ }
+
+ static void assertNotBlacklisted(Injector injector, Key<?> key) {
+ assertBlacklistState(injector, key, false);
+ }
+
+ private static final Field stateField;
+ private static final Method isBlacklistedMethod;
+ static {
+ try {
+ stateField =
+ Class.forName("com.google.inject.internal.InjectorImpl").getDeclaredField("state");
+ stateField.setAccessible(true);
+ isBlacklistedMethod =
+ Class.forName("com.google.inject.internal.State").getMethod("isBlacklisted", Key.class);
+ isBlacklistedMethod.setAccessible(true);
+ } catch (Exception e) {
+ throw new Error(e);
+ }
+ }
+
+ private static void assertBlacklistState(Injector injector, Key<?> key, boolean isBlacklisted) {
+ try {
+ TestCase.assertEquals(
+ isBlacklisted,
+ ((Boolean) isBlacklistedMethod.invoke(stateField.get(injector), key)).booleanValue());
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 1298e5d..55ee9b8 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -55,6 +55,16 @@
<scope>test</scope>
</dependency>
<!--
+ | Some extension tests depend on guava test libs which are not inherited
+ | from test scope.
+ -->
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava-testlib</artifactId>
+ <version>16.0.1</version>
+ <scope>test</scope>
+ </dependency>
+ <!--
| Some extension tests depend on cglib which is not embedded
| in an execution that doesn't include package.
-->
diff --git a/lib/build/guava-testlib-16.0.1.jar b/lib/build/guava-testlib-16.0.1.jar
new file mode 100644
index 0000000..59150aa
--- /dev/null
+++ b/lib/build/guava-testlib-16.0.1.jar
Binary files differ