Skip to content

Commit

Permalink
Add a helper class to trace GCDMulticastDelegate invocation context
Browse files Browse the repository at this point in the history
  • Loading branch information
pwetrifork committed Nov 10, 2017
1 parent f15b9b4 commit ba08cf5
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 3 deletions.
41 changes: 40 additions & 1 deletion Utilities/GCDMulticastDelegate.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#import <Foundation/Foundation.h>

@class GCDMulticastDelegateEnumerator;
@class GCDMulticastDelegateEnumerator, GCDMulticastDelegateInvocationContext;

/**
* This class provides multicast delegate functionality. That is:
Expand Down Expand Up @@ -54,5 +54,44 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr ofClass:(Class)aClass;
- (BOOL)getNextDelegate:(id _Nullable * _Nonnull)delPtr delegateQueue:(dispatch_queue_t _Nullable * _Nonnull)dqPtr forSelector:(SEL)aSelector;

@end

/**
* A helper class for propagating custom context across multicast delegate invocations.
*
* This class serves 2 main purposes:
* - provides an auxiliary path of custom data delivery to the invoked delegate methods
* - makes it possible to track the delegate method invocation completion
*
* The context propagates along the cascade of invocations, i.e. when a delegate method calls another multicast delegate,
* that subsequent invocation belongs to the same context. This is particularly relevant w.r.t. the xmpp framework
* architecture where a scenario with two layers of delegation is common: stream -> module and module -> application.
* The propagating context is what enables the framework to deliver stream event-related information across modules
* to the application callbacks.
*
* The default context propagation junctions are invocation forwarding and delegate enumerator creation. As long as
* they are executed under an existing context, the propagation is automatic.
*
* A manual propagation scenario (e.g. asynchronous message processing within a module) would consist of the following steps:
* 1. Capturing the context object while still in the delegate callback context with @c currentContext.
* 2. Entering the captured context's @c continuityGroup.
* 3. Restoring the context on an arbitrary queue with @c becomeCurrentOnQueue:forActionWithBlock:
* 4. Leaving the @c continuityGroup within the action block submitted to @c becomeCurrentOnQueue:forActionWithBlock:
*
* Steps 2. and 4. are only required if @c becomeCurrentOnQueue:forActionWithBlock: itself is invoked asynchronously
* (e.g. in a network or disk IO completion callback).
*/
@interface GCDMulticastDelegateInvocationContext : NSObject

@property (nonatomic, strong, readonly) id value;
@property (nonatomic, strong, readonly) dispatch_group_t continuityGroup;

+ (instancetype)currentContext;

- (instancetype)initWithValue:(id)value;
- (instancetype)init NS_UNAVAILABLE;

- (void)becomeCurrentOnQueue:(dispatch_queue_t)queue forActionWithBlock:(dispatch_block_t)block;

@end
NS_ASSUME_NONNULL_END
76 changes: 74 additions & 2 deletions Utilities/GCDMulticastDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

static void * const GCDMulticastDelegateInvocationContextKey = (void *)&GCDMulticastDelegateInvocationContextKey;

static void GCDMulticastDelegateInvocationContextLeave(void *contextPtr);

/**
* How does this class work?
*
Expand Down Expand Up @@ -76,6 +80,12 @@ - (id)initFromDelegateNodes:(NSMutableArray *)inDelegateNodes;

@end

@interface GCDMulticastDelegateInvocationContext ()

- (dispatch_queue_t)transferContextToTargetQueue:(dispatch_queue_t)targetQueue;

@end

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -227,7 +237,22 @@ - (BOOL)hasDelegateThatRespondsToSelector:(SEL)aSelector

- (GCDMulticastDelegateEnumerator *)delegateEnumerator
{
return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:delegateNodes];
NSMutableArray *actualDelegateNodes;

GCDMulticastDelegateInvocationContext *currentInvocationContext = [GCDMulticastDelegateInvocationContext currentContext];
if (currentInvocationContext) {
actualDelegateNodes = [[NSMutableArray alloc] init];
for (GCDMulticastDelegateNode *node in delegateNodes) {
dispatch_queue_t contextTransferQueue = [currentInvocationContext transferContextToTargetQueue:node.delegateQueue];
GCDMulticastDelegateNode *contextTransferNode = [[GCDMulticastDelegateNode alloc] initWithDelegate:node.delegate
delegateQueue:contextTransferQueue];
[actualDelegateNodes addObject:contextTransferNode];
}
} else {
actualDelegateNodes = delegateNodes;
}

return [[GCDMulticastDelegateEnumerator alloc] initFromDelegateNodes:actualDelegateNodes];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
Expand Down Expand Up @@ -276,7 +301,10 @@ - (void)forwardInvocation:(NSInvocation *)origInvocation

NSInvocation *dupInvocation = [self duplicateInvocation:origInvocation];

dispatch_async(node.delegateQueue, ^{ @autoreleasepool {
GCDMulticastDelegateInvocationContext *currentContext = [GCDMulticastDelegateInvocationContext currentContext];
dispatch_queue_t invocationQueue = [currentContext transferContextToTargetQueue:node.delegateQueue] ?: node.delegateQueue;

dispatch_async(invocationQueue, ^{ @autoreleasepool {

[dupInvocation invokeWithTarget:nodeDelegate];

Expand Down Expand Up @@ -652,3 +680,47 @@ - (BOOL)getNextDelegate:(id *)delPtr delegateQueue:(dispatch_queue_t *)dqPtr for
}

@end

@implementation GCDMulticastDelegateInvocationContext

+ (instancetype)currentContext
{
void *contextPtr = dispatch_get_specific(GCDMulticastDelegateInvocationContextKey);
return contextPtr ? CFBridgingRelease(CFRetain(contextPtr)) : nil;
}

- (instancetype)initWithValue:(id)value
{
self = [super init];
if (self) {
_continuityGroup = dispatch_group_create();
_value = value;
}
return self;
}

- (void)becomeCurrentOnQueue:(dispatch_queue_t)queue forActionWithBlock:(dispatch_block_t)block
{
dispatch_async([self transferContextToTargetQueue:queue], block);
}

- (dispatch_queue_t)transferContextToTargetQueue:(dispatch_queue_t)targetQueue
{
dispatch_group_enter(self.continuityGroup);

dispatch_queue_t contextTransferQueue = dispatch_queue_create_with_target("GCDMulticastDelegateInvocationContext.contextTransferQueue", nil, targetQueue);
dispatch_queue_set_specific(contextTransferQueue,
GCDMulticastDelegateInvocationContextKey,
(void *)CFBridgingRetain(self),
GCDMulticastDelegateInvocationContextLeave);

return contextTransferQueue;
}

@end

static void GCDMulticastDelegateInvocationContextLeave(void *contextPtr)
{
GCDMulticastDelegateInvocationContext *context = CFBridgingRelease(contextPtr);
dispatch_group_leave(context.continuityGroup);
}

0 comments on commit ba08cf5

Please sign in to comment.