Ordered sequence of operations in restkit - ios

I use the following code to start N requests, where every request is made of two request that must go hand-by-hand ( I do not care of blocking the UI because I want the app blocked):
objectManager.operationQueue.maxConcurrentOperationCount = 1;
for (int i = 0; i< n; i++)
{
[objectManager postObject:reqObj
path:#"sync.json"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[operation waitUntilFinished];
// Do something and then send the second request
[self sendAck];
} // end success
failure:^(RKObjectRequestOperation *operation, NSError *error) {}
];
}
And the second request is very similar:
-(void)sendAck
{
[objectManager postObject:reqObj
path:#"sync.json"
parameters:nil
success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
[operation waitUntilFinished];
}
failure:^(RKObjectRequestOperation *operation, NSError *error) {}
]
}
But after checking the logs at the server I realized that all the "acks", I mean all the second requests come after all the first requests. And the results are obviously incorrect.
If a request i is started, we must wait the second request to finish before sending the i+1 request. That is:
req. i, second req. on i, req. i+1, second req. on i+ 1,...
and not
req .i ,req. i+1, ....., second req. on i, second req. on i+1
The use of operation queue is wrong or am I missing something?

I never tried this, but a good way to ensure that you are calling the requests in a specific order is by placing them in a queue like described here.
Another approach is make the calls synchronous, a good way to do it is described here.

