Make TurboModule creation thread-safe

Summary:
NativeModules can be created from any number of threads. In the legacy system, `ModuleHolder`, the class responsible for creating NativeModules, has built-in concurrency control to ensure that NativeModule creation is thread-safe. This diff introduces that thread-safety to the TurboModule infra. Basically, after this diff, if `n` threads race to create a TurboModule x, only the first thread will create x. All other threads will wait until x is created.

Changelog:
[Android][Fixed] - Make TurboModule creation thread-safe

Reviewed By: mdvacca

Differential Revision: D20659799

fbshipit-source-id: 2b720fe1ea49e40ae0d6dae50d422f23a6f45520
This commit is contained in:
Ramanpreet Nara
2020-04-03 18:38:38 -07:00
committed by Facebook GitHub Bot
parent bd3868643d
commit d36fa783a9
2 changed files with 257 additions and 46 deletions
@@ -7,7 +7,10 @@
package com.facebook.react.turbomodule.core;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.infer.annotation.Assertions;
import com.facebook.jni.HybridData;
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.bridge.CxxModuleWrapper;
@@ -26,11 +29,20 @@ import java.util.*;
* a Java module, that the C++ counterpart calls.
*/
public class TurboModuleManager implements JSIModule, TurboModuleRegistry {
private final TurboModuleManagerDelegate mTurbomoduleManagerDelegate;
private static volatile boolean sIsSoLibraryLoaded;
private final List<String> mEagerInitModuleNames;
private final TurboModuleProvider mJavaModuleProvider;
private final TurboModuleProvider mCxxModuleProvider;
private final Map<String, TurboModule> mTurboModules = new HashMap<>();
// Prevents the creation of new TurboModules once cleanup as been initiated.
private final Object mTurboModuleCleanupLock = new Object();
@GuardedBy("mTurboModuleCleanupLock")
private boolean mTurboModuleCleanupStarted = false;
// List of TurboModules that have been, or are currently being, instantiated
@GuardedBy("mTurboModuleCleanupLock")
private final Map<String, TurboModuleHolder> mTurboModuleHolders = new HashMap<>();
@DoNotStrip
@SuppressWarnings("unused")
@@ -38,7 +50,7 @@ public class TurboModuleManager implements JSIModule, TurboModuleRegistry {
public TurboModuleManager(
JavaScriptContextHolder jsContext,
TurboModuleManagerDelegate tmmDelegate,
@Nullable final TurboModuleManagerDelegate delegate,
CallInvokerHolder jsCallInvokerHolder,
CallInvokerHolder nativeCallInvokerHolder) {
maybeLoadSoLibrary();
@@ -47,70 +59,212 @@ public class TurboModuleManager implements JSIModule, TurboModuleRegistry {
jsContext.get(),
(CallInvokerHolderImpl) jsCallInvokerHolder,
(CallInvokerHolderImpl) nativeCallInvokerHolder,
tmmDelegate);
mTurbomoduleManagerDelegate = tmmDelegate;
delegate);
installJSIBindings();
mEagerInitModuleNames =
delegate == null ? new ArrayList<String>() : delegate.getEagerInitModuleNames();
mJavaModuleProvider =
new TurboModuleProvider() {
@Nullable
public TurboModule getModule(String moduleName) {
if (delegate == null) {
return null;
}
return delegate.getModule(moduleName);
}
};
mCxxModuleProvider =
new TurboModuleProvider() {
@Nullable
public TurboModule getModule(String moduleName) {
if (delegate == null) {
return null;
}
CxxModuleWrapper nativeModule = delegate.getLegacyCxxModule(moduleName);
if (nativeModule != null) {
// TurboModuleManagerDelegate must always return TurboModules
Assertions.assertCondition(
nativeModule instanceof TurboModule,
"CxxModuleWrapper \"" + moduleName + "\" is not a TurboModule");
return (TurboModule) nativeModule;
}
return null;
}
};
}
public List<String> getEagerInitModuleNames() {
return mTurbomoduleManagerDelegate.getEagerInitModuleNames();
return mEagerInitModuleNames;
}
@DoNotStrip
/**
* TurboModuleHolders are used as locks to ensure that when n threads race to create a
* TurboModule, only the first thread creates that TurboModule. All other n - 1 threads wait until
* the TurboModule is created and initialized.
*/
@Nullable
private TurboModule getJavaModule(String name) {
if (!mTurboModules.containsKey(name)) {
final TurboModule turboModule = mTurbomoduleManagerDelegate.getModule(name);
private TurboModuleHolder getOrMaybeCreateTurboModuleHolder(String moduleName) {
synchronized (mTurboModuleCleanupLock) {
if (mTurboModuleCleanupStarted) {
/*
* Always return null after cleanup has started, so that getModule(moduleName) returns null.
*/
return null;
}
/*
* TODO(T64619790): Should we populate mJavaTurboModuleHolders ahead of time, to avoid having
* * to control concurrent access to it?
*/
if (!mTurboModuleHolders.containsKey(moduleName)) {
mTurboModuleHolders.put(moduleName, new TurboModuleHolder());
}
return mTurboModuleHolders.get(moduleName);
}
}
/**
* If n threads race to create TurboModule x, then only the first thread should create x. All n -
* 1 other threads should wait until x is created and initialized.
*
* <p>Note: After we've started cleanup, getModule will always return null.
*/
@Nullable
public TurboModule getModule(String moduleName) {
final TurboModuleHolder moduleHolder = getOrMaybeCreateTurboModuleHolder(moduleName);
if (moduleHolder == null) {
return null;
}
boolean shouldCreateModule = false;
synchronized (moduleHolder) {
if (moduleHolder.isDoneCreatingModule()) {
return moduleHolder.getModule();
}
if (!moduleHolder.isCreatingModule()) {
// Only one thread gets here
shouldCreateModule = true;
moduleHolder.startCreatingModule();
}
}
if (shouldCreateModule) {
TurboModule turboModule = mJavaModuleProvider.getModule(moduleName);
if (turboModule == null) {
turboModule = mCxxModuleProvider.getModule(moduleName);
}
if (turboModule != null) {
/**
* TurboModuleManager is initialized after ReactApplicationContext has been setup.
* Therefore, it's safe to call initialize on the TurboModule.
synchronized (moduleHolder) {
moduleHolder.setModule(turboModule);
}
/*
* TurboModuleManager is initialized after ReactApplicationContext has been set up.
* NativeModules should be initialized after ReactApplicationContext has been set up.
* Therefore, we should initialize on the TurboModule now.
*/
((NativeModule) turboModule).initialize();
mTurboModules.put(name, turboModule);
}
synchronized (moduleHolder) {
moduleHolder.endCreatingModule();
moduleHolder.notifyAll();
}
return turboModule;
}
return mTurboModules.get(name);
synchronized (moduleHolder) {
boolean wasInterrupted = false;
while (moduleHolder.isCreatingModule()) {
try {
// Wait until TurboModule is created and initialized
moduleHolder.wait();
} catch (InterruptedException e) {
wasInterrupted = true;
}
}
if (wasInterrupted) {
/*
* TurboModules should ideally be quick to create and initialize. Therefore,
* we wait until the TurboModule is done initializing before re-interrupting the
* current thread.
*/
Thread.currentThread().interrupt();
}
return moduleHolder.getModule();
}
}
@DoNotStrip
@Nullable
private TurboModule getLegacyCxxModule(String name) {
if (!mTurboModules.containsKey(name)) {
final CxxModuleWrapper turboModule = mTurbomoduleManagerDelegate.getLegacyCxxModule(name);
private CxxModuleWrapper getLegacyCxxModule(String moduleName) {
final TurboModule turboModule = getModule(moduleName);
if (!(turboModule instanceof CxxModuleWrapper)) {
return null;
}
if (turboModule instanceof TurboModule) {
/**
* TurboModuleManager is initialized after ReactApplicationContext has been setup.
* Therefore, it's safe to call initialize on the TurboModule.
*/
((NativeModule) turboModule).initialize();
return (CxxModuleWrapper) turboModule;
}
mTurboModules.put(name, (TurboModule) turboModule);
@DoNotStrip
@Nullable
private TurboModule getJavaModule(String moduleName) {
final TurboModule turboModule = getModule(moduleName);
if (turboModule instanceof CxxModuleWrapper) {
return null;
}
return turboModule;
}
/** Which TurboModules have been created? */
public Collection<TurboModule> getModules() {
final List<TurboModuleHolder> turboModuleHolders = new ArrayList<>();
synchronized (mTurboModuleCleanupLock) {
turboModuleHolders.addAll(mTurboModuleHolders.values());
}
final List<TurboModule> turboModules = new ArrayList<>();
for (final TurboModuleHolder moduleHolder : turboModuleHolders) {
synchronized (moduleHolder) {
// No need to wait for the TurboModule to finish being created and initialized
if (moduleHolder.getModule() != null) {
turboModules.add(moduleHolder.getModule());
}
}
}
return mTurboModules.get(name);
return turboModules;
}
@Nullable
public TurboModule getModule(String name) {
TurboModule javaModule = getJavaModule(name);
if (javaModule != null) {
return javaModule;
public boolean hasModule(String moduleName) {
TurboModuleHolder moduleHolder;
synchronized (mTurboModuleCleanupLock) {
moduleHolder = mTurboModuleHolders.get(moduleName);
}
return getLegacyCxxModule(name);
}
public Collection<TurboModule> getModules() {
return mTurboModules.values();
}
if (moduleHolder != null) {
synchronized (moduleHolder) {
if (moduleHolder.getModule() != null) {
return true;
}
}
}
public boolean hasModule(String name) {
return mTurboModules.containsKey(name);
return false;
}
private native HybridData initHybrid(
@@ -126,12 +280,32 @@ public class TurboModuleManager implements JSIModule, TurboModuleRegistry {
@Override
public void onCatalystInstanceDestroy() {
for (TurboModule turboModule : mTurboModules.values()) {
// TODO(T48014458): Rename this to invalidate()
((NativeModule) turboModule).onCatalystInstanceDestroy();
/*
* Halt the production of new TurboModules.
*
* <p>After this point, mTurboModuleHolders will not be accessed by TurboModuleManager.
* Therefore, it won't be modified.
*
* <p>The TurboModuleHolders in mTurboModuleHolders, however, can still be populated with newly
* created TurboModules.
*/
synchronized (mTurboModuleCleanupLock) {
mTurboModuleCleanupStarted = true;
}
mTurboModules.clear();
final Set<String> turboModuleNames = new HashSet<>(mTurboModuleHolders.keySet());
for (final String moduleName : turboModuleNames) {
// Retrieve the TurboModule, possibly waiting for it to finish instantiating.
final TurboModule turboModule = getModule(moduleName);
if (turboModule != null) {
// TODO(T48014458): Rename this to invalidate()
((NativeModule) turboModule).onCatalystInstanceDestroy();
}
}
mTurboModuleHolders.clear();
// Delete the native part of this hybrid class.
mHybridData.resetNative();
@@ -144,4 +318,41 @@ public class TurboModuleManager implements JSIModule, TurboModuleRegistry {
sIsSoLibraryLoaded = true;
}
}
private static class TurboModuleHolder {
private volatile TurboModule mModule = null;
private volatile boolean mIsTryingToCreate = false;
private volatile boolean mIsDoneCreatingModule = false;
void setModule(@NonNull TurboModule module) {
mModule = module;
}
@Nullable
TurboModule getModule() {
return mModule;
}
void startCreatingModule() {
mIsTryingToCreate = true;
}
void endCreatingModule() {
mIsTryingToCreate = false;
mIsDoneCreatingModule = true;
}
boolean isDoneCreatingModule() {
return mIsDoneCreatingModule;
}
boolean isCreatingModule() {
return mIsTryingToCreate;
}
}
private interface TurboModuleProvider {
@Nullable
TurboModule getModule(String name);
}
}
@@ -93,10 +93,10 @@ void TurboModuleManager::installJSIBindings() {
}
static auto getLegacyCxxModule =
delegate->getClass()
javaPart->getClass()
->getMethod<jni::alias_ref<CxxModuleWrapper::javaobject>(
const std::string &)>("getLegacyCxxModule");
auto legacyCxxModule = getLegacyCxxModule(delegate.get(), name);
auto legacyCxxModule = getLegacyCxxModule(javaPart.get(), name);
if (legacyCxxModule) {
auto turboModule = std::make_shared<react::TurboCxxModule>(