Skip to content

Commit 06ab635

Browse files
sebalandAndrei Konovalov
andauthored
fix(IoT): Fixing race conditions during cleanup in AWSIoTStreamThread (#5477)
--------- Co-authored-by: Andrei Konovalov <[email protected]>
1 parent 80711f4 commit 06ab635

File tree

4 files changed

+122
-57
lines changed

4 files changed

+122
-57
lines changed

AWSIoT/Internal/AWSIoTStreamThread.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
2020

2121
@interface AWSIoTStreamThread : NSThread
2222

23-
@property(strong, nullable) void (^onStop)(void);
23+
@property(nonatomic, copy, nullable) void (^onStop)(void);
2424

2525
-(instancetype)initWithSession:(nonnull AWSMQTTSession *)session
2626
decoderInputStream:(nonnull NSInputStream *)decoderInputStream

AWSIoT/Internal/AWSIoTStreamThread.m

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ @interface AWSIoTStreamThread()
2727
@property(nonatomic, assign) NSTimeInterval defaultRunLoopTimeInterval;
2828
@property(nonatomic, assign) BOOL isRunning;
2929
@property(nonatomic, assign) BOOL shouldDisconnect;
30+
@property(nonatomic, strong) dispatch_queue_t serialQueue;
31+
@property(nonatomic, assign) BOOL didCleanUp;
3032
@end
3133

3234
@implementation AWSIoTStreamThread
@@ -40,34 +42,42 @@ - (nonnull instancetype)initWithSession:(nonnull AWSMQTTSession *)session
4042
outputStream:nil];
4143
}
4244

43-
-(instancetype)initWithSession:(nonnull AWSMQTTSession *)session
44-
decoderInputStream:(nonnull NSInputStream *)decoderInputStream
45-
encoderOutputStream:(nonnull NSOutputStream *)encoderOutputStream
46-
outputStream:(nullable NSOutputStream *)outputStream; {
45+
- (instancetype)initWithSession:(nonnull AWSMQTTSession *)session
46+
decoderInputStream:(nonnull NSInputStream *)decoderInputStream
47+
encoderOutputStream:(nonnull NSOutputStream *)encoderOutputStream
48+
outputStream:(nullable NSOutputStream *)outputStream {
4749
if (self = [super init]) {
4850
_session = session;
4951
_decoderInputStream = decoderInputStream;
5052
_encoderOutputStream = encoderOutputStream;
5153
_outputStream = outputStream;
5254
_defaultRunLoopTimeInterval = 10;
5355
_shouldDisconnect = NO;
56+
_serialQueue = dispatch_queue_create("com.amazonaws.iot.streamthread.syncQueue", DISPATCH_QUEUE_SERIAL);
57+
_didCleanUp = NO;
5458
}
5559
return self;
5660
}
5761

5862
- (void)main {
63+
if (self.isRunning) {
64+
AWSDDLogWarn(@"Attempted to start a thread that is already running: [%@]", self);
65+
return;
66+
}
67+
5968
AWSDDLogVerbose(@"Started execution of Thread: [%@]", self);
6069
//This is invoked in a new thread by the webSocketDidOpen method or by the Connect method. Get the runLoop from the thread.
6170
self.runLoopForStreamsThread = [NSRunLoop currentRunLoop];
6271

6372
//Setup a default timer to ensure that the RunLoop always has atleast one timer on it. This is to prevent the while loop
6473
//below to spin in tight loop when all input sources and session timers are shutdown during a reconnect sequence.
74+
__weak typeof(self) weakSelf = self;
6575
self.defaultRunLoopTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:60.0]
66-
interval:60.0
67-
target:self
68-
selector:@selector(timerHandler:)
69-
userInfo:nil
70-
repeats:YES];
76+
interval:60.0
77+
repeats:YES
78+
block:^(NSTimer * _Nonnull timer) {
79+
AWSDDLogVerbose(@"Default run loop timer executed on Thread: [%@]. isRunning = %@. isCancelled = %@", weakSelf, weakSelf.isRunning ? @"YES" : @"NO", weakSelf.isCancelled ? @"YES" : @"NO");
80+
}];
7181
[self.runLoopForStreamsThread addTimer:self.defaultRunLoopTimer
7282
forMode:NSDefaultRunLoopMode];
7383

