Skip to content

Commit 3560c0a

Browse files
rubennortefacebook-github-bot
authored andcommitted
Add new dangerouslyForceOverride method to feature flags (facebook#46963)
Summary: Changelog: [internal] This introduces a new method in `ReactNativeFeatureFlags` to force setting overrides without triggering any errors (they're returned a string instead to be handled in userland). This is partially equivalent to calling `dangerouslyReset` and `override` but completely avoids the hard crashes that could be caused by race conditions. Reviewed By: javache, rshest Differential Revision: D64186262
1 parent c09f824 commit 3560c0a

22 files changed

+285
-48
lines changed

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt

+27-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<405d53cd6aba78616b8690c26a0accad>>
7+
* @generated SignedSource<<e568acc70be10bdd8f86caddd03cde05>>
88
*/
99

1010
/**
@@ -358,6 +358,32 @@ public object ReactNativeFeatureFlags {
358358
accessor = accessorProvider()
359359
}
360360

361+
/**
362+
* This is a combination of `dangerouslyReset` and `override` that reduces
363+
* the likeliness of a race condition between the two calls.
364+
*
365+
* This is **dangerous** because it can introduce consistency issues that will
366+
* be much harder to debug. For example, it could hide the fact that feature
367+
* flags are read before you set the values you want to use everywhere. It
368+
* could also cause a workflow to suddently have different feature flags for
369+
* behaviors that were configured with different values before.
370+
*
371+
* It returns a string that contains the feature flags that were accessed
372+
* before this call (or between the last call to `dangerouslyReset` and this
373+
* call). If you are using this method, you do not want the hard crash that
374+
* you would get from using `dangerouslyReset` and `override` separately,
375+
* but you should still log this somehow.
376+
*
377+
* Please see the documentation of `dangerouslyReset` for additional details.
378+
*/
379+
@JvmStatic
380+
public fun dangerouslyForceOverride(provider: ReactNativeFeatureFlagsProvider): String? {
381+
val newAccessor = accessorProvider()
382+
val previouslyAccessedFlags = newAccessor.dangerouslyForceOverride(provider)
383+
accessor = newAccessor
384+
return previouslyAccessedFlags
385+
}
386+
361387
/**
362388
* This is just used to replace the default ReactNativeFeatureFlagsCxxAccessor
363389
* that uses JNI with a version that doesn't, to simplify testing.

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsAccessor.kt

+2
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ public interface ReactNativeFeatureFlagsAccessor : ReactNativeFeatureFlagsProvid
1111
public fun override(provider: ReactNativeFeatureFlagsProvider)
1212

1313
public fun dangerouslyReset()
14+
15+
public fun dangerouslyForceOverride(provider: ReactNativeFeatureFlagsProvider): String?
1416
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<84ee754f916b48a7c55ea94e166510c7>>
7+
* @generated SignedSource<<a2a7b39f3f71be2a278faf2c21ede6a3>>
88
*/
99

1010
/**
@@ -515,4 +515,7 @@ public class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAccesso
515515
ReactNativeFeatureFlagsCxxInterop.override(provider as Any)
516516

517517
override fun dangerouslyReset(): Unit = ReactNativeFeatureFlagsCxxInterop.dangerouslyReset()
518+
519+
override fun dangerouslyForceOverride(provider: ReactNativeFeatureFlagsProvider): String? =
520+
ReactNativeFeatureFlagsCxxInterop.dangerouslyForceOverride(provider as Any)
518521
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<eebeaa749dafac5d49d9d6b356f88817>>
7+
* @generated SignedSource<<3cf8639dfd8a92a954a055c3ebdc749c>>
88
*/
99

1010
/**
@@ -129,4 +129,6 @@ public object ReactNativeFeatureFlagsCxxInterop {
129129
@DoNotStrip @JvmStatic public external fun override(provider: Any)
130130

131131
@DoNotStrip @JvmStatic public external fun dangerouslyReset()
132+
133+
@DoNotStrip @JvmStatic public external fun dangerouslyForceOverride(provider: Any): String?
132134
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt

+17-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<4dc2364f5bcd765d7b61dbcab0d9533d>>
7+
* @generated SignedSource<<91c4268e7e88e2e3c1f3d50d149c0d2b>>
88
*/
99

1010
/**
@@ -574,7 +574,21 @@ public class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcces
574574
}
575575

576576
override fun dangerouslyReset() {
577-
// We don't need to do anything here because `ReactNativeFeatureFlags` will
578-
// just create a new instance of this class.
577+
// We don't need to do anything else here because `ReactNativeFeatureFlags` will just create a
578+
// new instance of this class.
579+
}
580+
581+
override fun dangerouslyForceOverride(provider: ReactNativeFeatureFlagsProvider): String? {
582+
val accessedFeatureFlags = getAccessedFeatureFlags()
583+
currentProvider = provider
584+
return accessedFeatureFlags
585+
}
586+
587+
internal fun getAccessedFeatureFlags(): String? {
588+
if (accessedFeatureFlags.isEmpty()) {
589+
return null
590+
}
591+
592+
return accessedFeatureFlags.joinToString(separator = ", ") { it }
579593
}
580594
}

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp

+15-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<0bb021affcef5162b578ad3f1e45508f>>
7+
* @generated SignedSource<<f4dba9f073137759e18fb71411232d7a>>
88
*/
99

1010
/**
@@ -594,11 +594,25 @@ void JReactNativeFeatureFlagsCxxInterop::dangerouslyReset(
594594
ReactNativeFeatureFlags::dangerouslyReset();
595595
}
596596

597+
jni::local_ref<jstring> JReactNativeFeatureFlagsCxxInterop::dangerouslyForceOverride(
598+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/,
599+
jni::alias_ref<jobject> provider) {
600+
auto accessedFlags = ReactNativeFeatureFlags::dangerouslyForceOverride(
601+
std::make_unique<ReactNativeFeatureFlagsProviderHolder>(provider));
602+
if (accessedFlags.has_value()) {
603+
return jni::make_jstring(accessedFlags.value());
604+
}
605+
return nullptr;
606+
}
607+
597608
void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
598609
javaClassLocal()->registerNatives({
599610
makeNativeMethod(
600611
"override", JReactNativeFeatureFlagsCxxInterop::override),
601612
makeNativeMethod("dangerouslyReset", JReactNativeFeatureFlagsCxxInterop::dangerouslyReset),
613+
makeNativeMethod(
614+
"dangerouslyForceOverride",
615+
JReactNativeFeatureFlagsCxxInterop::dangerouslyForceOverride),
602616
makeNativeMethod(
603617
"commonTestFlag",
604618
JReactNativeFeatureFlagsCxxInterop::commonTestFlag),

packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<fe97369e30d5359ffcd9c8e304dbc121>>
7+
* @generated SignedSource<<9b325f01aea83bc93db31c9a63bea3f7>>
88
*/
99

1010
/**
@@ -184,6 +184,10 @@ class JReactNativeFeatureFlagsCxxInterop
184184
static void dangerouslyReset(
185185
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
186186

187+
static jni::local_ref<jstring> dangerouslyForceOverride(
188+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>,
189+
jni::alias_ref<jobject> provider);
190+
187191
static void registerNatives();
188192
};
189193

packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp

+23-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<013c1fa01cf029635c04b50f83cc80ef>>
7+
* @generated SignedSource<<14957b53f50e3ea943f0e3631475f336>>
88
*/
99

1010
/**
@@ -21,6 +21,11 @@
2121

2222
namespace facebook::react {
2323

24+
#pragma GCC diagnostic push
25+
#pragma GCC diagnostic ignored "-Wglobal-constructors"
26+
std::unique_ptr<ReactNativeFeatureFlagsAccessor> accessor_;
27+
#pragma GCC diagnostic pop
28+
2429
bool ReactNativeFeatureFlags::commonTestFlag() {
2530
return getAccessor().commonTestFlag();
2631
}
@@ -223,16 +228,26 @@ void ReactNativeFeatureFlags::override(
223228
}
224229

225230
void ReactNativeFeatureFlags::dangerouslyReset() {
226-
getAccessor(true);
231+
accessor_ = std::make_unique<ReactNativeFeatureFlagsAccessor>();
232+
}
233+
234+
std::optional<std::string> ReactNativeFeatureFlags::dangerouslyForceOverride(
235+
std::unique_ptr<ReactNativeFeatureFlagsProvider> provider) {
236+
auto accessor = std::make_unique<ReactNativeFeatureFlagsAccessor>();
237+
accessor->override(std::move(provider));
238+
239+
std::swap(accessor_, accessor);
240+
241+
// Now accessor is the old accessor
242+
return accessor == nullptr ? std::nullopt
243+
: accessor->getAccessedFeatureFlagNames();
227244
}
228245

229-
ReactNativeFeatureFlagsAccessor& ReactNativeFeatureFlags::getAccessor(
230-
bool reset) {
231-
static std::unique_ptr<ReactNativeFeatureFlagsAccessor> accessor;
232-
if (accessor == nullptr || reset) {
233-
accessor = std::make_unique<ReactNativeFeatureFlagsAccessor>();
246+
ReactNativeFeatureFlagsAccessor& ReactNativeFeatureFlags::getAccessor() {
247+
if (accessor_ == nullptr) {
248+
accessor_ = std::make_unique<ReactNativeFeatureFlagsAccessor>();
234249
}
235-
return *accessor;
250+
return *accessor_;
236251
}
237252

238253
} // namespace facebook::react

packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h

+19-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<3bbe3eb333030be7f32c0965c9db4a5c>>
7+
* @generated SignedSource<<c5a4993e3c7093f221cce2c891d86905>>
88
*/
99

1010
/**
@@ -22,6 +22,8 @@
2222
#include <react/featureflags/ReactNativeFeatureFlagsAccessor.h>
2323
#include <react/featureflags/ReactNativeFeatureFlagsProvider.h>
2424
#include <memory>
25+
#include <optional>
26+
#include <string>
2527

2628
#ifndef RN_EXPORT
2729
#define RN_EXPORT __attribute__((visibility("default")))
@@ -317,9 +319,24 @@ class ReactNativeFeatureFlags {
317319
*/
318320
RN_EXPORT static void dangerouslyReset();
319321

322+
/**
323+
* This is a combination of `dangerouslyReset` and `override` that reduces
324+
* the likeliness of a race condition between the two calls.
325+
*
326+
* This is **dangerous** because it can introduce consistency issues that will
327+
* be much harder to debug. For example, it could hide the fact that feature
328+
* flags are read before you set the values you want to use everywhere. It
329+
* could also cause a workflow to suddently have different feature flags for
330+
* behaviors that were configured with different values before.
331+
*
332+
* Please see the documentation of `dangerouslyReset` for additional details.
333+
*/
334+
RN_EXPORT static std::optional<std::string> dangerouslyForceOverride(
335+
std::unique_ptr<ReactNativeFeatureFlagsProvider> provider);
336+
320337
private:
321338
ReactNativeFeatureFlags() = delete;
322-
static ReactNativeFeatureFlagsAccessor& getAccessor(bool reset = false);
339+
static ReactNativeFeatureFlagsAccessor& getAccessor();
323340
};
324341

325342
} // namespace facebook::react

packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp

+19-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<087de432d750fea45c0c564e626ce1d9>>
7+
* @generated SignedSource<<c106a754b3abef1e9e4b503772f85b0f>>
88
*/
99

1010
/**
@@ -923,13 +923,8 @@ void ReactNativeFeatureFlagsAccessor::override(
923923
currentProvider_ = std::move(provider);
924924
}
925925

926-
void ReactNativeFeatureFlagsAccessor::markFlagAsAccessed(
927-
int position,
928-
const char* flagName) {
929-
accessedFeatureFlags_[position] = flagName;
930-
}
931-
932-
void ReactNativeFeatureFlagsAccessor::ensureFlagsNotAccessed() {
926+
std::optional<std::string>
927+
ReactNativeFeatureFlagsAccessor::getAccessedFeatureFlagNames() const {
933928
std::ostringstream featureFlagListBuilder;
934929
for (const auto& featureFlagName : accessedFeatureFlags_) {
935930
if (featureFlagName != nullptr) {
@@ -943,10 +938,24 @@ void ReactNativeFeatureFlagsAccessor::ensureFlagsNotAccessed() {
943938
accessedFeatureFlagNames.substr(0, accessedFeatureFlagNames.size() - 2);
944939
}
945940

946-
if (!accessedFeatureFlagNames.empty()) {
941+
return accessedFeatureFlagNames.empty()
942+
? std::nullopt
943+
: std::optional{accessedFeatureFlagNames};
944+
}
945+
946+
void ReactNativeFeatureFlagsAccessor::markFlagAsAccessed(
947+
int position,
948+
const char* flagName) {
949+
accessedFeatureFlags_[position] = flagName;
950+
}
951+
952+
void ReactNativeFeatureFlagsAccessor::ensureFlagsNotAccessed() {
953+
auto accessedFeatureFlagNames = getAccessedFeatureFlagNames();
954+
955+
if (accessedFeatureFlagNames.has_value()) {
947956
throw std::runtime_error(
948957
"Feature flags were accessed before being overridden: " +
949-
accessedFeatureFlagNames);
958+
accessedFeatureFlagNames.value());
950959
}
951960
}
952961

packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<447de2c56cd16e313210f14d0ee3e9e9>>
7+
* @generated SignedSource<<0c9cfdad6a33dd4044a5945befb45522>>
88
*/
99

