Skip to content

Commit 15d95bb

Browse files
samebGuice Team
authored and
Guice Team
committed
Fix issues with recursive JIT loading that would lead to a deadlock and/or crash inside Guava's LoadingCache. The problem had to do with the fact that Guice eagerly stores uninitialized JIT bindings in an attempt to allow circular dependencies, and Guice also attempts to cleanup failed JIT bindings (to remove these uninitialized JIT bindings). The JIT binding cache was a guard against a recursive call back into the constructor's cache, but we were removing that guard.
We now only remove the guard if we aren't in the process of loading that JIT binding. The failed JIT binding is naturally cleaned up later on, as the existing & new tests attest to. Fixes many issues: - Fixes #1633 - Fixes #785 - Fixes #1389 - Fixes #1394 Many thanks to @swankjesse for the test-case added in #1389 that was helpful in diagnosing the problem, and to @PaulFridrick for the diagnoses in #1633. PiperOrigin-RevId: 525219839
1 parent f447cc4 commit 15d95bb

File tree

4 files changed

+127
-6
lines changed

4 files changed

+127
-6
lines changed

core/src/com/google/inject/internal/ConstructorInjectorStore.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package com.google.inject.internal;
1818

19-
2019
import com.google.common.collect.ImmutableList;
2120
import com.google.common.collect.Lists;
2221
import com.google.inject.spi.InjectionPoint;
@@ -43,6 +42,11 @@ protected ConstructorInjector<?> create(InjectionPoint constructorInjector, Erro
4342
this.injector = injector;
4443
}
4544

