NSInvocationOperation ignoring maxConcurrentOperationCount - iphone

I'm trying to queue up some TWRequest calls using NSInvocationOperation. It seems to add the method calls in the correct order, but the doSomething: methods get called at the same time pretty much, ie run concurrently, rather than one after the other, which is what I want to achieve.
In addition, the complete in the wrong order, which suggests it's not running one after the other...
- (void)prepare {
if(!self.queue){
self.queue = [[NSOperationQueue alloc] init];
[self.queue setMaxConcurrentOperationCount:1];
}
for(NSString *text in calls){
NSLog(#"Adding to Queue... %#", text);
NSInvocationOperation *indexOperation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(doSomething:) object:text];
[self.queue addOperation:indexOperation];
}
}
- (void)doSomething:(NSString*)someText {
TWRequest *request = [[TWRequest alloc] initWithURL:[NSURL URLWithString:#"http://something.com"] parameters:nil requestMethod:TWRequestMethodGET];
NSLog(#"About to Perform Request... %#", someText);
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error)
{
dispatch_sync(dispatch_get_main_queue(), ^{
// works fine
NSLog(#"Network Finished... %#", someText);
});
}];
}
In the log I see this:
2011-12-30 18:34:34.553 app[32745:10703] Adding to Queue... 1
2011-12-30 18:34:34.555 app[32745:10703] Adding to Queue... 2
2011-12-30 18:34:34.556 app[32745:10703] Adding to Queue... 3
2011-12-30 18:34:34.557 app[32745:13e03] About to Perform Request... 1
2011-12-30 18:34:34.560 app[32745:13e03] About to Perform Request... 2
2011-12-30 18:34:34.563 app[32745:13e03] About to Perform Request... 3
2011-12-30 18:34:35.303 app[32745:10703] Network finished... 3
2011-12-30 18:34:35.454 app[32745:10703] Network finished... 2
2011-12-30 18:34:35.601 app[32745:10703] Network finished... 1
I'm expecting to see (2) to Perform Request after (1) has finished etc... Any pointers?

The operation queue is working fine. As #Joe said in the comment, performRequestWithHandler: starts an asynchronous connection and returns immediately. You can see this by adding an NSLog to the end of doSomething as follows:
- (void)doSomething:(NSString*)someText {
TWRequest *request = [[TWRequest alloc] initWithURL:[NSURL URLWithString:#"http://something.com"] parameters:nil requestMethod:TWRequestMethodGET];
NSLog(#"About to Perform Request... %#", someText);
[request performRequestWithHandler:^(NSData *responseData, NSHTTPURLResponse *urlResponse, NSError *error)
{
dispatch_sync(dispatch_get_main_queue(), ^{
// works fine
NSLog(#"Network Finished... %#", someText);
});
}];
NSLog(#"doSomething Finished");
}
To have each request happen serially you need to either make the request synchronous within the operation (use the signedRequest method and a synchronous NSURLConnection) or don't use an operation queue and invoke the next request in the completion handler of the current request. Keep in mind that if you use an operation queue the order in which operations are performed is not based on the order they are added. You might consider using GCD directly with a serial dispatch queue.

Related

Perform AFHTTPClient request, in background, with NSOperationQueue chronologically

I have an HTTPClient request as follows :
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:urlStringMain]];
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:url];
NSDictionary *params = [NSDictionary dictionaryWithObjectsAndKeys:
//parameters
nil];
[self beginBackgroundUpdateTask];
[httpClient postPath:postPath parameters:params success:^(AFHTTPRequestOperation *operation, id responseObject) {
//id results = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONWritingPrettyPrinted error:nil];
//completion code
[self endBackgroundUpdateTask];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//failure code
}];
[httpClient release];
The background task is executed in :
- (void) beginBackgroundUpdateTask{
[operationQueue addOperationWithBlock:^{
NSLog(#"started upload process as a background job");
self.backgroundUpdateTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[self endBackgroundUpdateTask];
}];
}];
}
And ended in :
- (void) endBackgroundUpdateTask{
NSLog(#"complete upload process as a background job");
[[UIApplication sharedApplication] endBackgroundTask: self.backgroundUpdateTask];
self.backgroundUpdateTask = UIBackgroundTaskInvalid;
}
where self.backgroundUpdateTask is a UIBackgroundTaskIdentifier object, and operationQueue is an object of NSOperationQueue (public member), initialized in viewDidLoad:
operationQueue = [[NSOperationQueue alloc] init];
[operationQueue setMaxConcurrentOperationCount:NSOperationQueueDefaultMaxConcurrentOperationCount];
Now what I want to do is, perform these requests chronologically, in the background, such that pushing/popping from the viewController does not affect the request. It should also not be affected if the application goes into background. At times I post text, and at other times I post an image. Now, images take longer to upload, than text so if subsequent requests of text and images are made, texts are posted first, and images later. This breaks the chronology of the tasks, hence I wanted to use the NSOperationQueue. But being new to operation queues, I cannot seem to make it work. The chronology is still not being respected. How do I perform the task in the way I want to.
PS. Also, as you can see in the code, i have added [self endBackgroundUpdateTask] in both, the completion block of the httpClient request, and the beginBackgroundUpdateTask method. Now i understand this is not good. Where exactly should the endBackgroundUpdateTask method be called ?
Thank you.
The HTTPClient can handle the background task for you if you ask it to, simply set the appropriate flag (you need to create the operation and call setShouldExecuteAsBackgroundTaskWithExpirationHandler:).
Rather than change how the uploads operate, keep it simple. Send a date with the upload and use that to maintain your order information on the server.
If you really must execute your operations serially then you can get the operationQueue from the client and set it to only execute a single operation at a time.
In either case you shouldn't need to create your own operation queue.

iPhone how to fetch data asynchronously from a web service with API call limit?

I'm pulling data from a web service with an API call limit of 125 per hour. My initial sync of the user's data will use a method similar to the one below. I'm having trouble understanding the correctness of concurrency implications of the code below.
I'm adding a series of AFHTTPRequestOperation over to a serial NSOPerationsQueue (max concurrent count = 1). The resulting calls return asynchronously and cause the method to process the data dictionary. Because of the API call limit, I know that at some point my code will fail and start to return error dictionaries instead.
Can I expect the following code to return full data dictionaries sequentially, or due to asynchronous nature of callbacks, can some of them complete before earlier requests?
Because I'm trying to do initial sync, I want to make sure that once the code fails due to API call limit, I have no "holes" in my data up until the failure point.
-(void)addRequestWithString:(NSString*)requestString
{
// 1: Create a NSURL and a NSURLRequest to points to the web service providing data. Add Oauth1 information to the request, including any extra parameters that are not in scope of Oauth1 protocol
NSURL *url = [NSURL URLWithString:requestString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[ self.auth authorizeRequest:request withExtraParams:self.extraAuthParameters];
// 2: Use AFHTTPRequestOperation class, alloc and init it with the request.
AFHTTPRequestOperation *datasource_download_operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
// 3: Give the user feedback, while downloading the data source by enabling network activity indicator.
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
// 4: By using setCompletionBlockWithSuccess:failure:, you can add two blocks: one for the case where the operation finishes successfully, and one for the case where it fails.
[datasource_download_operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSDictionary* dictonary = [NSJSONSerialization JSONObjectWithData:(NSData *)responseObject
options:NSJSONReadingMutableContainers error:&error];
[self processResponseDictionary:dictonary];
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
// 8: In case you are not successful, you display a message to notify the user.
// Connection error message
DLog(#"API fetch error: %#", error);
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
}];
// 9: Finally, add ìdatasource_download_operationî to ìdownloadQueueî of PendingOperations.
[[self syncQueue] addOperation:datasource_download_operation];
}
Your approach will continue the operations even after they start failing.
If you need the operations to go one at a time, but stop once the failure block is hit, enqueue a new request in the completion block of the prior request.
(This code is from an answer to AFNetworking Synchronous Operation in NSOperationQueue on iPhone; I didn't write it.)
NSEnumerator *enumerator = [operations reverseObjectEnumerator];
AFHTTPRequestOperation *currentOperation = nil;
AFHTTPRequestOperation *nextOperation = [enumerator nextObject];
while (nextOperation != nil && (currentOperation = [enumerator nextObject])) {
currentOperation.completionBlock = ^{
[client enqueueHTTPRequestOperation:nextOperation];
}
nextOperation = currentOperation;
}
[client enqueueHTTPRequestOperation:currentOperation];
If the failure block is hit, the following operations will never be enqueued.

NSURLConnection sendAsynchronousRequest: Blocking Main Thread

I'm using NSURLConnection to make multiple asynchronous requests. I'd like to show a progress indicator to show how many requests have been completed out of the total number to be performed. However, when I attempt to set up and display this progress indicator either before making the request, or in another method called before performing the request, it will not show. The progress indicator displays fine when the request is commented out. But when it's not, it's as if Xcode looks ahead and sees an asynchronous request coming and blocks the main thread, thereby making UI changes impossible.
Here's the relevant code being called, including both the request and code to show the progress indicator:
- (void)getRegionalInformationFromChecked:(NSSet *)set atIndex:(NSInteger)index {
__block BOOL responseRecieved = NO;
NSString *stringForURL = [NSString stringWithFormat:#"http://www.thebluealliance.com/api/v1/event/details?event=%#",[[set allObjects] objectAtIndex:index]];
NSURL *url = [NSURL URLWithString:stringForURL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSLog(#"URL IS GO: %#", stringForURL);
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:queue completionHandler:^(NSURLResponse *_response, NSData *_data, NSError *_error) {
NSLog(#"CHECKED DATA RETURNED AT INDEX %i", index);
NSError *error;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableContainers error:&error];
if (!_regionalDetails) {
_regionalDetails = [[NSMutableArray alloc] init];
}
[_regionalDetails addObject:dict];
responseRecieved = YES;
}];
regionalSchedulesToGet = [set count];
while (responseRecieved == NO) {}
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]] setLabelText:[NSString stringWithFormat: #"Getting regional %i of %i", index+2, [set count]]];
if (index+1 < [set count]) {
[self getRegionalInformationFromChecked:set atIndex:index+1];
} else {
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]] setLabelText:#"Writing to file"];
}
}
When the asynchronous request's block is commented out, the MBProgressHUD displays its value fine. But when the block is inserted, the SDK refuses to update the progress indicator, even after leaving the block (after which any threading issues should have been resolved). It does not update until there are no more requests to display, at which point it reads "Writing to file".
Why does an asynchronous request seem to block the main thread, and why can I not make changes on the main thread immediately before or after the request is called?
With
while (responseRecieved == NO) {}
you block the main thread (probably with almost 100% CPU load) until the asynchronous block has finished. Then you call your
function recursively, start another asynchronous block and block again until that has
finished. Therefore the program control does not return to the main runloop until all
operations have finished. Only then the UI updates are done.
Instead of waiting synchronously (which is always a bad idea),
you should start the next operation at the end of the completion block.
Note also that the queue argument of sendAsynchronousRequest is the queue on which
the completion handler is called, so you can just use [NSOperationQueue mainQueue].
Then your code looks roughly like this:
- (void)getRegionalInformationFromChecked:(NSSet *)set atIndex:(NSInteger)index
{
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]]
setLabelText:[NSString stringWithFormat:#"Getting regional %i of %i", index+1, [set count]]];
NSString *stringForURL = [NSString stringWithFormat:#"http://www.thebluealliance.com/api/v1/event/details?event=%#",[[set allObjects] objectAtIndex:index]];
NSURL *url = [NSURL URLWithString:stringForURL];
NSLog(#"URL IS GO: %#", stringForURL);
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *_response, NSData *_data, NSError *_error) {
NSLog(#"CHECKED DATA RETURNED AT INDEX %i", index);
NSError *error;
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:_data options:NSJSONReadingMutableContainers error:&error];
if (!_regionalDetails) {
_regionalDetails = [[NSMutableArray alloc] init];
}
[_regionalDetails addObject:dict];
if (index+1 < [set count]) {
[self getRegionalInformationFromChecked:set atIndex:index+1];
} else {
[[MBProgressHUD HUDForView:[[UIApplication sharedApplication] keyWindow]] setLabelText:#"Writing to file"];
// ... perhaps call a completion function from here ?
}
}];
}
But note that the initial call to getRegionalInformationFromChecked will now
return almost immediately (that's how asynchronous tasks work :-).
Try to dispatch on the main thread all the methods that involve UI refresh

AFNetworking + download images in queue + cancel operations

I need to download a queue of images.
I created my operations first, then add them with the "enqueue" method of AFNetworking.
I have 2 problems :
1) I didn't have the progress bar working for the queue (and I have it working with a custom operation queue)
2) I didn't find the solution to stop the queue when I want
I created first operations to batch and add theme in a array:
while ((dict = [enumerator nextObject]))
{
NSMutableURLRequest *request = [[MyHTTPClient sharedClient] requestWithMethod:#"GET" path:#"ws/webapp/services/pull_image" parameters:dict];
AFImageRequestOperation *operation = [AFImageRequestOperation imageRequestOperationWithRequest:request
imageProcessingBlock:nil cacheName:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image)
{
NSLog(#"image : %#", [image description]);
// process images
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error)
{
// manage errors
}];
[operations addObject:operation];
}
Then, I enqueue the operations:
[[MyHTTPClient sharedClient] enqueueBatchOfHTTPRequestOperations:operations
progressBlock:^(NSUInteger numberOfCompletedOperations, NSUInteger totalNumberOfOperations)
{
float percentDone = ((float)((int)numberOfCompletedOperations) / (float)((int)totalNumberOfOperations));
[delegate syncServicesController:self updateProgressView:percentDone];
}
completionBlock:^(NSArray *operations)
{
//
}];
So, the progress download didn't work.
But I can see the progress of numberOfCompletedOperations... ? 1,2,3,4,5... Does I need to force the refresh of the progress view in the main thread ?
And when I tried to stop the network tasks:
- (void)cancelAllRequests
{
[[MyHTTPClient sharedClient] cancelAllHTTPOperationsWithMethod:#"GET" path:#"ws/webapp/services/pull_image"];
}
I don't understand how to stop the queue of requests... This seems that works but I have this error : -[NSBlockOperation request]: unrecognized selector sent to instance 0x16f54c70
These were actually just fixed in the last day or two :)
Go ahead and update to the latest version of master, which includes the following:
cc2115e469: Progress blocks now dispatch to main by default, just like all of the other completion blocks in AFNetworking. This should fix any issues around the UI not updating there.
cac44aeb34: Fixes that problem with NSBlockOperation being sent request. There was an incorrect assumption baked into cancelAllHTTPOperationsWithMethod: that all operations were AFHTTPRequestOperation. The only downside is that it will not handle your batched operations. For that, you can always iterate through httpClient.operationQueue.operations and pick out the one you want.

ASINetworkQueue with order

I want to make queue with ten requests, and I want to load it in order.
How can I do it? Have you any ideas?
ASINetworkQueue runs all the requests in the order they were inserted into the queue. It is a first in, first out (FIFO) system.
If you want to ensure they all get run one after another rather than in parallel then you can set the concurrency to 1. The queue will start from the first request and run it one by one until it gets to the last request
ASINetworkQueue *networkQueue = [[ASINetworkQueue alloc] init];
// Here we add all our 10 requests, the order in which we add
// them determines the order they will execute
// Set the concurrency to 1 and fire off the queue
[networkQueue setMaxConcurrentOperationCount:1];
[networkQueue go];
Check AFNetworking as ASIHTTPRequest is not maintained anymore. You can use it with NSOperationQueue, which hast the property maxConcurrentOperationCount. If you set this to 1:
Setting the maximum number of operations to 1 effectively creates a serial queue for processing operations.
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"https://gowalla.com/users/mattt.json"]];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON) {
NSLog(#"Name: %# %#", [JSON valueForKeyPath:#"first_name"], [JSON valueForKeyPath:#"last_name"]);
} failure:nil];
NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
[queue setMaxConcurrentOperationCount:1];
[queue addOperation:operation];
[queue addOperation:anotherOperation];