@@ -82,7 +92,7 @@ - (void)main {
8292
[self.session connectToInputStream:self.decoderInputStream
8393
outputStream:self.encoderOutputStream];
8494

85-
while (self.isRunning && !self.isCancelled) {
95+
while ([self shouldContinueRunning]) {
8696
//This will continue run until the thread is cancelled
8797
//Run one cycle of the runloop. This will return after a input source event or timer event is processed
8898
[self.runLoopForStreamsThread runMode:NSDefaultRunLoopMode
@@ -94,60 +104,76 @@ - (void)main {
94104
AWSDDLogVerbose(@"Finished execution of Thread: [%@]", self);
95105
}
96106

107+
- (BOOL)shouldContinueRunning {
108+
__block BOOL shouldRun;
109+
dispatch_sync(self.serialQueue, ^{
110+
shouldRun = self.isRunning && !self.isCancelled && self.defaultRunLoopTimer != nil;
111+
});
112+
return shouldRun;
113+
}
114+
97115
- (void)cancel {
98116
AWSDDLogVerbose(@"Issued Cancel on thread [%@]", (NSThread *)self);
99-
self.isRunning = NO;
100-
[super cancel];
117+
dispatch_sync(self.serialQueue, ^{
118+
self.isRunning = NO;
119+
[super cancel];
120+
});
101121
}
102122

103123
- (void)cancelAndDisconnect:(BOOL)shouldDisconnect {
104124
AWSDDLogVerbose(@"Issued Cancel and Disconnect = [%@] on thread [%@]", shouldDisconnect ? @"YES" : @"NO", (NSThread *)self);
105-
self.shouldDisconnect = shouldDisconnect;
106-
self.isRunning = NO;
107-
[super cancel];
125+
dispatch_sync(self.serialQueue, ^{
126+
self.shouldDisconnect = shouldDisconnect;
127+
self.isRunning = NO;
128+
[super cancel];
129+
});
108130
}
109131

110132
- (void)cleanUp {
111-
if (self.defaultRunLoopTimer) {
112-
[self.defaultRunLoopTimer invalidate];
113-
self.defaultRunLoopTimer = nil;
114-
}
115-
116-
if (self.shouldDisconnect) {
117-
if (self.session) {
118-
[self.session close];
119-
self.session = nil;
133+
dispatch_sync(self.serialQueue, ^{
134+
if (self.didCleanUp) {
135+
AWSDDLogVerbose(@"Clean up already called for thread: [%@]", (NSThread *)self);
136+
return;
120137
}
121138

122-
if (self.outputStream) {
123-
self.outputStream.delegate = nil;
124-
[self.outputStream close];
125-
[self.outputStream removeFromRunLoop:self.runLoopForStreamsThread
126-
forMode:NSDefaultRunLoopMode];
127-
self.outputStream = nil;
139+
self.didCleanUp = YES;
140+
if (self.defaultRunLoopTimer) {
141+
[self.defaultRunLoopTimer invalidate];
142+
self.defaultRunLoopTimer = nil;
128143
}
129144

130-
if (self.decoderInputStream) {
131-
[self.decoderInputStream close];
132-
self.decoderInputStream = nil;
145+
if (self.shouldDisconnect) {
146+
if (self.session) {
147+
[self.session close];
148+
self.session = nil;
149+
}
150+
151+
if (self.outputStream) {
152+
self.outputStream.delegate = nil;
153+
[self.outputStream close];
154+
[self.outputStream removeFromRunLoop:self.runLoopForStreamsThread
155+
forMode:NSDefaultRunLoopMode];
156+
self.outputStream = nil;
157+
}
158+
159+
if (self.decoderInputStream) {
160+
[self.decoderInputStream close];
161+
self.decoderInputStream = nil;
162+
}
163+
164+
if (self.encoderOutputStream) {
165+
[self.encoderOutputStream close];
166+
self.encoderOutputStream = nil;
167+
}
168+
} else {
169+
AWSDDLogVerbose(@"Skipping disconnect for thread: [%@]", (NSThread *)self);
133170
}
134171

135-
if (self.encoderOutputStream) {
136-
[self.encoderOutputStream close];
137-
self.encoderOutputStream = nil;
172+
if (self.onStop) {
173+
self.onStop();
174+
self.onStop = nil;
138175
}
139-
} else {
140-
AWSDDLogVerbose(@"Skipping disconnect for thread: [%@]", (NSThread *)self);
141-
}
142-
143-
if (self.onStop) {
144-
self.onStop();
145-
self.onStop = nil;
146-
}
147-
}
148-
149-
- (void)timerHandler:(NSTimer*)theTimer {
150-
AWSDDLogVerbose(@"Default run loop timer executed on Thread: [%@]. isRunning = %@. isCancelled = %@", self, self.isRunning ? @"YES" : @"NO", self.isCancelled ? @"YES" : @"NO");
176+
});
151177
}
152178

153179
@end

AWSIoTUnitTests/AWSIoTStreamThreadTests.m

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
@interface AWSIoTStreamThread()
2121
@property(nonatomic, assign) NSTimeInterval defaultRunLoopTimeInterval;
22+
@property (nonatomic, assign) BOOL isRunning;
23+
@property (nonatomic, strong) dispatch_queue_t serialQueue;
24+
@property (nonatomic, assign) BOOL didCleanUp;
25+
@property (nonatomic, strong, nullable) NSTimer *defaultRunLoopTimer;
2226
@end
2327

2428

@@ -56,6 +60,7 @@ - (void)setUp {
5660
}
5761

5862
- (void)tearDown {
63+
[self.thread cancelAndDisconnect:YES];
5964
self.thread = nil;
6065
self.session = nil;
6166
self.decoderInputStream = nil;
@@ -67,6 +72,7 @@ - (void)tearDown {
6772
/// When: The thread is started
6873
/// Then: The output stream is opened and the session is connected to the decoder and encoder streams
6974
- (void)testStart_shouldOpenStream_andInvokeConnectOnSession {
75+
OCMVerify([self.outputStream scheduleInRunLoop:[OCMArg any] forMode:NSDefaultRunLoopMode]);
7076
OCMVerify([self.outputStream open]);
7177
OCMVerify([self.session connectToInputStream:[OCMArg any] outputStream:[OCMArg any]]);
7278
}
@@ -87,6 +93,7 @@ - (void)testCancelAndDisconnect_shouldCloseStreams_andInvokeOnStop {
8793
OCMVerify([self.encoderOutputStream close]);
8894
OCMVerify([self.outputStream close]);
8995
OCMVerify([self.session close]);
96+
XCTAssertFalse(self.thread.isRunning);
9097
}
9198

9299
/// Given: A running AWSIoTStreamThread
@@ -108,9 +115,9 @@ - (void)testCancel_shouldNotCloseStreams_andInvokeOnStop {
108115
didInvokeDecoderInputStreamClose = YES;
109116
}];
110117

111-
__block BOOL didInvokeEncoderDecoderInputStreamClose = NO;
118+
__block BOOL didInvokeEncoderOutputStreamClose = NO;
112119
[OCMStub([self.encoderOutputStream close]) andDo:^(NSInvocation *invocation) {
113-
didInvokeEncoderDecoderInputStreamClose = YES;
120+
didInvokeEncoderOutputStreamClose = YES;
114121
}];
115122

116123
__block BOOL didInvokeOutputStreamClose = NO;
@@ -121,10 +128,41 @@ - (void)testCancel_shouldNotCloseStreams_andInvokeOnStop {
121128
[self.thread cancelAndDisconnect:NO];
122129
[self waitForExpectations:@[stopExpectation] timeout:1];
123130

124-
XCTAssertFalse(didInvokeSessionClose);
125-
XCTAssertFalse(didInvokeDecoderInputStreamClose);
126-
XCTAssertFalse(didInvokeEncoderDecoderInputStreamClose);
127-
XCTAssertFalse(didInvokeOutputStreamClose);
131+
XCTAssertFalse(didInvokeSessionClose, @"The `close` method on `session` should not be invoked");
132+
XCTAssertFalse(didInvokeDecoderInputStreamClose, @"The `close` method on `decoderInputStream` should not be invoked");
133+
XCTAssertFalse(didInvokeEncoderOutputStreamClose, @"The `close` method on `encoderOutputStream` should not be invoked");
134+
XCTAssertFalse(didInvokeOutputStreamClose, @"The `close` method on `outputStream` should not be invoked");
135+
}
136+
137+
- (void)testCancelAndDisconnect_shouldSetDidCleanUp_andInvalidateTimer {
138+
XCTestExpectation *stopExpectation = [self expectationWithDescription:@"AWSIoTStreamThread.onStop expectation"];
139+
self.thread.onStop = ^{
140+
[stopExpectation fulfill];
141+
};
142+
143+
[self.thread cancelAndDisconnect:YES];
144+
145+
[self waitForExpectations:@[stopExpectation] timeout:1];
146+
XCTAssertTrue(self.thread.didCleanUp, @"didCleanUp should be YES after cleanup");
147+
XCTAssertNil(self.thread.defaultRunLoopTimer, @"defaultRunLoopTimer should be nil after invalidation");
148+
}
149+
150+
- (void)testCancelAndDisconnect_shouldSynchronizeOnCleanupQueue {
151+
XCTestExpectation *stopExpectation = [self expectationWithDescription:@"AWSIoTStreamThread.onStop expectation"];
152+
self.thread.onStop = ^{
153+
[stopExpectation fulfill];
154+
};
155+
156+
[self.thread cancelAndDisconnect:YES];
157+
158+
// Validate synchronization
159+
__block BOOL didSynchronize = NO;
160+
dispatch_sync(self.thread.serialQueue, ^{
161+
didSynchronize = YES;
162+
});
163+
164+
XCTAssertTrue(didSynchronize, @"The cleanupQueue should synchronize the operations");
165+
[self waitForExpectations:@[stopExpectation] timeout:1];
128166
}
129167

130168
@end

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
### Bug Fixes
66

77
- **AWSIoT**
8-
- Fixing a race condition when invalidating/creating the reconnect timer (#5454)
9-
- Fixing a potential race condition in the timer ring queue (#5461)
8+
- Fixing race conditions during cleanup in `AWSIoTStreamThread` (#5477)
9+
- Fixing a race condition when invalidating/creating the reconnect timer in `AWSIoTMQTTClient` (#5454)
10+
- Fixing a potential race condition in the timer ring queue in `AWSMQTTSession` (#5461)
1011

1112
## 2.38.0
1213

0 commit comments

Comments
 (0)