Johnathon Sullinger
Johnathon Sullinger

Reputation: 7414

Running blocks on the same thread

I am running into a bit of an issue that I can't figure out. If I have a method signature with a Block parameter with a callback in it, and within my method I use a API that has another Block, the API executes async and my code continues on and invokes my callback method. This lets my view controller know that everything is completed, when in fact often times it's not due to the API block.

Since my method always invokes the callback method (and is ran async) can I force the API call that uses a block to run synchronously?

View Controller

[self.tagRepo sync:^(BOOL finished) {
    if (finished) {
        NSLog(@"Sync completed.");
        if (self.tagRepo.isSyncing)
            NSLog(@"Syncing continues...");
    }
}];

[tagRepo Sync]

- (void)sync:(void(^)(BOOL finished))completed {
    if (self.isSyncing)
        return;

    NSLog(@"Synchronizing tags...");
    self.isSyncing = YES;

    [[EvernoteNoteStore noteStore] getSyncStateWithSuccess:^(EDAMSyncState *syncState) {
        BOOL performCallback = YES;

        if (syncState.fullSyncBefore > lastSyncTime) {
            [self processSync:syncState];
        } else if (syncState.updateCount > self.lastUpdateCount) {
            [self processSync:syncState];
        } else {
            performCallback = NO; // Block calling the call back when we begin the listTagsWithSuccess async.
            self.clientTags = [[self fetchAll] mutableCopy];
            [[EvernoteNoteStore noteStore] listTagsWithSuccess:^(NSArray *tags) {
                self.serverTags = [tags mutableCopy];
                [self processClientTags]; // Only need to make sure client sends any new tags to the server.

                // invoke call back.
                self.isSyncing = NO;
                if (completed)
                    completed(YES);
            } failure:^(NSError *error) {
                self.isSyncing = NO;
                NSLog(@"Failed to list tags.");
            }];
        }

        self.isSyncing = NO;
        if (completed && performCallback)
            completed(YES);
    } failure:^(NSError *error) {
        self.isSyncing = NO;
        NSLog(@"Failed to process a sync.");

        if (completed)
            completed(NO);
    }];
}

What happens is that when I invoke the [sync] method, my NSLog's show that the callback method is invoked before any of my processSync methods are completed. This I am assuming is because the processSync method invocations take place within another block, so the completion block on the current thread goes ahead and gets called.

Am I using blocks in a incorrect manor or is there a typical way of handling callback's that have nested blocks within it. Should I be trying to run the secondary blocks on the current thread through some GCD dispatches? Set up KVO? The issue I ran in to with trying KVO was that the number of blocks within the API (Evernote's) used during a sync process varies, depending on what changes took place. So it's hard to determine when the NSNotificationCenter posts happen, what stage the sync is in and if it is completed/needs to execute a callback or post another notification. I assume there is a standard approach to getting around this. Any tips would be appreciated!

Johnathon.

Update 1

