Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions FirebaseRemoteConfig/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Unreleased
- [fixed] Fixed data races that could mix stale and current Remote Config
experiment state during database loading and activation. (#16303)
- [fixed] Made experiment payload replacement atomic and delayed A/B Testing
updates until the queued active-experiment database replacement completes.

# 12.17.0
- [fixed] Fixed a memory leak in Remote Config where `activateWithCompletion:`
retained the `FIRRemoteConfig` instance indefinitely (#16413).
Expand Down
9 changes: 9 additions & 0 deletions FirebaseRemoteConfig/Sources/RCNConfigDBManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ typedef void (^RCNDBLoadCompletion)(BOOL success,
value:(NSData *)value
completionHandler:(RCNDBCompletion)handler;

/// Atomically replaces all experiment records for `key`.
/// @param key The experiment data key, as defined in `RCNConfigDefines.h`.
/// @param values The serialized experiment values to persist.
/// @param handler The callback, invoked on the main queue after the replacement finishes. It is an
/// ordering barrier for the queued operation, not a durability guarantee.
- (void)replaceExperimentTableWithKey:(NSString *)key
values:(NSArray<NSData *> *)values
completionHandler:(RCNDBCompletion)handler;

- (void)updateMetadataWithOption:(RCNUpdateOption)option
namespace:(NSString *)namespace
values:(NSArray *)values
Expand Down
51 changes: 47 additions & 4 deletions FirebaseRemoteConfig/Sources/RCNConfigDBManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@
/// Introduce a dedicated serial queue for gIsNewDatabase access.
static dispatch_queue_t gIsNewDatabaseQueue;

static dispatch_queue_t RCNIsNewDatabaseQueue(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
gIsNewDatabaseQueue = dispatch_queue_create("com.google.FirebaseRemoteConfig.gIsNewDatabase",
DISPATCH_QUEUE_SERIAL);
});
return gIsNewDatabaseQueue;
}

/// Remote Config database path for deprecated V0 version.
static NSString *RemoteConfigPathForOldDatabaseV0(void) {
NSArray *dirPaths =
Expand Down Expand Up @@ -85,7 +94,7 @@ static BOOL RemoteConfigCreateFilePathIfNotExist(NSString *filePath) {
}
NSFileManager *fileManager = [NSFileManager defaultManager];
if (![fileManager fileExistsAtPath:filePath]) {
dispatch_sync(gIsNewDatabaseQueue, ^{
dispatch_sync(RCNIsNewDatabaseQueue(), ^{
gIsNewDatabase = YES;
});
NSError *error;
Expand Down Expand Up @@ -124,8 +133,6 @@ + (instancetype)sharedInstance {
static dispatch_once_t onceToken;
static RCNConfigDBManager *sharedInstance;
dispatch_once(&onceToken, ^{
gIsNewDatabaseQueue = dispatch_queue_create("com.google.FirebaseRemoteConfig.gIsNewDatabase",
DISPATCH_QUEUE_SERIAL);
sharedInstance = [[RCNConfigDBManager alloc] init];
});
return sharedInstance;
Expand All @@ -139,6 +146,7 @@ + (NSString *)remoteConfigPathForDatabase {
- (instancetype)init {
self = [super init];
if (self) {
(void)RCNIsNewDatabaseQueue();
_databaseOperationQueue =
dispatch_queue_create("com.google.GoogleConfigService.database", DISPATCH_QUEUE_SERIAL);
[self createOrOpenDatabase];
Expand Down Expand Up @@ -485,6 +493,41 @@ - (void)insertExperimentTableWithKey:(NSString *)key
});
}

- (void)replaceExperimentTableWithKey:(NSString *)key
values:(NSArray<NSData *> *)values
completionHandler:(RCNDBCompletion)handler {
NSString *keyCopy = [key copy];
NSArray<NSData *> *valuesCopy = [values copy];
Comment thread
ryannair05 marked this conversation as resolved.
dispatch_async(_databaseOperationQueue, ^{
BOOL transactionStarted = [self executeQuery:"BEGIN IMMEDIATE TRANSACTION"];
BOOL success = transactionStarted;
if (success) {
const char *deleteSQL = "DELETE FROM " RCNTableNameExperiment " WHERE key = ?";
success = [self executeQuery:deleteSQL withParams:@[ keyCopy ]];
}

for (NSData *value in valuesCopy) {
if (!success) {
break;
}
success = [self insertExperimentTableWithKey:keyCopy value:value];
}

if (success) {
success = [self executeQuery:"COMMIT TRANSACTION"];
}
if (!success && transactionStarted) {
[self executeQuery:"ROLLBACK TRANSACTION"];
}

if (handler) {
dispatch_async(dispatch_get_main_queue(), ^{
handler(success, nil);
});
}
});
}

- (BOOL)insertExperimentTableWithKey:(NSString *)key value:(NSData *)dataValue {
if ([key isEqualToString:@RCNExperimentTableKeyMetadata]) {
return [self updateExperimentMetadata:dataValue];
Expand Down Expand Up @@ -1227,7 +1270,7 @@ - (BOOL)logErrorWithSQL:(const char *)SQL

- (BOOL)isNewDatabase {
__block BOOL isNew;
dispatch_sync(gIsNewDatabaseQueue, ^{
dispatch_sync(RCNIsNewDatabaseQueue(), ^{
isNew = gIsNewDatabase;
});
return isNew;
Expand Down
Loading
Loading