Skip to content

Commit a34540f

Browse files
FIS: race condition (#4148)
* FIRInstallationsSingleOperationPromiseCache: a failing test for race condition added. * ./scripts/style.sh * FIRInstallationsSingleOperationPromiseCache: synchronize entire code blocks not relying on `atomic` property. The atomic property synchronization was insufficient to synchronize `newOperationHandler()`. * ./scripts/style.sh
1 parent 6fd815a commit a34540f

File tree

2 files changed

+104
-14
lines changed

2 files changed

+104
-14
lines changed

FirebaseInstallations/Source/Library/InstallationsIDController/FIRInstallationsSingleOperationPromiseCache.m

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
@interface FIRInstallationsSingleOperationPromiseCache <ResultType>()
2626
@property(nonatomic, readonly) FBLPromise *_Nonnull (^newOperationHandler)(void);
27-
@property(atomic, nullable) FBLPromise *pendingPromise;
27+
@property(nonatomic, nullable) FBLPromise *pendingPromise;
2828
@end
2929

3030
@implementation FIRInstallationsSingleOperationPromiseCache
@@ -44,24 +44,32 @@ - (instancetype)initWithNewOperationHandler:
4444
}
4545

4646
- (FBLPromise *)getExistingPendingOrCreateNewPromise {
47-
if (!self.pendingPromise) {
48-
self.pendingPromise = self.newOperationHandler();
47+
@synchronized(self) {
48+
if (!self.pendingPromise) {
49+
self.pendingPromise = self.newOperationHandler();
4950

50-
self.pendingPromise
51-
.then(^id(id result) {
52-
self.pendingPromise = nil;
53-
return nil;
54-
})
55-
.catch(^void(NSError *error) {
56-
self.pendingPromise = nil;
57-
});
58-
}
51+
self.pendingPromise
52+
.then(^id(id result) {
53+
@synchronized(self) {
54+
self.pendingPromise = nil;
55+
return nil;
56+
}
57+
})
58+
.catch(^void(NSError *error) {
59+
@synchronized(self) {
60+
self.pendingPromise = nil;
61+
}
62+
});
63+
}
5964

60-
return self.pendingPromise;
65+
return self.pendingPromise;
66+
}
6167
}
6268

6369
- (nullable FBLPromise *)getExistingPendingPromise {
64-
return self.pendingPromise;
70+
@synchronized(self) {
71+
return self.pendingPromise;
72+
}
6573
}
6674

6775
@end
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2019 Google
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#import <XCTest/XCTest.h>
18+
19+
#import "FBLPromise+Testing.h"
20+
#import "FBLPromises.h"
21+
22+
#import "FIRInstallationsSingleOperationPromiseCache.h"
23+
24+
@interface FIRInstallationsSingleOperationPromiseCacheTests : XCTestCase
25+
26+
@end
27+
28+
@implementation FIRInstallationsSingleOperationPromiseCacheTests
29+
30+
// This test is flaky by definition.
31+
// If this test fails at least once it means there must be a concurrency issue.
32+
- (void)testRaceCondition {
33+
for (NSInteger i = 0; i < 100; i++) {
34+
[self assertRaceConditionWithParallelOperationCount:10000];
35+
}
36+
}
37+
38+
- (void)assertRaceConditionWithParallelOperationCount:(NSInteger)count {
39+
FIRInstallationsSingleOperationPromiseCache *promiseCache =
40+
[[FIRInstallationsSingleOperationPromiseCache alloc]
41+
initWithNewOperationHandler:^FBLPromise *_Nonnull {
42+
[NSThread sleepForTimeInterval:0.001];
43+
return [[FBLPromise resolvedWith:[[NSObject alloc] init]] delay:0.001];
44+
}];
45+
46+
XCTestExpectation *operationsExpectation =
47+
[self expectationWithDescription:@"operationsExpectation"];
48+
operationsExpectation.expectedFulfillmentCount = count;
49+
50+
for (NSInteger i = 0; i < count; i++) {
51+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
52+
XCTAssertNoThrow([promiseCache getExistingPendingPromise]);
53+
});
54+
55+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
56+
FBLPromise *promise = [promiseCache getExistingPendingOrCreateNewPromise];
57+
XCTAssertNotNil(promise);
58+
[operationsExpectation fulfill];
59+
});
60+
}
61+
62+
XCTAssert(FBLWaitForPromisesWithTimeout(10));
63+
[self waitForExpectations:@[ operationsExpectation ] timeout:10];
64+
65+
// The resolved promise cleanup may not happen instantly. Wait until it happens.
66+
FBLPromise *existingPromise = [promiseCache getExistingPendingPromise];
67+
if (existingPromise) {
68+
XCTestExpectation *cleanupExpectation = [self expectationWithDescription:@""];
69+
70+
[existingPromise then:^id _Nullable(id _Nullable value) {
71+
[cleanupExpectation fulfill];
72+
return nil;
73+
}];
74+
75+
[self waitForExpectations:@[ cleanupExpectation ] timeout:0.5];
76+
}
77+
78+
// Check if resolved promise cleaned up.
79+
XCTAssertNil([promiseCache getExistingPendingPromise]);
80+
}
81+
82+
@end

0 commit comments

Comments
 (0)