The reason for this behavior is how you use the `NSOperationQueue:
In the for loop you are effectively enqueueing N "send" requests. All are executed in order and sequentially.
When the first request got finished, the next "send" request will be executed. Since the first "send" request is finished you enqueue the corresponding "sendAck". That is, it will be appended to the tail of the queue, where other "send" requests are still waiting.
When the second "send "request get finished, the next "send" request will be executed and so on. Since the second "send" request is finished you enqueue the corresponding "sendAck" and so force.
When all "send" requests have been executed, the first "sendAck" request gets send. When it finished, the next "sendAck" will be executed and so force until all "sendAck" requests have eventually been send.

Using "recursion", i.e. eliminating the for loop and using a global variabile which counts the number of total requests is a better approach, as in this answer of SO

Related

iOS AFNetwork 3.0: Is there a faster way to send multiple API requests and wait until all of it is finished?

I am currently using the following method to send GET API requests. This method works, but I was wondering if there is a faster way. All I need regarding requirements is to know when all of the Deleted mail has been synced. Any tips or suggestions are appreciated.
- (void)syncDeletedMail:(NSArray *)array atIdx:(NSInteger)idx {
if (idx < array.count) {
NSInteger idNumber = array[idx];
[apiClient deleteMail:idNumber onSuccess:^(id result) {
[self syncDeletedMail:array atIdx:(idx + 1)];
} onFailure:^(NSError *error){
[self syncDeletedMail:array atIdx:(idx + 1)];
}];
} else {
NSLog(#"finished");
}
}
Edit: I don't care what order it is completed (not sure if it matters in terms of speed), as long as all the API requests come back completed.
You can just send deleteMail requests at once and use dispatch_group to know when all the requests are finished. Below is the implementation,
- (void)syncDeletedMail:(NSArray *)array {
dispatch_group_t serviceGroup = dispatch_group_create();
for (NSInteger* idNumber in array)
{
dispatch_group_enter(serviceGroup);
[apiClient deleteMail:idNumber onSuccess:^(id result) {
dispatch_group_leave(serviceGroup);
} onFailure:^(NSError *error){
dispatch_group_leave(serviceGroup);
}];
}
dispatch_group_notify(serviceGroup,dispatch_get_main_queue(),^{
NSLog(#"All email are deleted!");
});
}
Here you can see all the requests are fired at the same time so it will reduce the time from n folds to 1.
Swift Version of #Kamran :
let group = DispatchGroup()
for model in self.cellModels {
group.enter()
HTTPAPI.call() { (result) in
// DO YOUR CHANGE
switch result {
...
}
group.leave()
}
}
group.notify(queue: DispatchQueue.main) {
// UPDATE UI or RELOAD TABLE VIEW etc.
// self.tableView.reloadData()
}
I suppose your request is due to the fact that you might have huge amounts of queued delete requests, not just five or ten of them.
In this case, I'd also try and consider adding a server side API call that allows you to delete more than just one item at a time, maybe up to ten or twenty, so that you could also reduce the overhead of the network traffic you'd be generating (a single GET isn't just sending the id of the item you are deleting but also a bunch of data that will basically sent on and on again for each and every call) by grouping the mails in batches.

Executing functions and blocks through time

I don't understand how Objective-C loop system works. I have function (hope names are right, rather check in code) which executes query from Health Kit. I got my mind blown when I realised that function pass return value before query finishes.
__block bool allBeckuped = true;
HKSampleQuery *mySampleQuery = [[HKSampleQuery alloc] initWithSampleType:mySampleType
predicate:myPredicate
limit:HKObjectQueryNoLimit
sortDescriptors:#[mySortDescriptor]
resultsHandler:^(HKSampleQuery *query, NSArray *results, NSError *error) {
if(!error && results)
{
for(HKQuantitySample *samples in results)///main hk loop
{
allBeckuped = false;
NSLog(#"1");
}
}
}];//end of query
[healthStore executeQuery:mySampleQuery];
NSLog(#"2");
return allBeckuped;
I'm trying to check if there are any new data, but I don't know where to put condition for that, because nslog2 is called before nslog 1.
Any words I should Google up?
Any words I should google up?
You can start with: asynchronous design, blocks, GCD/Grand Central Dispatch should help as well - you're not using it but asynchronous designs often do.
Look at the initWithSampleType: method you are calling, it is an example of a method following the asynchronous model. Rather than return a result immediately, which is the synchronous model you are probably used to, its last argument, resultsHandler:, is a block which the method calls at some future time passing the result of its operation to it.
This is the pattern you will need to learn and follow.
Your method which contains the call to initWithSampleType: cannot return a result (e.g. your allBeckuped) synchronously. So it needs to take a "results handler" block argument, and the block you pass to initWithSampleType: should call the block passed to your method - and so the asynchronous flow of control is weaved.
HTH

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!
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:
At this point it's too late to construct a batch operation (see the implementation)
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.

Why does saveInBackgroundWithBlock only work *once* in my Parse-enabled class?

I have a class with this code which gets called a few times per minute on average, and only runs on the main thread:
PFObject* eventObj = [PFObject objectWithClassName:#"AdminConsoleEvent"];
eventObj[kACParseEventName] = event;
eventObj[kACParseEventUrgency] = urgency;
if( param1 )
eventObj[kACParseEventParam1] = param1;
eventObj[kACParseEventPointerToAdminConsole] = self.adminConsole;
=== [eventObj saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
+++ if( !succeeded ) {
//here
}
}];
If I put a breakpoint where === is, I see that every time eventObj is how I expect... a non-nil object with valid information on it.
If I put a breakpoint where +++ is, then I see that it gets hit exactly only once -- the first time this code is called. If I look on the Parse data browser (online), sure enough, only the first object gets saved (immediately)! The rest never show up.
Why the heck isn't the block (+++) ever running for subsequent calls? Why aren't the other objects being saved?
OK this fixed it...
[PFObject saveAllInBackground:#[eventObj, self.adminConsole] block:^(BOOL succeeded, NSError *error) {
I assume that this is because there was a circular reference: self.adminConsole had a reference being added to it for eventObj, and eventObj had a reference being added to it for self.adminConsole. For whatever reason, that breaks Parse for me if I use saveInBackground directly on the objects.

ReactiveCocoa, Serialize network requests without fire & forget

I am trying to implement code, so I can serialize network requests, basically, the next request should start only after the first one is done. I also want to subscribe to these requests, so I can handle errors. The code looks like follows:
- (RACSignal * ) sendRequest: (Request *) request{
[[[RACSignal return:nil
deliverOn: [RACScheduler scheduler]
mapReplace: [self.network sendRequest]]; // A different thread is spawned to execute the request
}
and it is called as:
[self sendRequest:request
subscribeNext: ^(id x) {
NSLog(#"Request has been sent");
}];
Note that sendRequest can be called from multiple threads in parallel, so the requests need to be queued.
Putting the requests on the same scheduler, didn't work, as the send happens on another thread, and the next request gets picked up, before the previous is finished.
I also looked at using RACSubject that can help in buffering the requests, but it is good for fire and forget.
I was able to achieve the above using the concat command, therefore it is something like:
- (RACSignal * ) sendRequest: (Request *) request subscriber:(id<RACSubscriber>) subscriber{
[[[RACSignal return:nil
deliverOn: [RACScheduler scheduler]
flattenMap:^RACStream *(id value) {
[self.network sendRequest]]; // A different thread is spawned to execute the request
}]
doNext: ^(id x) {
[subscriber sendNext];
}
[[self sendRequest:request
concat]
subscribeNext: ^(id x) {
NSLog(#"Request has been sent");
}];
It turns out that an NSOperationQueue is unavoidable.
I have made RACSerialCommand to serialize the command execution. It has an interface similar to RACCommand, but with built-in NSOperationQueue to serialize the executions.
Feel free to try it.

Resources