The following code gets called when I invoke the `[[EvernoteNoteStore noteStore] ^listTagsWithSuccess] method.

- (void)getSyncStateWithSuccess:(void(^)(EDAMSyncState *syncState))success 
                        failure:(void(^)(NSError *error))failure
{
    [self invokeAsyncIdBlock:^id {
        return [[self currentNoteStore] getSyncState:[self authenticationToken]];
    } success:success failure:failure];
}

- (void)listTagsWithSuccess:(void(^)(NSArray *tags))success
                    failure:(void(^)(NSError *error))failure
{
     [self invokeAsyncIdBlock:^id {
        return [[self currentNoteStore] listTags:[self authenticationToken]];
    } success:success failure:failure];
}

- (void)invokeAsyncIdBlock:(id(^)())block
                   success:(void(^)(id))success
                   failure:(void(^)(NSError *error))failure
{
    dispatch_async(self.session.queue, ^(void) {
        id retVal = nil;
        @try {
            if (block) {
                retVal = block();
                dispatch_async(dispatch_get_main_queue(),
                               ^{
                                   if (success) {
                                       success(retVal);
                                   }
                               });
            }
        }
        @catch (NSException *exception) {
            NSError *error = [self errorFromNSException:exception];
            [self processError:failure withError:error];
        }
    });
}

Update 2

I provide the processSync method to show what other async stuff is being used. Within the processSync method I make another Evernote SDK method invocation, and then it ends up invoking processTags. I omit the processServerTags because the code is large, but included processClientTags, which is essentially the same as what is littered all through the processServerTags. So essentially I have 3-4 nested Evernote SDK async blocks running.

- (void)processSync:(EDAMSyncState *)syncState {
    BOOL fullSync = NO;
    // If we have never updated, perform full sync.
    if (!self.lastUpdateCount)
        fullSync = YES;

    [[EvernoteNoteStore noteStore] getSyncChunkAfterUSN:self.currentChunkUSN maxEntries:200 fullSyncOnly:NO success:^(EDAMSyncChunk *syncChunk) {
        // Loop, re-grabbing the next chunk
        if (syncChunk.chunkHighUSN < syncChunk.updateCount) {
            // Cache the current sync chunk. Since only so much is handed to us
            // during this hand-shake, we cache and go get more.
            [self cacheSyncChunk:syncChunk];
            self.currentChunkUSN = syncChunk.chunkHighUSN;

            // Fetch more sync chunks.
            [self processSync:syncState];
        } else {
            // Retrieved the last sync chunk, cache it and begin processing.
            [self cacheSyncChunk:syncChunk];
            self.currentChunkUSN = syncChunk.chunkHighUSN;

            // Build list of server tags
            [self processTags];

            // Time stamp ourselves so we know when we last updated.
            self.lastSyncTime = [NSDate endateFromEDAMTimestamp:syncState.currentTime];
            self.lastUpdateCount = syncState.updateCount;
        }
    } failure:^(NSError *error) {
        NSLog(@"Failed to process full sync.");
    }];
}

- (void)processTags {

    // Process the tags found on the server first. We bring down any new tags from the server and cache them.
    // Handles any naming conflicts or duplicate conflicts that come up.
    self.clientTags = [[self fetchAll] mutableCopy];
    [self processServerTags];

    // Process client tags. We check if the client has tags that do not exist on the server and send them.
    [self processClientTags];

    // Delete any expunged tags that we still have cached.
    [self expungeTags];

    NSLog(@"Completed syncing tags.");
}

- (void)processClientTags {
    NSLog(@"Processing client tags - Ensuring server is current with client tags.");
    // Now we compare our local cache to the server, in the event new tags were created.
    // TODO: Test this.
    for (Tag *clientTag in self.clientTags) {
        // Predicate for comparing all client tags against server tags.
        // We compare GUID's and Names. Since we can't have duplicate's of either, we check.
        // It is possible that the client created a Tag (GUID #1) and created it on the server externally (GUID #2) but share the same name.
        // In this case, we have to rename them.
        NSPredicate *compareGuidPredicate = [NSPredicate predicateWithFormat:@"guid == %@", clientTag.guid];

        //Check if this note exists already on the server.
        if (![[self.serverTags filteredArrayUsingPredicate:compareGuidPredicate] count]) {
            // If not, we make sure it was never expunged.
            if ([self.expungedTags containsObject:clientTag.guid])
                continue;

            EDAMTag *serverTag = [[EDAMTag alloc] initWithGuid:nil name:clientTag.name parentGuid:nil updateSequenceNum:0];
            serverTag = [self convertManagedTag:clientTag toEvernoteTag:serverTag convertingOnlyChangedProperties:NO];

            // Check which is newer. If the server is newer, update the client, if the client is newer
            // do nothing. It will be handled later under the processClientTags method.
            [[EvernoteNoteStore noteStore] createTag:serverTag success:^(EDAMTag *tag) {
                NSLog(@"Created new %@ tag on the server.", serverTag.name);
                clientTag.guid = tag.guid;
                NSLog(@"Server GUID %@ assigned to Client GUID %@", tag.guid, clientTag.guid);
                [self saveAllChanges];
            } failure:^(NSError *error) {
                NSLog(@"Failed to create the %@ tag.\n%@", clientTag.name, [error description]);
            }];
        }
    }
    NSLog(@"Client tag processing completed.");
}

After reading Rob's answer, it looks like I will need to do some re-architecting of the source which isn't that big of an issue for me. For every method that has asynchronous code ran within it, the method signature will need to include a callback block. The async code will invoke that callback block when it is done.

Upvotes: 4

Views: 887

Answers (1)

Rob
Rob

Reputation: 437432

If you're seeing your block you passed to sync called before the processSync methods are done, then that would suggest that processSync is, itself, must be performing some asynchronous operation. (You've updated your question with that code, and this appears to be the case.) If this is, in fact, the case, then you'd want to (a) change the processSync method to take a completion block parameter itself, and (b) have the sync method move the call to its own completed() to the block you pass to processSync. This way, you ensure that completed() is not called until it's really completed.

Thus, it might look something like:

- (void)sync:(void(^)(BOOL finished))completed {
    if (self.isSyncing)
        return;

    NSLog(@"Synchronizing tags...");
    self.isSyncing = YES;

    [[EvernoteNoteStore noteStore] getSyncStateWithSuccess:^(EDAMSyncState *syncState) {
        if (syncState.fullSyncBefore > lastSyncTime || syncState.updateCount > self.lastUpdateCount) {
            [self processSync:syncState completionHandler:^(BOOL finished){
                self.isSyncing = NO;
                if (completed)
                    completed(finished);
            }];
        } else {
            self.clientTags = [[self fetchAll] mutableCopy];
            [[EvernoteNoteStore noteStore] listTagsWithSuccess:^(NSArray *tags) {
                self.serverTags = [tags mutableCopy];
                [self processClientTags]; // Only need to make sure client sends any new tags to the server.

                // invoke call back.
                self.isSyncing = NO;
                if (completed)
                    completed(YES);
            } failure:^(NSError *error) {
                self.isSyncing = NO;
                NSLog(@"Failed to list tags.");
                if (completed)
                    completed(NO);
            }];
        }

        // self.isSyncing = NO;
        // if (completed && performCallback)
        //     completed(YES);
    } failure:^(NSError *error) {
        self.isSyncing = NO;
        NSLog(@"Failed to process a sync.");

        if (completed)
            completed(NO);
    }];
}

Note, this eliminates the performCallback boolean, as we simply ensure that all paths call the callback, and in the case of processSync, the calling of the callback is deferred until processSync has finished its asynchronous process first.

This obviously assumes that you will refactor processSync to take a completion handler of its own.

The bottom line is that you only want to call completion blocks when either (a) the ultimate asynchronous processes have successfully completed; or (b) have failed. But don't call the completion block until that asynchronous process is done, nesting it as necessary, as illustrated above.

Upvotes: 1

Related Questions