Skip to content

Commit 1ebf3f9

Browse files
Fix random failure in MTRPerControllerStorageTests testSubscriptionPool
Two fixes here: 1. We were not consistently dispatching the unitTestSubscriptionPoolWorkComplete notification, because when _clearSubscriptionPoolWork was called from dealloc we had already cleared our delegate list before the call. This was the proximate cause of the random failure: when we got deallocated before the work item for our subscription pool work got dequeued, we would end up with subscriptionRunningCount too high (since it would increment on enqueue but we would miss the decrement. 2. The fake device is not consistent about whether it actually dispatches the unitTestSubscriptionPoolDequeue notification: usually it does not (because it was dealloced before that happened), but sometimes it does. When it does, we were ending up with more dequeue notifications than expected in the test. The fix for this is to not track the dequeue/complete bits in the test for the fake device at all. Fixes project-chip#38797
1 parent f89539f commit 1ebf3f9

File tree

2 files changed

+33
-21
lines changed

2 files changed

+33
-21
lines changed

src/darwin/Framework/CHIP/MTRDevice_Concrete.mm

+17-4
Original file line numberDiff line numberDiff line change
@@ -518,9 +518,9 @@ - (void)dealloc
518518

519519
[[NSNotificationCenter defaultCenter] removeObserver:_systemTimeChangeObserverToken];
520520

521+
__block id testDelegate = nil;
521522
#ifdef DEBUG
522523
// Save the first delegate for testing
523-
__block id testDelegate = nil;
524524
for (MTRDeviceDelegateInfo * delegateInfo in _delegates) {
525525
testDelegate = delegateInfo.delegate;
526526
break;
@@ -542,7 +542,7 @@ - (void)dealloc
542542

543543
// Clear this device from subscription pool and persist cached data to storage as needed.
544544
std::lock_guard lock(_lock);
545-
[self _clearSubscriptionPoolWork];
545+
[self _clearSubscriptionPoolWorkWithProvidedDelegate:testDelegate];
546546
[self _doPersistClusterData];
547547
}
548548

@@ -1381,16 +1381,29 @@ - (BOOL)_deviceUsesThread
13811381
}
13821382

