Search code examples
iosafnetworking-2nsoperationqueue

AFNetworking 2 composing batch with dependencies


I'm using AFNetworking 2, and would like to use the NSURLSession approach, but read the GitHub issue where Mattt explains why this doesn't work with batching. So, instead, I'm using AFHTTPRequestOperations from a singleton class containing an NSOperationQueue.

I've created a significant number of discrete operations. Each of these operations is called from different areas of the app, but in some parts of the app, its useful to batch them together (think "full refresh"). Here's a method that does this:

-(void) getEverything {

  AFHTTPRequestOperation *ssoA = [SecurityOps authenticateSSO];

  AFHTTPRequestOperation *atSC = [SecurityOps attachSessionCookies];
  [atSC addDependency:ssoA];

  AFHTTPRequestOperation *comL = [CommunityOps communityListOp];
  [comL addDependency:ssoA];

  AFHTTPRequestOperation *comS = [CommunityOps searchCommunityOp:nil :nil];
  [comS addDependency:comL];

  AFHTTPRequestOperation *stu1 = [StudentOps fdpFullOp]; // 3 Ops in Sequence
  [stu1 addDependency:ssoA];

  AFHTTPRequestOperation *stu2 = [StudentOps progressDataOp];
  [stu2 addDependency:ssoA];

  AFHTTPRequestOperation *stu3 = [StudentOps programTitleOp];
  [stu3 addDependency:ssoA];

  AFHTTPRequestOperation *stu4 = [StudentOps graduationDateOp];
  [stu4 addDependency:ssoA];

  NSArray *ops = [AFURLConnectionOperation
                  batchOfRequestOperations:@[ssoA, atSC, comL, comS, stu1, stu2, stu3, stu4]
                             progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {

    NSLog(@"%lu of %lu complete", numberOfFinishedOperations, totalNumberOfOperations);

  } completionBlock:^(NSArray *operations) {

    NSLog(@"All operations in batch complete");

  }];

  [self.Que addOperations:ops waitUntilFinished:NO];
}

This works just fine, with one exception: The "fdpFullOp" actually launches other operations in a sequence. In its completion block, it adds opB to the queue, and then opB adds opC to the queue in its completion block. These additional operations are, of course, not counted in the "batch" (as written above), so this batch completes before opB and opC are done.

Question 1: When adding an op from the completion block of another, can I add it to the "batch" (for overall batch completion tracking)?

One alternative I've tried is to sequence all of the ops in the queue at batch creation (below). This provides accurate batch completion notice. However, as stu1B requires data from stu1A, and stu1C requires data from stu1B, this only works if predecessor operations persist their data somewhere (e.g. NSUserDefaults) that successor operations can get it. This seems a bit "inelegant", but it does work.

-(void) getEverything {

  AFHTTPRequestOperation *ssoA = [SecurityOps authenticateSSO];

  AFHTTPRequestOperation *atSC = [SecurityOps attachSessionCookies];
  [atSC addDependency:ssoA];

  AFHTTPRequestOperation *comL = [CommunityOps communityListOp];
  [comL addDependency:ssoA];

  AFHTTPRequestOperation *comS = [CommunityOps searchCommunityOp:nil :nil];
  [comS addDependency:comL];

  AFHTTPRequestOperation *stu1A = [StudentOps fdpFullOp]; // 1 of 3 op sequence
  [stu1A addDependency:ssoA];

  AFHTTPRequestOperation *stu1B = [StudentOps fdpSessionOp]; // 2 of 3 op sequence
  [stu1B addDependency:stu1A];

  AFHTTPRequestOperation *stu1C = [StudentOps fdpDegreePlanOp]; // 3 of 3 op sequence
  [stu1C addDependency:stu1B];

  AFHTTPRequestOperation *stu2 = [StudentOps progressDataOp];
  [stu2 addDependency:ssoA];

  AFHTTPRequestOperation *stu3 = [StudentOps programTitleOp];
  [stu3 addDependency:ssoA];

  AFHTTPRequestOperation *stu4 = [StudentOps graduationDateOp];
  [stu4 addDependency:ssoA];

  NSArray *ops = [AFURLConnectionOperation
                  batchOfRequestOperations:@[ssoA, atSC, comL, comS, stu1A, stu1B, stu1C, stu2, stu3, stu4]
                             progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {

    NSLog(@"%lu of %lu complete", numberOfFinishedOperations, totalNumberOfOperations);

  } completionBlock:^(NSArray *operations) {

    NSLog(@"All operations in batch complete");

  }];

  [self.Que addOperations:ops waitUntilFinished:NO];
}

Question 2: Is there a better way (other than persisting data in each op and then reading from storage in the successor op) to pass data between dependent operations in a batch?

Finally, it occurs to me that I might be making this entire process more difficult than it should be. I'd love to hear about alternate approaches that still provide an overall concurrent queue, still provide overall batch progress/completion tracking, but also allow inter-op dependency management and data passing. Thanks!


Solution

  • You shouldn't use NSOperation dependencies for this because later operations rely on processing with completionBlock but NSOperationQueue considers that work a side effect.

    According to the docs, completionBlock is "the block to execute after the operation’s main task is completed". In the case of AFHTTPRequestOperation, "the operation’s main task" is "making an HTTP request". The "main task" doesn't include parsing JSON, persisting data, checking HTTP status codes, etc. - that's all handled in completionBlock.

    So in your code, if the ssoA operation succeeds in making a network request, but authentication fails, all the later operations will still continue.

    Instead, you should just add dependent operations from the completion blocks of the earlier operations.

    When adding an op from the completion block of another, can I add it to the "batch" (for overall batch completion tracking)?

    You can't, because:

    1. At this point it's too late to construct a batch operation (see the implementation)
    2. It doesn't make sense, because the later operations may not ever get created (for example, if authentication fails)

    As an alternative, you could create one NSProgress object, and update it as work progresses to reflect what's been done and what is known to remain. You could use this, for example, to update a UIProgressView.

    Is there a better way (other than persisting data in each op and then reading from storage in the successor op) to pass data between dependent operations in a batch?

    If you add dependent operations from the completion blocks of the earlier operations, then you can just pass local variables around after validating the success conditions.