Reputation: 18508
Setup
I have two areas in my program - branch 1
and branch 2
- where network requests are made asynchronously 1 concurrent GET request at a time for each area. Requests are sent 1 at a time because the server has a tiny few milliseconds of a grace period for any user making requests to the server. Running 1 concurrent request at a time aims to aid that grace period.
The operation has been made such that if any request fails in any branch - there is a fail safe to just repeat the request again.
Problem:
When I run these branches separately from each other, i.e. not at the same time, the server is happy. However, as soon as I allow for both operations to take place at the same time the server throws a 429
error which is the error letting the user know that there are too many requests coming in at any one time
. What then happens is that half of the requests fails, and then because of the fail safe the requests are being sent again for processing until they're all completed.
It's a waste of time and resources on behalf of the user's data package.
Scenario
Branch 1
sends out 10 requests (normally its within the thousands, processing one at a time, but for simplicity purposes we'll keep it at 10).
These 10 requests will be processed concurrently 1 at a time very quickly and return with their appropriate data objects.
Now if we run branch 1
operation again - 10 requests concurrently and then add branch 2's
10 requests, this will create 20 request operations - twice as many - GOING OUT to the server which will cause roughly half of the requests from both branch 1 and 2 to fail, with the fail-safe then kicking in to resend those request operations. boohoo.
Question
Is there a way to funnel these request operations coming out of my application regardless of where these two branches lay within my app so that we can control and bottleneck the request operations going out to the server so that the server won't be bombarded with requests only for half of the requests to fail?
Possible solution
One idea I had was to create a singleton that would act as a queue as well as the bottle neck required that would allow me to add request operations from ANY branch anywhere within my code base - hence why i thought a singleton would be a great idea for this - and then as soon as it realises a request operation has been added to the queue to then to have that queue run 1 concurrent request at at time.
This I think will work great for my purposes, I just didn't know how to handle the completion blocks of the requests since the request operation from Branch 1
are different from the completion blocks of the request operations from Branch 2
.
How would you manage that? Is there anything we can do?
Edit 1 - What I'm currently doing
In Branch 1 - Using AFHTTPOperation:
1) For each process I create a for loop that iterates 10 times adding 10 AFHTTPRequestOperation
s to a local array called multipleOperations of type NSMutableArray
.
2) I then add the multipleOperations array to a batchOfRequestOperations method like so:
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:muiltipleOperations
progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations){
//Do some stuff here for each iteration
}
completionBlock:^(NSArray*operations){
//Call the method that repeats this process again.
}];
3) I then add the operations to the mainQueue like so
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];
I'm now wondering whether this process in branch one even has it set to 1 concurrenncy, cant see that it is :\
In Branch 2 using NSURLSessionDataTask (just realised, wouldnt it be easier if they were all using the same subclass? lol
I notice that I am using a AFHTTPSessionManager
that creates NSURLSessionDataTasks
.. which doesnt run in a for loop but instead calls its own method for another request to be sent out EVERY TIME the request executes its own completion block. So it will forever keep calling itself as soon as the previous request in branch 2 is done. This is how I know its running only 1 request at a time concurrently. So thats good.
Is there anyway to make this more cleaner?
No where am I using HTTPRequestOperationManager
:( Can someone show me how things can be done properly?
Thank you.
Upvotes: 2
Views: 912
Reputation: 42325
Based on your edit, I can see a some things you'll probably want to change. First off, yes, it will be much easier if you use either AFHTTPOperation
s or NSURLSessionDataTask
s, but not both.
If you're going to use AFHTTPOperation
, then don't queue the operations on [NSOperationQueue mainQueue]
. The reason being is that that is the main UI queue and you don't want your network operations blocking it. What you should be doing is creating your own NSOperationQueue
and setting its concurrency to 1.
NSOperationQueue *networkQueue = [[NSOperationQueue alloc] init];
networkQueue.maxConcurrentOperationCount = 1;
Using your idea of a singleton to store that queue somewhere you can access from anywhere in your app would be a good way to go about making sure you send all your network operations through the same queue.
That being said, I'd recommend using NSURLSessionDataTask
with AFHTTPSessionManager
instead. The docs for AFNetworking state:
Developers targeting iOS 7 or Mac OS X 10.9 or later that deal extensively with a web service are encouraged to subclass AFHTTPSessionManager, providing a class method that returns a shared singleton object on which authentication and other configuration can be shared across the application.
That sounds like a good way to accomplish what you want. Basically you'll create a subclass of AFHTTPSessionManager
with a class method that creates an instance initialized with a custom NSURLSessionConfiguration
. You can configure the NSURLSessionConfiguration
however is appropriate for your app, but the main property you'll be interested in for this particular issue is HTTPMaximumConnectionsPerHost
:
This property determines the maximum number of simultaneous connections made to each host by tasks within sessions based on this configuration.
With that set to 1
, you can let AFHTTPSessionManager
(well, actually, NSURLSession
) worry about making sure that only one request is made to your server at any given time.
Something along these lines:
@interface AppHTTPSessionManager : AFHTTPSessionManager
+ (instancetype)appSession;
@end
@implementation AppHTTPSessionManager
+ (instancetype)appSession {
static AppHTTPSessionManager *_appSession = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.HTTPMaximumConnectionsPerHost = 1;
_appSession = [[AppHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
});
return _appSession;
}
@end
Then, whenever you want to make a network call, make sure you create your NSURLSessionDataTask
from [AppHTTPSessionManager appSession]
. If you do, those tasks should automatically be limited to one request to the server at a time.
Upvotes: 1