Skip to content

Commit 56be86e

Browse files
ArtVolkovArtem Volkov
andauthored
Fix RemoteConfig multithreading (#8626)
* Fix RemoteConfig multithreading * RCNConfigContent multithreading tests av/rc-fix-threads-stuck * RCNConfigContent test style av/rc-fix-threads-stuck * RCNConfigContent test more asserts av/rc-fix-threads-stuck Additional info: it seems safe to put XCTAssert in non-main queue. Apple example: https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations?language=objc Callback in example "is executed on the delegate queue" https://developer.apple.com/documentation/foundation/nsurlsession/1410330-datataskwithurl?language=objc Co-authored-by: Artem Volkov <[email protected]>
1 parent 23b6ace commit 56be86e

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

FirebaseRemoteConfig/Sources/RCNConfigContent.m

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ @implementation RCNConfigContent {
4141
RCNConfigDBManager *_DBManager;
4242
/// Current bundle identifier;
4343
NSString *_bundleIdentifier;
44-
/// Dispatch semaphore to block all config reads until we have read from the database. This only
44+
/// Blocks all config reads until we have read from the database. This only
4545
/// potentially blocks on the first read. Should be a no-wait for all subsequent reads once we
4646
/// have data read into memory from the database.
47-
dispatch_semaphore_t _configLoadFromDBSemaphore;
47+
dispatch_group_t _dispatch_group;
4848
/// Boolean indicating if initial DB load of fetched,active and default config has succeeded.
4949
BOOL _isConfigLoadFromDBCompleted;
5050
/// Boolean indicating that the load from database has initiated at least once.
5151
BOOL _isDatabaseLoadAlreadyInitiated;
5252
}
5353

5454
/// Default timeout when waiting to read data from database.
55-
static const NSTimeInterval kDatabaseLoadTimeoutSecs = 30.0;
55+
const NSTimeInterval kDatabaseLoadTimeoutSecs = 30.0;
5656

5757
/// Singleton instance of RCNConfigContent.
5858
+ (instancetype)sharedInstance {
@@ -86,7 +86,8 @@ - (instancetype)initWithDBManager:(RCNConfigDBManager *)DBManager {
8686
_bundleIdentifier = @"";
8787
}
8888
_DBManager = DBManager;
89-
_configLoadFromDBSemaphore = dispatch_semaphore_create(0);
89+
// Waits for both config and Personalization data to load.
90+
_dispatch_group = dispatch_group_create();
9091
[self loadConfigFromMainTable];
9192
}
9293
return self;
@@ -112,22 +113,25 @@ - (void)loadConfigFromMainTable {
112113
NSAssert(!_isDatabaseLoadAlreadyInitiated, @"Database load has already been initiated");
113114
_isDatabaseLoadAlreadyInitiated = true;
114115

116+
dispatch_group_enter(_dispatch_group);
115117
[_DBManager
116118
loadMainWithBundleIdentifier:_bundleIdentifier
117119
completionHandler:^(BOOL success, NSDictionary *fetchedConfig,
118120
NSDictionary *activeConfig, NSDictionary *defaultConfig) {
119121
self->_fetchedConfig = [fetchedConfig mutableCopy];
120122
self->_activeConfig = [activeConfig mutableCopy];
121123
self->_defaultConfig = [defaultConfig mutableCopy];
122-
dispatch_semaphore_signal(self->_configLoadFromDBSemaphore);
124+
dispatch_group_leave(self->_dispatch_group);
123125
}];
124126

125127
// TODO(karenzeng): Refactor personalization to be returned in loadMainWithBundleIdentifier above
128+
dispatch_group_enter(_dispatch_group);
126129
[_DBManager loadPersonalizationWithCompletionHandler:^(
127130
BOOL success, NSDictionary *fetchedPersonalization,
128131
NSDictionary *activePersonalization, NSDictionary *defaultConfig) {
129132
self->_fetchedPersonalization = [fetchedPersonalization copy];
130133
self->_activePersonalization = [activePersonalization copy];
134+
dispatch_group_leave(self->_dispatch_group);
131135
}];
132136
}
133137

@@ -359,12 +363,12 @@ - (NSDictionary *)getConfigAndMetadataForNamespace:(NSString *)FIRNamespace {
359363
/// configs until load is done.
360364
/// @return Database load completion status.
361365
- (BOOL)checkAndWaitForInitialDatabaseLoad {
362-
/// Wait on semaphore until done. This should be a no-op for subsequent calls.
366+
/// Wait until load is done. This should be a no-op for subsequent calls.
363367
if (!_isConfigLoadFromDBCompleted) {
364-
long result = dispatch_semaphore_wait(
365-
_configLoadFromDBSemaphore,
368+
intptr_t isErrorOrTimeout = dispatch_group_wait(
369+
_dispatch_group,
366370
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDatabaseLoadTimeoutSecs * NSEC_PER_SEC)));
367-
if (result != 0) {
371+
if (isErrorOrTimeout) {
368372
FIRLogError(kFIRLoggerRemoteConfig, @"I-RCN000048",
369373
@"Timed out waiting for fetched config to be loaded from DB");
370374
return false;

FirebaseRemoteConfig/Sources/RCNConfigDBManager.m

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,9 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {
868868
dispatch_async(_databaseOperationQueue, ^{
869869
RCNConfigDBManager *strongSelf = weakSelf;
870870
if (!strongSelf) {
871+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
872+
handler(NO, [NSMutableDictionary new], [NSMutableDictionary new], nil);
873+
});
871874
return;
872875
}
873876

@@ -898,7 +901,7 @@ - (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {
898901
}
899902

900903
if (handler) {
901-
dispatch_async(dispatch_get_main_queue(), ^{
904+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
902905
handler(YES, fetchedPersonalization, activePersonalization, nil);
903906
});
904907
}
@@ -972,6 +975,9 @@ - (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier
972975
dispatch_async(_databaseOperationQueue, ^{
973976
RCNConfigDBManager *strongSelf = weakSelf;
974977
if (!strongSelf) {
978+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
979+
handler(NO, [NSDictionary new], [NSDictionary new], [NSDictionary new]);
980+
});
975981
return;
976982
}
977983
__block NSDictionary *fetchedConfig =

FirebaseRemoteConfig/Tests/Unit/RCNConfigContentTest.m

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,36 @@
2525
#import "FirebaseRemoteConfig/Tests/Unit/RCNTestUtilities.h"
2626

2727
@interface RCNConfigContent (Testing)
28-
- (void)checkAndWaitForInitialDatabaseLoad;
28+
- (BOOL)checkAndWaitForInitialDatabaseLoad;
29+
@end
30+
31+
extern const NSTimeInterval kDatabaseLoadTimeoutSecs;
32+
@interface RCNConfigDBManagerMock : RCNConfigDBManager
33+
@property(nonatomic, assign) BOOL isLoadMainCompleted;
34+
@property(nonatomic, assign) BOOL isLoadPersonalizationCompleted;
35+
@end
36+
@implementation RCNConfigDBManagerMock
37+
- (void)createOrOpenDatabase {
38+
}
39+
- (void)loadMainWithBundleIdentifier:(NSString *)bundleIdentifier
40+
completionHandler:(RCNDBLoadCompletion)handler {
41+
double justSmallDelay = 0.008;
42+
XCTAssertTrue(justSmallDelay < kDatabaseLoadTimeoutSecs);
43+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justSmallDelay * NSEC_PER_SEC)),
44+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
45+
self.isLoadMainCompleted = YES;
46+
handler(YES, nil, nil, nil);
47+
});
48+
}
49+
- (void)loadPersonalizationWithCompletionHandler:(RCNDBLoadCompletion)handler {
50+
double justOtherSmallDelay = 0.009;
51+
XCTAssertTrue(justOtherSmallDelay < kDatabaseLoadTimeoutSecs);
52+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(justOtherSmallDelay * NSEC_PER_SEC)),
53+
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
54+
self.isLoadPersonalizationCompleted = YES;
55+
handler(YES, nil, nil, nil);
56+
});
57+
}
2958
@end
3059

3160
@interface RCNConfigContentTest : XCTestCase {
@@ -262,4 +291,39 @@ - (void)testCopyFromDictionaryUpdatesActiveConfig {
262291
[_configContent.activeConfig[@"dummy_namespace"][@"new_key"] dataValue]);
263292
}
264293

294+
- (void)testCheckAndWaitForInitialDatabaseLoad {
295+
RCNConfigDBManagerMock *mockDBManager = [[RCNConfigDBManagerMock alloc] init];
296+
RCNConfigContent *configContent = [[RCNConfigContent alloc] initWithDBManager:mockDBManager];
297+
298+
// Check that no one of first three calls of `-checkAndWaitForInitialDatabaseLoad` do not produce
299+
// timeout error <begin>
300+
XCTestExpectation *expectation1 =
301+
[self expectationWithDescription:
302+
@"1st `checkAndWaitForInitialDatabaseLoad` return without timeout"];
303+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
304+
XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
305+
[expectation1 fulfill];
306+
});
307+
XCTestExpectation *expectation2 =
308+
[self expectationWithDescription:
309+
@"2nd `checkAndWaitForInitialDatabaseLoad` return without timeout"];
310+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
311+
XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
312+
[expectation2 fulfill];
313+
});
314+
315+
XCTAssertTrue([configContent checkAndWaitForInitialDatabaseLoad]);
316+
// Check that both `-load...` methods already completed after 1st wait.
317+
// This make us sure that both `-loadMainWithBundleIdentifier` and
318+
// `-loadPersonalizationWithCompletionHandler` methods synched with
319+
// `-checkAndWaitForInitialDatabaseLoad`.
320+
XCTAssertTrue(mockDBManager.isLoadMainCompleted);
321+
XCTAssertTrue(mockDBManager.isLoadPersonalizationCompleted);
322+
323+
// Check that no one of first three calls of `-checkAndWaitForInitialDatabaseLoad` do not produce
324+
// timeout error <end>.
325+
// This make us sure that there no threads "stucked" on `-checkAndWaitForInitialDatabaseLoad`.
326+
[self waitForExpectationsWithTimeout:0.5 * kDatabaseLoadTimeoutSecs handler:nil];
327+
}
328+
265329
@end

0 commit comments

Comments
 (0)