[Darwin] MTRAsyncCallbackWorkQueue tsan fix and API strengthening (#26800)
diff --git a/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.h b/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.h
index bad4ded..acb6da8 100644
--- a/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.h
+++ b/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.h
@@ -54,6 +54,7 @@
- (void)invalidate;
// Work items may be enqueued from any queue or thread
+// Note: Once a work item is enqueued, its handlers cannot be modified
- (void)enqueueWorkItem:(MTRAsyncCallbackQueueWorkItem *)item;
// TODO: Add a "set concurrency width" method to allow for more than 1 work item at a time
@@ -71,10 +72,12 @@
// Called by the creater of the work item when async work is done and should
// be removed from the queue. The work queue will run the next work item.
+// Note: This must only be called from within the readyHandler
- (void)endWork;
// Called by the creater of the work item when async work should be retried.
// The work queue will call this workItem's readyHandler again.
+// Note: This must only be called from within the readyHandler
- (void)retryWork;
@end
diff --git a/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.mm b/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.mm
index e97ee15..2b31d07 100644
--- a/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.mm
+++ b/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue.mm
@@ -41,10 +41,13 @@
@end
@interface MTRAsyncCallbackQueueWorkItem ()
+@property (nonatomic, readonly) os_unfair_lock lock;
@property (nonatomic, strong, readonly) dispatch_queue_t queue;
@property (nonatomic, readwrite) NSUInteger retryCount;
@property (nonatomic, strong) MTRAsyncCallbackWorkQueue * workQueue;
+@property (nonatomic, readonly) BOOL enqueued;
// Called by the queue
+- (void)markedEnqueued;
- (void)callReadyHandlerWithContext:(id)context;
- (void)cancel;
@end
@@ -72,6 +75,13 @@
- (void)enqueueWorkItem:(MTRAsyncCallbackQueueWorkItem *)item
{
+ if (item.enqueued) {
+ MTR_LOG_ERROR("MTRAsyncCallbackWorkQueue enqueueWorkItem: item cannot be enqueued twice");
+ return;
+ }
+
+ [item markedEnqueued];
+
os_unfair_lock_lock(&_lock);
item.workQueue = self;
[self.items addObject:item];
@@ -163,12 +173,14 @@
- (instancetype)initWithQueue:(dispatch_queue_t)queue
{
if (self = [super init]) {
+ _lock = OS_UNFAIR_LOCK_INIT;
_queue = queue;
}
return self;
}
-- (void)invalidate
+// assume lock is held
+- (void)_invalidate
{
// Make sure we don't leak via handlers that close over us, as ours must.
// This is a bit odd, since these are supposed to be non-nullable
@@ -181,6 +193,38 @@
_cancelHandler = nil;
}
+- (void)invalidate
+{
+ os_unfair_lock_lock(&_lock);
+ [self _invalidate];
+ os_unfair_lock_unlock(&_lock);
+}
+
+- (void)markedEnqueued
+{
+ os_unfair_lock_lock(&_lock);
+ _enqueued = YES;
+ os_unfair_lock_unlock(&_lock);
+}
+
+- (void)setReadyHandler:(MTRAsyncCallbackReadyHandler)readyHandler
+{
+ os_unfair_lock_lock(&_lock);
+ if (!_enqueued) {
+ _readyHandler = readyHandler;
+ }
+ os_unfair_lock_unlock(&_lock);
+}
+
+- (void)setCancelHandler:(dispatch_block_t)cancelHandler
+{
+ os_unfair_lock_lock(&_lock);
+ if (!_enqueued) {
+ _cancelHandler = cancelHandler;
+ }
+ os_unfair_lock_unlock(&_lock);
+}
+
- (void)endWork
{
[self.workQueue endWork:self];
@@ -196,12 +240,19 @@
- (void)callReadyHandlerWithContext:(id)context
{
dispatch_async(self.queue, ^{
- if (self.readyHandler == nil) {
+ os_unfair_lock_lock(&self->_lock);
+ MTRAsyncCallbackReadyHandler readyHandler = self->_readyHandler;
+ NSUInteger retryCount = self->_retryCount;
+ if (readyHandler) {
+ self->_retryCount++;
+ }
+ os_unfair_lock_unlock(&self->_lock);
+
+ if (readyHandler == nil) {
// Nothing to do here.
[self endWork];
} else {
- self.readyHandler(context, self.retryCount);
- self.retryCount++;
+ readyHandler(context, retryCount);
}
});
}
@@ -209,11 +260,15 @@
// Called by the work queue
- (void)cancel
{
- dispatch_async(self.queue, ^{
- if (self.cancelHandler != nil) {
- self.cancelHandler();
- }
- [self invalidate];
- });
+ os_unfair_lock_lock(&self->_lock);
+ dispatch_block_t cancelHandler = self->_cancelHandler;
+ [self _invalidate];
+ os_unfair_lock_unlock(&self->_lock);
+
+ if (cancelHandler) {
+ dispatch_async(self.queue, ^{
+ cancelHandler();
+ });
+ }
}
@end
diff --git a/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue_Internal.h b/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue_Internal.h
index 35eb765..49e6343 100644
--- a/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRAsyncCallbackWorkQueue_Internal.h
@@ -26,9 +26,6 @@
@interface MTRAsyncCallbackWorkQueue ()
// The MTRDevice object is only held and passed back as a reference and is opaque to the queue
- (instancetype)initWithContext:(id _Nullable)context queue:(dispatch_queue_t)queue;
-
-// Called by DeviceController at device clean up time
-- (void)invalidate;
@end
NS_ASSUME_NONNULL_END