Skip to content

Commit f201907

Browse files
authored
Fix deadlock issue. (#2235)
* Fix deadlock issue. The deadlock was arising when components are initialized concurrently from different threads all waiting for `Lazy.get` to resolve which would never succeed. * Increase timeout, add licenses. * Added comments.
1 parent 4b85eb8 commit f201907

File tree

4 files changed

+243
-46
lines changed

4 files changed

+243
-46
lines changed

firebase-components/src/main/java/com/google/firebase/components/ComponentRuntime.java

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import java.util.Map;
3535
import java.util.Set;
3636
import java.util.concurrent.Executor;
37+
import java.util.concurrent.atomic.AtomicReference;
3738

3839
/**
3940
* Main entry point to the component system.
@@ -48,7 +49,7 @@ public class ComponentRuntime extends AbstractComponentContainer implements Comp
4849
private final Map<Class<?>, LazySet<?>> lazySetMap = new HashMap<>();
4950
private final List<Provider<ComponentRegistrar>> unprocessedRegistrarProviders;
5051
private final EventBus eventBus;
51-
private Boolean eagerComponentsInitializedWith = null;
52+
private final AtomicReference<Boolean> eagerComponentsInitializedWith = new AtomicReference<>();
5253

5354
/**
5455
* Creates an instance of {@link ComponentRuntime} for the provided {@link ComponentRegistrar}s
@@ -90,49 +91,63 @@ private ComponentRuntime(
9091
discoverComponents(componentsToAdd);
9192
}
9293

93-
private synchronized void discoverComponents(List<Component<?>> componentsToAdd) {
94-
95-
Iterator<Provider<ComponentRegistrar>> iterator = unprocessedRegistrarProviders.iterator();
96-
while (iterator.hasNext()) {
97-
Provider<ComponentRegistrar> provider = iterator.next();
98-
try {
99-
ComponentRegistrar registrar = provider.get();
100-
if (registrar != null) {
101-
componentsToAdd.addAll(registrar.getComponents());
94+
private void discoverComponents(List<Component<?>> componentsToAdd) {
95+
// During discovery many things need to happen, of which "Deferred resolution" and "Set binding
96+
// updates" are of most interest. During these phases we execute "client" code(i.e. the code of
97+
// SDKs participating in Components DI), this code can indirectly end up calling back into the
98+
// ComponentRuntime which, without proper care, can result in a deadlock. For this reason,
99+
// instead of executing such code in the synchronized block below, we store it in a list and
100+
// execute right after the synchronized section.
101+
List<Runnable> runAfterDiscovery = new ArrayList<>();
102+
synchronized (this) {
103+
Iterator<Provider<ComponentRegistrar>> iterator = unprocessedRegistrarProviders.iterator();
104+
while (iterator.hasNext()) {
105+
Provider<ComponentRegistrar> provider = iterator.next();
106+
try {
107+
ComponentRegistrar registrar = provider.get();
108+
if (registrar != null) {
109+
componentsToAdd.addAll(registrar.getComponents());
110+
iterator.remove();
111+
}
112+
} catch (InvalidRegistrarException ex) {
102113
iterator.remove();
114+
Log.w(ComponentDiscovery.TAG, "Invalid component registrar.", ex);
103115
}
104-
} catch (InvalidRegistrarException ex) {
105-
iterator.remove();
106-
Log.w(ComponentDiscovery.TAG, "Invalid component registrar.", ex);
107116
}
108-
}
109117

110-
if (components.isEmpty()) {
111-
CycleDetector.detect(componentsToAdd);
112-
} else {
113-
ArrayList<Component<?>> allComponents = new ArrayList<>(this.components.keySet());
114-
allComponents.addAll(componentsToAdd);
115-
CycleDetector.detect(allComponents);
116-
}
118+
if (components.isEmpty()) {
119+
CycleDetector.detect(componentsToAdd);
120+
} else {
121+
ArrayList<Component<?>> allComponents = new ArrayList<>(this.components.keySet());
122+
allComponents.addAll(componentsToAdd);
123+
CycleDetector.detect(allComponents);
124+
}
117125

118-
for (Component<?> component : componentsToAdd) {
119-
Lazy<?> lazy =
120-
new Lazy<>(
121-
() ->
122-
component.getFactory().create(new RestrictedComponentContainer(component, this)));
126+
for (Component<?> component : componentsToAdd) {
127+
Lazy<?> lazy =
128+
new Lazy<>(
129+
() ->
130+
component
131+
.getFactory()
132+
.create(new RestrictedComponentContainer(component, this)));
123133

124-
components.put(component, lazy);
125-
}
134+
components.put(component, lazy);
135+
}
126136

127-
processInstanceComponents(componentsToAdd);
128-
processSetComponents();
129-
processDependencies();
137+
runAfterDiscovery.addAll(processInstanceComponents(componentsToAdd));
138+
runAfterDiscovery.addAll(processSetComponents());
139+
processDependencies();
140+
}
141+
for (Runnable runnable : runAfterDiscovery) {
142+
runnable.run();
143+
}
130144
maybeInitializeEagerComponents();
131145
}
132146

133147
private void maybeInitializeEagerComponents() {
134-
if (eagerComponentsInitializedWith != null) {
135-
doInitializeEagerComponents(eagerComponentsInitializedWith);
148+
Boolean isDefaultApp = eagerComponentsInitializedWith.get();
149+
if (isDefaultApp != null) {
150+
doInitializeEagerComponents(components, isDefaultApp);
136151
}
137152
}
138153

@@ -153,7 +168,8 @@ private static <T> List<T> iterableToList(Iterable<T> iterable) {
153168
return result;
154169
}
155170

156-
private void processInstanceComponents(List<Component<?>> componentsToProcess) {
171+
private List<Runnable> processInstanceComponents(List<Component<?>> componentsToProcess) {
172+
ArrayList<Runnable> runnables = new ArrayList<>();
157173
for (Component<?> component : componentsToProcess) {
158174
if (!component.isValue()) {
159175
continue;
@@ -169,14 +185,16 @@ private void processInstanceComponents(List<Component<?>> componentsToProcess) {
169185
OptionalProvider<Object> deferred = (OptionalProvider<Object>) existingProvider;
170186
@SuppressWarnings("unchecked")
171187
Provider<Object> castedProvider = (Provider<Object>) provider;
172-
deferred.set(castedProvider);
188+
runnables.add(() -> deferred.set(castedProvider));
173189
}
174190
}
175191
}
192+
return runnables;
176193
}
177194

178195
/** Populates lazySetMap to make set components available for consumption via set dependencies. */
179-
private void processSetComponents() {
196+
private List<Runnable> processSetComponents() {
197+
ArrayList<Runnable> runnables = new ArrayList<>();
180198
Map<Class<?>, Set<Provider<?>>> setIndex = new HashMap<>();
181199
for (Map.Entry<Component<?>, Provider<?>> entry : components.entrySet()) {
182200
Component<?> component = entry.getKey();
@@ -202,10 +220,11 @@ private void processSetComponents() {
202220
} else {
203221
LazySet<Object> existingSet = (LazySet<Object>) lazySetMap.get(entry.getKey());
204222
for (Provider<?> provider : entry.getValue()) {
205-
existingSet.add((Provider<Object>) provider);
223+
runnables.add(() -> existingSet.add((Provider<Object>) provider));
206224
}
207225
}
208226
}
227+
return runnables;
209228
}
210229

211230
@Override
@@ -243,17 +262,28 @@ public synchronized <T> Provider<Set<T>> setOfProvider(Class<T> anInterface) {
243262
* <p>Should be called at an appropriate time in the owner's lifecycle.
244263
*
245264
* <p>Note: the method is idempotent.
265+
*
266+
* <p>Warning: it's important that this method is not synchronized as it could cause a deadlock if
267+
* another SDK is initializing in another thread.
246268
*/
247-
public synchronized void initializeEagerComponents(boolean isDefaultApp) {
248-
if (eagerComponentsInitializedWith != null) {
269+
public void initializeEagerComponents(boolean isDefaultApp) {
270+
if (!eagerComponentsInitializedWith.compareAndSet(null, isDefaultApp)) {
249271
return;
250272
}
251-
eagerComponentsInitializedWith = isDefaultApp;
252-
doInitializeEagerComponents(isDefaultApp);
273+
274+
// we copy the map under a lock to avoid a race condition.
275+
// Note that we cannot use a ConcurrentHashMap as it is broken on older Android versions, see:
276+
// https://issuetracker.google.com/issues/37042460
277+
HashMap<Component<?>, Provider<?>> componentsCopy;
278+
synchronized (this) {
279+
componentsCopy = new HashMap<>(components);
280+
}
281+
doInitializeEagerComponents(componentsCopy, isDefaultApp);
253282
}
254283

255-
private void doInitializeEagerComponents(boolean isDefaultApp) {
256-
for (Map.Entry<Component<?>, Provider<?>> entry : components.entrySet()) {
284+
private void doInitializeEagerComponents(
285+
Map<Component<?>, Provider<?>> componentsToInitialize, boolean isDefaultApp) {
286+
for (Map.Entry<Component<?>, Provider<?>> entry : componentsToInitialize.entrySet()) {
257287
Component<?> component = entry.getKey();
258288
Provider<?> provider = entry.getValue();
259289

@@ -266,9 +296,11 @@ private void doInitializeEagerComponents(boolean isDefaultApp) {
266296
}
267297

268298
@Override
269-
public synchronized void discoverComponents() {
270-
if (unprocessedRegistrarProviders.isEmpty()) {
271-
return;
299+
public void discoverComponents() {
300+
synchronized (this) {
301+
if (unprocessedRegistrarProviders.isEmpty()) {
302+
return;
303+
}
272304
}
273305
discoverComponents(new ArrayList<>());
274306
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.components.threading;
16+
17+
public class AnotherEagerComponent {}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.components.threading;
16+
17+
import static org.mockito.Mockito.mock;
18+
import static org.mockito.Mockito.when;
19+
20+
import androidx.test.ext.junit.runners.AndroidJUnit4;
21+
import com.google.firebase.components.Component;
22+
import com.google.firebase.components.ComponentRegistrar;
23+
import com.google.firebase.components.ComponentRuntime;
24+
import com.google.firebase.components.Dependency;
25+
import com.google.firebase.inject.Provider;
26+
import java.util.Collections;
27+
import java.util.concurrent.CountDownLatch;
28+
import java.util.concurrent.Executors;
29+
import org.junit.Test;
30+
import org.junit.runner.RunWith;
31+
32+
@RunWith(AndroidJUnit4.class)
33+
public class DeadlockTests {
34+
@Test(timeout = 2000)
35+
public void concurrentInitialization_withValueComponents_shouldNotDeadlock() {
36+
ComponentRuntime runtime =
37+
ComponentRuntime.builder(Runnable::run)
38+
.addComponent(
39+
Component.builder(EagerComponent.class)
40+
.add(Dependency.requiredProvider(AnotherEagerComponent.class))
41+
.factory(
42+
c -> {
43+
Provider<AnotherEagerComponent> another =
44+
c.getProvider(AnotherEagerComponent.class);
45+
CountDownLatch latch = new CountDownLatch(1);
46+
Executors.newSingleThreadExecutor()
47+
.execute(
48+
() -> {
49+
another.get();
50+
latch.countDown();
51+
});
52+
try {
53+
latch.await();
54+
} catch (InterruptedException e) {
55+
Thread.currentThread().interrupt();
56+
throw new AssertionError("Deadlock detected.", e);
57+
}
58+
return new EagerComponent();
59+
})
60+
.alwaysEager()
61+
.build())
62+
.addComponent(
63+
Component.builder(AnotherEagerComponent.class)
64+
.add(Dependency.optionalProvider(String.class))
65+
.factory(
66+
c -> {
67+
c.getProvider(String.class);
68+
return new AnotherEagerComponent();
69+
})
70+
.alwaysEager()
71+
.build())
72+
.build();
73+
runtime.initializeEagerComponents(true);
74+
}
75+
76+
@Test(timeout = 2000)
77+
public void concurrentInitialization_withDeferredComponents_shouldNotDeadlock()
78+
throws InterruptedException {
79+
CountDownLatch stringDeferredInjected = new CountDownLatch(1);
80+
@SuppressWarnings("unchecked")
81+
Provider<ComponentRegistrar> missingRegistrar = mock(Provider.class);
82+
ComponentRuntime runtime =
83+
ComponentRuntime.builder(Runnable::run)
84+
.addComponent(
85+
Component.builder(EagerComponent.class)
86+
.add(Dependency.deferred(String.class))
87+
.factory(
88+
c -> {
89+
c.getDeferred(String.class)
90+
.whenAvailable(
91+
p -> {
92+
p.get();
93+
stringDeferredInjected.countDown();
94+
});
95+
return new EagerComponent();
96+
})
97+
.alwaysEager()
98+
.build())
99+
.addLazyComponentRegistrars(Collections.singleton(missingRegistrar))
100+
.build();
101+
runtime.initializeEagerComponents(true);
102+
103+
when(missingRegistrar.get())
104+
.thenReturn(
105+
() ->
106+
Collections.singletonList(
107+
Component.builder(String.class)
108+
.add(Dependency.optionalProvider(Double.class))
109+
.factory(
110+
c -> {
111+
CountDownLatch latch = new CountDownLatch(1);
112+
Executors.newSingleThreadExecutor()
113+
.execute(
114+
() -> {
115+
c.getProvider(Double.class);
116+
latch.countDown();
117+
});
118+
try {
119+
latch.await();
120+
} catch (InterruptedException e) {
121+
Thread.currentThread().interrupt();
122+
throw new AssertionError("Deadlock detected", e);
123+
}
124+
return "Hello";
125+
})
126+
.build()));
127+
128+
runtime.discoverComponents();
129+
stringDeferredInjected.await();
130+
}
131+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2019 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.components.threading;
16+
17+
public class EagerComponent {}

0 commit comments

Comments
 (0)