Skip to content

Commit 1229899

Browse files
bzbarsky-applepull[bot]
authored andcommitted
Fix memory leaks in MTRAsyncCallbackQueueWorkItem. (#24817)
The item API pretty much requires a readyHandler that holds a reference to the item. Instead of assuming all consumers use __weak correctly to hold that reference, manually break cycles in MTRAsyncCallbackQueueWorkItem.
1 parent f90e2a3 commit 1229899

File tree

2 files changed

+111
-3
lines changed

2 files changed

+111
-3
lines changed

src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.mm

+25-3
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,23 @@ - (instancetype)initWithQueue:(dispatch_queue_t)queue
168168
return self;
169169
}
170170

171+
- (void)invalidate
172+
{
173+
// Make sure we don't leak via handlers that close over us, as ours must.
174+
// This is a bit odd, since these are supposed to be non-nullable
175+
// properties, but it's the best we can do given our API surface, unless we
176+
// assume that all consumers consistently use __weak refs to us inside their
177+
// handlers.
178+
//
179+
// Setting the attributes to nil will not compile; set the ivars directly.
180+
_readyHandler = nil;
181+
_cancelHandler = nil;
182+
}
183+
171184
- (void)endWork
172185
{
173186
[self.workQueue endWork:self];
187+
[self invalidate];
174188
}
175189

176190
- (void)retryWork
@@ -182,16 +196,24 @@ - (void)retryWork
182196
- (void)callReadyHandlerWithContext:(id)context
183197
{
184198
dispatch_async(self.queue, ^{
185-
self.readyHandler(context, self.retryCount);
186-
self.retryCount++;
199+
if (self.readyHandler == nil) {
200+
// Nothing to do here.
201+
[self endWork];
202+
} else {
203+
self.readyHandler(context, self.retryCount);
204+
self.retryCount++;
205+
}
187206
});
188207
}
189208

190209
// Called by the work queue
191210
- (void)cancel
192211
{
193212
dispatch_async(self.queue, ^{
194-
self.cancelHandler();
213+
if (self.cancelHandler != nil) {
214+
self.cancelHandler();
215+
}
216+
[self invalidate];
195217
});
196218
}
197219
@end

src/darwin/Framework/CHIPTests/MTRAsyncCallbackQueueTests.m

+86
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ - (void)testRunItem
4646
};
4747
[workQueue enqueueWorkItem:workItem1];
4848

49+
// Check for leaks.
50+
MTRAsyncCallbackQueueWorkItem * __weak weakItem = workItem1;
51+
[self addTeardownBlock:^() {
52+
XCTAssertNil(weakItem);
53+
}];
54+
4955
[self waitForExpectationsWithTimeout:5 handler:nil];
5056

5157
// see that it only ran once
@@ -170,4 +176,84 @@ - (void)testRunItemsAfterDrain
170176
[self waitForExpectationsWithTimeout:5 handler:nil];
171177
}
172178

179+
- (void)testRunItemNoHandlers
180+
{
181+
XCTestExpectation * expectation = [self expectationWithDescription:@"Work item called"];
182+
183+
MTRAsyncCallbackWorkQueue * workQueue = [[MTRAsyncCallbackWorkQueue alloc] initWithContext:nil queue:dispatch_get_main_queue()];
184+
185+
MTRAsyncCallbackQueueWorkItem * workItem1 =
186+
[[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)];
187+
MTRAsyncCallbackQueueWorkItem * workItem2 =
188+
[[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)];
189+
190+
__block int counter = 0;
191+
MTRAsyncCallbackReadyHandler readyHandler = ^(MTRDevice * _Nonnull device, NSUInteger retryCount) {
192+
counter++;
193+
[workItem2 endWork];
194+
[expectation fulfill];
195+
};
196+
workItem2.readyHandler = readyHandler;
197+
workItem2.cancelHandler = ^{
198+
};
199+
200+
// Check that trying to run workItem1 does not crash.
201+
[workQueue enqueueWorkItem:workItem1];
202+
[workQueue enqueueWorkItem:workItem2];
203+
204+
[self waitForExpectationsWithTimeout:5 handler:nil];
205+
206+
// see that it only ran once
207+
XCTAssertEqual(counter, 1);
208+
}
209+
210+
- (void)testInvalidation
211+
{
212+
XCTestExpectation * expectation = [self expectationWithDescription:@"Work item called"];
213+
XCTestExpectation * cancelExpectation = [self expectationWithDescription:@"Work item canceled"];
214+
215+
MTRAsyncCallbackWorkQueue * workQueue = [[MTRAsyncCallbackWorkQueue alloc] initWithContext:nil queue:dispatch_get_main_queue()];
216+
217+
MTRAsyncCallbackQueueWorkItem * workItem1 =
218+
[[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)];
219+
MTRAsyncCallbackReadyHandler readyHandler1 = ^(MTRDevice * _Nonnull device, NSUInteger retryCount) {
220+
// Give the code enqueing the other items a chance to run, so they can
221+
// actually get canceled.
222+
sleep(1);
223+
[workQueue invalidate];
224+
[workItem1 endWork];
225+
[expectation fulfill];
226+
};
227+
workItem1.readyHandler = readyHandler1;
228+
// No cancel handler on purpose.
229+
[workQueue enqueueWorkItem:workItem1];
230+
231+
MTRAsyncCallbackQueueWorkItem * workItem2 =
232+
[[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)];
233+
MTRAsyncCallbackReadyHandler readyHandler2 = ^(MTRDevice * _Nonnull device, NSUInteger retryCount) {
234+
// This should never get called.
235+
XCTAssertFalse(YES);
236+
[workItem2 endWork];
237+
};
238+
workItem2.readyHandler = readyHandler2;
239+
// No cancel handler on purpose.
240+
[workQueue enqueueWorkItem:workItem2];
241+
242+
MTRAsyncCallbackQueueWorkItem * workItem3 =
243+
[[MTRAsyncCallbackQueueWorkItem alloc] initWithQueue:dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)];
244+
MTRAsyncCallbackReadyHandler readyHandler3 = ^(MTRDevice * _Nonnull device, NSUInteger retryCount) {
245+
// This should never get called.
246+
XCTAssertFalse(YES);
247+
[workItem3 endWork];
248+
};
249+
dispatch_block_t cancelHandler3 = ^() {
250+
[cancelExpectation fulfill];
251+
};
252+
workItem3.readyHandler = readyHandler3;
253+
workItem3.cancelHandler = cancelHandler3;
254+
[workQueue enqueueWorkItem:workItem3];
255+
256+
[self waitForExpectations:@[ expectation, cancelExpectation ] timeout:5];
257+
}
258+
173259
@end

0 commit comments

Comments
 (0)