Skip to content

Commit d796edb

Browse files
authored
Merge pull request #4174 from wmathurin/fix-safe-collection-write-starvation
Fix safe collection write starvation under concurrent REST requests
2 parents dd5dcc3 + 39d60a2 commit d796edb

5 files changed

Lines changed: 151 additions & 24 deletions

File tree

‎libs/SalesforceSDKCommon/SalesforceSDKCommon/Classes/Util/SFSDKSafeMutableDictionary.m‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ - (instancetype)init {
3838
self = [super init];
3939
if (self) {
4040
self.backingDictionary = [NSMutableDictionary new];
41+
// A concurrent queue keeps independent readers parallel; barrier writes serialize with them.
4142
self.queue = dispatch_queue_create([NSString stringWithFormat:@"com.salesforce.mobilesdk.readWriteQueue%u", arc4random_uniform(UINT32_MAX)].UTF8String, DISPATCH_QUEUE_CONCURRENT);
4243
}
4344
return self;
@@ -94,7 +95,7 @@ - (void)setObject:(id)object forKey:(id<NSCopying>)aKey {
9495
[SFLogger w:[self class] format:@"Attempted to set object with nil key in safe dictionary"];
9596
return;
9697
}
97-
dispatch_barrier_async(self.queue, ^{
98+
dispatch_barrier_sync(self.queue, ^{
9899
self.backingDictionary[aKey] = object;
99100
});
100101
}
@@ -108,35 +109,33 @@ - (void)removeObject:(id<NSCopying>)aKey {
108109
[SFLogger w:[self class] format:@"Attempted to remove nil key from safe dictionary"];
109110
return;
110111
}
111-
dispatch_barrier_async(self.queue, ^{
112+
dispatch_barrier_sync(self.queue, ^{
112113
[self.backingDictionary removeObjectForKey:aKey];
113114
});
114115
}
115116

116117
- (void)removeAllObjects {
117-
dispatch_barrier_async(self.queue, ^{
118+
dispatch_barrier_sync(self.queue, ^{
118119
[self.backingDictionary removeAllObjects];
119120
});
120121
}
121122

122123
- (void)removeObjects:(NSArray<id<NSCopying>> *)keys {
123-
dispatch_barrier_async(self.queue, ^{
124+
dispatch_barrier_sync(self.queue, ^{
124125
[self.backingDictionary removeObjectsForKeys:keys];
125126
});
126127
}
127128

128129
- (void)addEntries:(NSDictionary *)otherDictionary {
129-
dispatch_barrier_async(self.queue, ^{
130+
dispatch_barrier_sync(self.queue, ^{
130131
[self.backingDictionary addEntriesFromDictionary:otherDictionary];
131132
});
132133
}
133134

134135
- (void)setDictionary:(NSDictionary *)dictionary {
135-
dispatch_barrier_async(self.queue, ^{
136+
dispatch_barrier_sync(self.queue, ^{
136137
[self.backingDictionary setDictionary:dictionary];
137138
});
138139
}
139140

140141
@end
141-
142-

‎libs/SalesforceSDKCommon/SalesforceSDKCommon/Classes/Util/SFSDKSafeMutableSet.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ NS_SWIFT_NAME(SafeMutableSet)
8181
- (void)filterUsingPredicate:(NSPredicate *)predicate;
8282

8383
/**
84-
* Enumerate objects in the set safely, using a block.
84+
* Enumerates a stable snapshot of the set using a block. The block executes outside the
85+
* collection's synchronization queue, so it may safely mutate the set. Mutations made while
86+
* enumeration is in progress do not change the snapshot and may interleave with block calls.
8587
*/
8688
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block;
8789

‎libs/SalesforceSDKCommon/SalesforceSDKCommon/Classes/Util/SFSDKSafeMutableSet.m‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -101,66 +101,67 @@ - (BOOL)isEqualToSet:(SFSDKSafeMutableSet *)that {
101101
}
102102

103103
- (void)addObject:(id)obj {
104-
dispatch_barrier_async(self.queue, ^{
104+
// Admission callers can immediately perform a synchronous membership read.
105+
// Publish the mutation before returning so a burst of callers cannot fill
106+
// their executor with readers waiting behind queued barrier writes.
107+
dispatch_barrier_sync(self.queue, ^{
105108
[self.backingSet addObject:obj];
106109
});
107110
}
108111

109112
- (void)addObjectsFromArray:(NSArray *)array {
110-
dispatch_barrier_async(self.queue, ^{
113+
dispatch_barrier_sync(self.queue, ^{
111114
[self.backingSet addObjectsFromArray:array];
112115
});
113116
}
114117

115118
- (void)removeAllObjects {
116-
dispatch_barrier_async(self.queue, ^{
119+
dispatch_barrier_sync(self.queue, ^{
117120
[self.backingSet removeAllObjects];
118121
});
119122
}
120123

121124
- (void)removeObject:(id)object {
122-
dispatch_barrier_async(self.queue, ^{
125+
dispatch_barrier_sync(self.queue, ^{
123126
[self.backingSet removeObject:object];
124127
});
125128
}
126129

127130
- (void)unionSet:(NSSet *)set {
128-
dispatch_barrier_async(self.queue, ^{
131+
dispatch_barrier_sync(self.queue, ^{
129132
[self.backingSet unionSet:set];
130133
});
131134
}
132135

133136
- (void)minusSet:(NSSet *)set {
134-
dispatch_barrier_async(self.queue, ^{
137+
dispatch_barrier_sync(self.queue, ^{
135138
[self.backingSet minusSet:set];
136139
});
137140
}
138141

139142
- (void)intersectSet:(NSSet *)set {
140-
dispatch_barrier_async(self.queue, ^{
143+
dispatch_barrier_sync(self.queue, ^{
141144
[self.backingSet intersectSet:set];
142145
});
143146
}
144147

145148
- (void)setSet:(NSSet *)set {
146-
dispatch_barrier_async(self.queue, ^{
149+
dispatch_barrier_sync(self.queue, ^{
147150
[self.backingSet setSet:set];
148151
});
149152
}
150153

151154
- (void)filterUsingPredicate:(NSPredicate *)predicate {
152-
dispatch_barrier_async(self.queue, ^{
155+
dispatch_barrier_sync(self.queue, ^{
153156
[self.backingSet filterUsingPredicate:predicate];
154157
});
155158
}
156159

157160
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, BOOL *stop))block {
158-
__block NSArray *array = [self allObjects];
159-
dispatch_barrier_sync(self.queue, ^{
160-
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
161-
block(obj, stop);
162-
}];
163-
});
161+
NSArray *array = [self allObjects];
162+
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
163+
block(obj, stop);
164+
}];
164165
}
165166

166167
#pragma mark - Class Level
@@ -177,6 +178,7 @@ + (id)setWithCapacity:(NSUInteger)numItems {
177178

178179
#pragma private methods
179180
- (void)initQueue {
181+
// A concurrent queue keeps independent readers parallel; barrier writes serialize with them.
180182
self.queue = dispatch_queue_create([NSString stringWithFormat:@"com.salesforce.mobilesdk.readWriteSetQ%u", arc4random_uniform(UINT32_MAX)].UTF8String, DISPATCH_QUEUE_CONCURRENT);
181183
}
182184

‎libs/SalesforceSDKCommon/SalesforceSDKCommonTests/SFSDKSafeMutableDictionaryTests.m‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,31 @@
2525
@import XCTest;
2626
#import "SFSDKSafeMutableDictionary.h"
2727

28+
// Blocking -hash pauses inside NSMutableDictionary's mutation so publication timing is deterministic.
29+
@interface SFSDKBlockingDictionaryKey : NSObject <NSCopying>
30+
@property (nonatomic, strong) dispatch_semaphore_t hashStarted;
31+
@property (nonatomic, strong) dispatch_semaphore_t allowHashToReturn;
32+
@property (atomic, assign) BOOL shouldBlockHash;
33+
@end
34+
35+
@implementation SFSDKBlockingDictionaryKey
36+
37+
- (id)copyWithZone:(NSZone *)zone {
38+
return self;
39+
}
40+
41+
- (NSUInteger)hash {
42+
if (self.shouldBlockHash) {
43+
self.shouldBlockHash = NO;
44+
dispatch_semaphore_signal(self.hashStarted);
45+
dispatch_semaphore_wait(self.allowHashToReturn,
46+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)));
47+
}
48+
return 1;
49+
}
50+
51+
@end
52+
2853
@interface SFSDKSafeMutableDictionaryTests : XCTestCase
2954

3055
@property (strong, nonatomic) SFSDKSafeMutableDictionary *testDictionary;
@@ -64,6 +89,32 @@ - (void)testConcurrentReadWrites {
6489
}];
6590
}
6691

92+
- (void)testSetObjectPublishesMutationBeforeReturning {
93+
SFSDKSafeMutableDictionary *dictionary = [[SFSDKSafeMutableDictionary alloc] init];
94+
SFSDKBlockingDictionaryKey *key = [SFSDKBlockingDictionaryKey new];
95+
key.hashStarted = dispatch_semaphore_create(0);
96+
key.allowHashToReturn = dispatch_semaphore_create(0);
97+
key.shouldBlockHash = YES;
98+
dispatch_semaphore_t setReturned = dispatch_semaphore_create(0);
99+
100+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
101+
[dictionary setObject:@"value" forKey:key];
102+
dispatch_semaphore_signal(setReturned);
103+
});
104+
105+
XCTAssertEqual(dispatch_semaphore_wait(key.hashStarted,
106+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))), 0,
107+
@"dictionary mutation should begin");
108+
XCTAssertNotEqual(dispatch_semaphore_wait(setReturned, DISPATCH_TIME_NOW), 0,
109+
@"setObject:forKey: must not return before its mutation is published");
110+
111+
dispatch_semaphore_signal(key.allowHashToReturn);
112+
XCTAssertEqual(dispatch_semaphore_wait(setReturned,
113+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))), 0,
114+
@"setObject:forKey: should return after publishing the mutation");
115+
XCTAssertEqualObjects([dictionary objectForKey:key], @"value");
116+
}
117+
67118
#pragma Mark - Helper Methods
68119

69120
- (NSArray *)generateTestKeys {

‎libs/SalesforceSDKCommon/SalesforceSDKCommonTests/SFSDKSafeMutableSetTests.m‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,27 @@
2525
@import XCTest;
2626
#import "SFSDKSafeMutableSet.h"
2727

28+
// Blocking -hash pauses inside NSMutableSet's mutation so publication timing is deterministic.
29+
@interface SFSDKBlockingHashObject : NSObject
30+
@property (nonatomic, strong) dispatch_semaphore_t hashStarted;
31+
@property (nonatomic, strong) dispatch_semaphore_t allowHashToReturn;
32+
@property (atomic, assign) BOOL shouldBlockHash;
33+
@end
34+
35+
@implementation SFSDKBlockingHashObject
36+
37+
- (NSUInteger)hash {
38+
if (self.shouldBlockHash) {
39+
self.shouldBlockHash = NO;
40+
dispatch_semaphore_signal(self.hashStarted);
41+
dispatch_semaphore_wait(self.allowHashToReturn,
42+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)));
43+
}
44+
return 1;
45+
}
46+
47+
@end
48+
2849
@interface SFSDKSafeMutableSetTests : XCTestCase
2950
@end
3051

@@ -76,6 +97,58 @@ - (void)testConcurrentWrites {
7697
}];
7798
}
7899

100+
- (void)testAddObjectPublishesMutationBeforeReturning {
101+
SFSDKSafeMutableSet *set = [SFSDKSafeMutableSet set];
102+
SFSDKBlockingHashObject *object = [SFSDKBlockingHashObject new];
103+
object.hashStarted = dispatch_semaphore_create(0);
104+
object.allowHashToReturn = dispatch_semaphore_create(0);
105+
object.shouldBlockHash = YES;
106+
dispatch_semaphore_t addReturned = dispatch_semaphore_create(0);
107+
108+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
109+
[set addObject:object];
110+
dispatch_semaphore_signal(addReturned);
111+
});
112+
113+
XCTAssertEqual(dispatch_semaphore_wait(object.hashStarted,
114+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))), 0,
115+
@"set mutation should begin");
116+
XCTAssertNotEqual(dispatch_semaphore_wait(addReturned, DISPATCH_TIME_NOW), 0,
117+
@"addObject: must not return before its mutation is published");
118+
119+
dispatch_semaphore_signal(object.allowHashToReturn);
120+
XCTAssertEqual(dispatch_semaphore_wait(addReturned,
121+
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC))), 0,
122+
@"addObject: should return after publishing the mutation");
123+
XCTAssertTrue([set containsObject:object]);
124+
}
125+
126+
- (void)testEnumerationCallbackCanMutateSet {
127+
SFSDKSafeMutableSet *set = [SFSDKSafeMutableSet set];
128+
[set addObjectsFromArray:@[@"one", @"two"]];
129+
NSMutableSet *enumeratedObjects = [NSMutableSet set];
130+
XCTestExpectation *enumerationCompleted = [self expectationWithDescription:@"enumerationCompleted"];
131+
132+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
133+
[set enumerateObjectsUsingBlock:^(id obj, BOOL *stop) {
134+
[enumeratedObjects addObject:obj];
135+
[set addObject:@"addedFromCallback"];
136+
}];
137+
[enumerationCompleted fulfill];
138+
});
139+
140+
XCTWaiterResult result = [XCTWaiter waitForExpectations:@[enumerationCompleted] timeout:2.0];
141+
XCTAssertEqual(result, XCTWaiterResultCompleted,
142+
@"enumeration callbacks must be able to mutate the set without deadlocking");
143+
if (result != XCTWaiterResultCompleted) {
144+
return;
145+
}
146+
147+
NSSet *expectedObjects = [NSSet setWithObjects:@"one", @"two", nil];
148+
XCTAssertEqualObjects(enumeratedObjects, expectedObjects);
149+
XCTAssertTrue([set containsObject:@"addedFromCallback"]);
150+
}
151+
79152
- (void)testConcurrentReadWrites {
80153
NSArray *inputs = @[@"Test1", @"Test2", @"Test3", @"Test4", @"Test5"];
81154

0 commit comments

Comments
 (0)