1010
/**
@@ -24,6 +24,7 @@
2424
#include <atomic>
2525
#include <memory>
2626
#include <optional>
27+
#include <string>
2728

2829
namespace facebook::react {
2930

@@ -82,6 +83,7 @@ class ReactNativeFeatureFlagsAccessor {
8283
bool useTurboModules();
8384

8485
void override(std::unique_ptr<ReactNativeFeatureFlagsProvider> provider);
86+
std::optional<std::string> getAccessedFeatureFlagNames() const;
8587

8688
private:
8789
void markFlagAsAccessed(int position, const char* flagName);

packages/react-native/ReactCommon/react/featureflags/tests/ReactNativeFeatureFlagsTest.cpp

+23
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,27 @@ TEST_F(ReactNativeFeatureFlagsTest, allowsOverridingAgainAfterReset) {
121121
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
122122
}
123123

124+
TEST_F(
125+
ReactNativeFeatureFlagsTest,
126+
allowsDangerouslyForcingOverridesWhenValuesHaveNotBeenAccessed) {
127+
auto accessedFlags = ReactNativeFeatureFlags::dangerouslyForceOverride(
128+
std::make_unique<ReactNativeFeatureFlagsTestOverrides>());
129+
130+
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
131+
EXPECT_EQ(accessedFlags.has_value(), false);
132+
}
133+
134+
TEST_F(
135+
ReactNativeFeatureFlagsTest,
136+
allowsDangerouslyForcingOverridesWhenValuesHaveBeenAccessed) {
137+
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), false);
138+
139+
auto accessedFlags = ReactNativeFeatureFlags::dangerouslyForceOverride(
140+
std::make_unique<ReactNativeFeatureFlagsTestOverrides>());
141+
142+
EXPECT_EQ(ReactNativeFeatureFlags::commonTestFlag(), true);
143+
EXPECT_EQ(accessedFlags.has_value(), true);
144+
EXPECT_EQ(accessedFlags.value(), "commonTestFlag");
145+
}
146+
124147
} // namespace facebook::react

packages/react-native/scripts/featureflags/templates/android/JReactNativeFeatureFlagsCxxInterop.cpp-template.js

+14
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,25 @@ void JReactNativeFeatureFlagsCxxInterop::dangerouslyReset(
8888
ReactNativeFeatureFlags::dangerouslyReset();
8989
}
9090
91+
jni::local_ref<jstring> JReactNativeFeatureFlagsCxxInterop::dangerouslyForceOverride(
92+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/,
93+
jni::alias_ref<jobject> provider) {
94+
auto accessedFlags = ReactNativeFeatureFlags::dangerouslyForceOverride(
95+
std::make_unique<ReactNativeFeatureFlagsProviderHolder>(provider));
96+
if (accessedFlags.has_value()) {
97+
return jni::make_jstring(accessedFlags.value());
98+
}
99+
return nullptr;
100+
}
101+
91102
void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
92103
javaClassLocal()->registerNatives({
93104
makeNativeMethod(
94105
"override", JReactNativeFeatureFlagsCxxInterop::override),
95106
makeNativeMethod("dangerouslyReset", JReactNativeFeatureFlagsCxxInterop::dangerouslyReset),
107+
makeNativeMethod(
108+
"dangerouslyForceOverride",
109+
JReactNativeFeatureFlagsCxxInterop::dangerouslyForceOverride),
96110
${Object.entries(definitions.common)
97111
.map(
98112
([flagName, flagConfig]) =>

0 commit comments

Comments
 (0)