45+
/** Returns true if the store is in the process of loading this injection point. */
46+
boolean isLoading(InjectionPoint ip) {
47+
return cache.isLoading(ip);
48+
}
49+
4650
/** Returns a new complete constructor injector with injection listeners registered. */
4751
public ConstructorInjector<?> get(InjectionPoint constructorInjector, Errors errors)
4852
throws ErrorsException {

core/src/com/google/inject/internal/FailableCache.java

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import com.google.common.collect.ImmutableMap;
2323
import com.google.common.collect.Maps;
2424
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.concurrent.ConcurrentHashMap;
2527

2628
/**
2729
* Lazily creates (and caches) values for keys. If creating the value fails (with errors), an
@@ -31,18 +33,23 @@
3133
*/
3234
public abstract class FailableCache<K, V> {
3335

36+
private final Set<K> loadingSet = ConcurrentHashMap.newKeySet();
37+
3438
private final LoadingCache<K, Object> delegate =
3539
CacheBuilder.newBuilder()
3640
.build(
3741
new CacheLoader<K, Object>() {
3842
@Override
3943
public Object load(K key) {
44+
loadingSet.add(key);
4045
Errors errors = new Errors();
4146
V result = null;
4247
try {
4348
result = FailableCache.this.create(key, errors);
4449
} catch (ErrorsException e) {
4550
errors.merge(e.getErrors());
51+
} finally {
52+
loadingSet.remove(key);
4653
}
4754
return errors.hasErrors() ? errors : result;
4855
}
@@ -66,6 +73,10 @@ boolean remove(K key) {
6673
return delegate.asMap().remove(key) != null;
6774
}
6875

76+
boolean isLoading(K key) {
77+
return loadingSet.contains(key);
78+
}
79+
6980
Map<K, V> asMap() {
7081
return Maps.transformValues(
7182
Maps.filterValues(

core/src/com/google/inject/internal/InjectorImpl.java

+19-5
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ private <T> BindingImpl<T> getJustInTimeBinding(Key<T> key, Errors errors, JitLi
264264
// first try to find a JIT binding that we've already created
265265
for (InjectorImpl injector = this; injector != null; injector = injector.parent) {
266266
@SuppressWarnings("unchecked") // we only store bindings that match their key
267-
BindingImpl<T> binding = (BindingImpl<T>) injector.jitBindingData.getJitBindings().get(key);
267+
BindingImpl<T> binding = (BindingImpl<T>) injector.jitBindingData.getJitBinding(key);
268268

269269
if (binding != null) {
270270
// If we found a JIT binding and we don't allow them,
@@ -656,12 +656,26 @@ private boolean cleanup(BindingImpl<?> binding, Set<Key<?>> encountered) {
656656
/** Cleans up any state that may have been cached when constructing the JIT binding. */
657657
private void removeFailedJitBinding(Binding<?> binding, InjectionPoint ip) {
658658
jitBindingData.addFailedJitBinding(binding.getKey());
659-
jitBindingData.removeJitBinding(binding.getKey());
660-
membersInjectorStore.remove(binding.getKey().getTypeLiteral());
661-
provisionListenerStore.remove(binding);
662-
if (ip != null) {
659+
// Be careful cleaning up constructors & jitBindings -- we can't remove
660+
// from `jitBindings` if we're still in the process of loading this constructor,
661+
// otherwise we can re-enter the constructor's cache and attempt to load it
662+
// while already loading it. See issues:
663+
// - https://github.com/google/guice/pull/1633
664+
// - https://github.com/google/guice/issues/785
665+
// - https://github.com/google/guice/pull/1389
666+
// - https://github.com/google/guice/pull/1394
667+
// (Note: there may be a better way to do this that avoids the need for the `isLoading`
668+
// conditional, but due to the recursive nature of JIT loading and the way we allow partially
669+
// initialized JIT bindings [to support circular dependencies], there's no other great way
670+
// that I could figure out.)
671+
if (ip == null || !constructors.isLoading(ip)) {
672+
jitBindingData.removeJitBinding(binding.getKey());
673+
}
674+
if (ip != null && !constructors.isLoading(ip)) {
663675
constructors.remove(ip);
664676
}
677+
membersInjectorStore.remove(binding.getKey().getTypeLiteral());
678+
provisionListenerStore.remove(binding);
665679
}
666680

667681
/** Safely gets the dependencies of possibly not initialized bindings. */

core/test/com/google/inject/ImplicitBindingTest.java

+92
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616

1717
package com.google.inject;
1818

19+
import static com.google.common.collect.Iterables.getOnlyElement;
1920
import static com.google.common.truth.Truth.assertThat;
21+
import static org.junit.Assert.assertThrows;
2022

2123
import com.google.common.collect.Iterables;
2224
import com.google.inject.internal.Annotations;
@@ -439,4 +441,94 @@ public void testImplicitJdkBindings_publicCxtor() {
439441
// String has a public nullary constructor, so Guice will call it.
440442
assertEquals("", injector.getInstance(String.class));
441443
}
444+
445+
public void testRecursiveLoadWithOptionals() {
446+
Injector injector =
447+
Guice.createInjector(
448+
new AbstractModule() {
449+
@Override
450+
protected void configure() {
451+
bind(A1.class);
452+
}
453+
});
454+
assertThat(injector.getExistingBinding(Key.get(D1.class))).isNull();
455+
assertThat(injector.getExistingBinding(Key.get(Unresolved.class))).isNull();
456+
}
457+
458+
static class A1 {
459+
@Inject B1 b;
460+
}
461+
462+
static class B1 {
463+
@Inject C1 c;
464+
}
465+
466+
static class C1 {
467+
@Inject(optional = true)
468+
D1 d;
469+
470+
@Inject E1 e;
471+
}
472+
473+
static class D1 {
474+
@Inject B1 b;
475+
@Inject Unresolved unresolved;
476+
}
477+
478+
static class E1 {
479+
@Inject B1 b;
480+
}
481+
482+
public void testRecursiveLoadWithoutOptionals_atInjectorCreation() {
483+
CreationException ce =
484+
assertThrows(
485+
CreationException.class,
486+
() ->
487+
Guice.createInjector(
488+
new AbstractModule() {
489+
@Provides
490+
public V provideV(Z z) {
491+
return null;
492+
}
493+
}));
494+
assertThat(ce.getErrorMessages()).hasSize(1);
495+
assertThat(getOnlyElement(ce.getErrorMessages()).getMessage())
496+
.contains("No implementation for " + Unresolved.class.getName() + " was bound.");
497+
}
498+
499+
public void testRecursiveLoadWithoutOptionals_afterCreation() {
500+
Injector injector = Guice.createInjector();
501+
ConfigurationException ce =
502+
assertThrows(ConfigurationException.class, () -> injector.getBinding(Z.class));
503+
assertThat(ce.getErrorMessages()).hasSize(1);
504+
assertThat(getOnlyElement(ce.getErrorMessages()).getMessage())
505+
.contains("No implementation for " + Unresolved.class.getName() + " was bound.");
506+
assertThat(injector.getExistingBinding(Key.get(Z.class))).isNull();
507+
assertThat(injector.getExistingBinding(Key.get(Y.class))).isNull();
508+
assertThat(injector.getExistingBinding(Key.get(X.class))).isNull();
509+
assertThat(injector.getExistingBinding(Key.get(W.class))).isNull();
510+
assertThat(injector.getExistingBinding(Key.get(Unresolved.class))).isNull();
511+
}
512+
513+
static class V {}
514+
515+
static class X {
516+
@Inject Z z;
517+
}
518+
519+
static class Z {
520+
@Inject W w;
521+
@Inject X x;
522+
}
523+
524+
static class W {
525+
@Inject Y y;
526+
@Inject Z z;
527+
}
528+
529+
static class Y {
530+
@Inject Unresolved unresolved;
531+
}
532+
533+
interface Unresolved {}
442534
}

0 commit comments

Comments
 (0)