13831383
- (void)_clearSubscriptionPoolWork
1384+
{
1385+
[self _clearSubscriptionPoolWorkWithProvidedDelegate:nil];
1386+
}
1387+
1388+
// We provide a delegate to notify if we are doing
1389+
// _clearSubscriptionPoolWorkWithProvidedDelegate after clearing our delegates
1390+
// in dealloc.
1391+
- (void)_clearSubscriptionPoolWorkWithProvidedDelegate:(nullable id)providedDelegate
13841392
{
13851393
os_unfair_lock_assert_owner(&self->_lock);
13861394
MTRAsyncWorkCompletionBlock completion = self->_subscriptionPoolWorkCompletionBlock;
13871395
if (completion) {
13881396
#ifdef DEBUG
1389-
[self _callDelegatesWithBlock:^(id testDelegate) {
1397+
auto notificationBlock = ^(id testDelegate) {
13901398
if ([testDelegate respondsToSelector:@selector(unitTestSubscriptionPoolWorkComplete:)]) {
13911399
[testDelegate unitTestSubscriptionPoolWorkComplete:self];
13921400
}
1393-
}];
1401+
};
1402+
if (providedDelegate != nil) {
1403+
notificationBlock(providedDelegate);
1404+
} else {
1405+
[self _callDelegatesWithBlock:notificationBlock];
1406+
}
13941407
#endif
13951408
self->_subscriptionPoolWorkCompletionBlock = nil;
13961409
completion(MTRAsyncWorkComplete);

src/darwin/Framework/CHIPTests/MTRPerControllerStorageTests.m

+16-17
Original file line numberDiff line numberDiff line change
@@ -2811,13 +2811,6 @@ - (void)doTestSubscriptionPoolWithSize:(NSInteger)subscriptionPoolSize deviceOnb
28112811

28122812
NSArray<NSNumber *> * orderedDeviceIDs = [deviceOnboardingPayloads allKeys];
28132813

2814-
// We have one fake device, which we use an invalid node ID for, that we use
2815-
// to test what happens if a device is deallocated while its subscription
2816-
// work item is queued.
2817-
NSMutableArray<NSNumber *> * orderedDeviceIDsIncludingFake = [orderedDeviceIDs mutableCopy];
2818-
NSNumber * fakeDeviceID = @(0xFFFFFFFFFFFFFFFF);
2819-
[orderedDeviceIDsIncludingFake insertObject:fakeDeviceID atIndex:0];
2820-
28212814
// Commission 5 devices
28222815
for (NSNumber * deviceID in orderedDeviceIDs) {
28232816
certificateIssuer.nextNodeID = deviceID;
@@ -2851,7 +2844,7 @@ - (void)doTestSubscriptionPoolWithSize:(NSInteger)subscriptionPoolSize deviceOnb
28512844
}
28522845

28532846
NSMutableDictionary<NSNumber *, MTRDeviceTestDelegate *> * deviceDelegates = [NSMutableDictionary dictionary];
2854-
for (NSNumber * deviceID in orderedDeviceIDsIncludingFake) {
2847+
for (NSNumber * deviceID in orderedDeviceIDs) {
28552848
deviceDelegates[deviceID] = [[MTRDeviceTestDelegate alloc] init];
28562849
}
28572850

@@ -2861,7 +2854,7 @@ - (void)doTestSubscriptionPoolWithSize:(NSInteger)subscriptionPoolSize deviceOnb
28612854
__block NSUInteger subscriptionDequeueCount = 0;
28622855
__block BOOL baseDeviceReadCompleted = NO;
28632856

2864-
for (NSNumber * deviceID in orderedDeviceIDsIncludingFake) {
2857+
for (NSNumber * deviceID in orderedDeviceIDs) {
28652858
MTRDeviceTestDelegate * delegate = deviceDelegates[deviceID];
28662859
delegate.pretendThreadEnabled = YES;
28672860

@@ -2890,17 +2883,17 @@ - (void)doTestSubscriptionPoolWithSize:(NSInteger)subscriptionPoolSize deviceOnb
28902883
};
28912884
__weak __auto_type weakDelegate = delegate;
28922885
delegate.onReportEnd = ^{
2893-
// We don't have a subscriptionExpectation for the fake device.
2894-
XCTestExpectation * subscriptionExpectation = subscriptionExpectations[deviceID];
2895-
if (subscriptionExpectation) {
2896-
[subscriptionExpectation fulfill];
2897-
}
2886+
[subscriptionExpectations[deviceID] fulfill];
28982887
// reset callback so expectation not fulfilled twice, given the run time of this can be long due to subscription pool
28992888
__strong __auto_type strongDelegate = weakDelegate;
29002889
strongDelegate.onReportEnd = nil;
29012890
};
29022891
}
29032892

2893+
// We have one fake device, which we use an invalid node ID for, that we use
2894+
// to test what happens if a device is deallocated while its subscription
2895+
// work item is queued.
2896+
//
29042897
// Schedule a job in the controller's pool that will not complete until the
29052898
// fake device is deallocated.
29062899
__auto_type * fakeDeviceDeletedExpectation = [self expectationWithDescription:@"Fake device deleted"];
@@ -2911,18 +2904,24 @@ - (void)doTestSubscriptionPoolWithSize:(NSInteger)subscriptionPoolSize deviceOnb
29112904
}];
29122905
[controller.concurrentSubscriptionPool enqueueWorkItem:blockingWorkItem description:@"Waiting for fake device deletion"];
29132906

2907+
// Make sure the delegate for the fake device does not go away before the
2908+
// fake device does, so keep it out of the @autoreleasepool block.
2909+
MTRDeviceTestDelegate * fakeDeviceDelegate = [[MTRDeviceTestDelegate alloc] init];
2910+
fakeDeviceDelegate.pretendThreadEnabled = YES;
2911+
29142912
@autoreleasepool {
29152913
// Create our fake device and have it dealloc before the blocking WorkItem
29162914
// completes and hence before its subscription work item gets a chance
29172915
// to run (in the width-1 case).
2918-
MTRDeviceTestDelegate * delegate = deviceDelegates[fakeDeviceID];
2916+
29192917
// onSubscriptionCallbackDelete is called from dealloc
2920-
delegate.onSubscriptionCallbackDelete = ^{
2918+
fakeDeviceDelegate.onSubscriptionCallbackDelete = ^{
29212919
[fakeDeviceDeletedExpectation fulfill];
29222920
};
29232921

2922+
NSNumber * fakeDeviceID = @(0xFFFFFFFFFFFFFFFF);
29242923
__auto_type * device = [MTRDevice deviceWithNodeID:fakeDeviceID controller:controller];
2925-
[device setDelegate:delegate queue:queue];
2924+
[device setDelegate:fakeDeviceDelegate queue:queue];
29262925
}
29272926

29282927
for (NSNumber * deviceID in orderedDeviceIDs) {

0 commit comments

Comments
 (0)