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.
Related
I have a unit test class with multiple test cases. Some test cases have common XCTest Assertions. These test cases implements an API call. This API call can have different input request parameters but the response is same. I have put test assertions on response. So, is it good by design to extract out this common assertions code on response in a separate method (not a test method) and call that method in the test methods?
Below is the code for reference:
- (void)testRequest {
AppType app = #"A";
NSDictionary *requestMessage = #{requestMessage};
__block BOOL hasReceivedResponse = NO;
[Class handleRequestMessage:requestMessage
appType:app
managedObjectContext:self.fixtures.managedObjectContext
completionBlock:^(NSDictionary *responseMessage, NSError *error) {
XCTAssertNil(error);
NSString *name = responseMessage[#"name"];
XCTAssert([name isEqualToString:#"Search"]);
NSString *response = responseMessage[#"response"];
XCTAssert([response isEqualToString:#"1"]);
hasReceivedResponse = YES;
}];
[[NSRunLoop currentRunLoop] runUntilCompletionIndicator:&hasReceivedResponse];
}
Here appType can be A, B, C. Response as present in completion block remains the same. Can I extract completion block code to a separate method?
Refactored Code:
- (BOOL)receivedResponseForRequestMessage:(NSDictionary *)responseMessage error:(NSError *)error {
XCTAssertNil(error);
NSString *name = responseMessage[#"name"];
XCTAssert([name isEqualToString:#"Search"]);
NSString *response = responseMessage[#"response"];
XCTAssert([response isEqualToString:#"1"]);
}
-(void)testRequestForA {
AppType app = #"A";
NSDictionary *requestMessage = #{requestMessage};
__block BOOL hasReceivedResponse = NO;
[Class handleRequestMessage:requestMessage
appType:app
managedObjectContext:self.fixtures.managedObjectContext
completionBlock:^(NSDictionary *responseMessage, NSError *error) {
hasReceivedResponse = [self receivedResponseForRequestMessage:responseMessage error:error
}];
[[NSRunLoop currentRunLoop] runUntilCompletionIndicator:&hasReceivedResponse];
}
-(void)testRequestForB {
AppType app = #"B";
NSDictionary *requestMessage = #{requestMessage};
__block BOOL hasReceivedResponse = NO;
[Class handleRequestMessage:requestMessage
appType:app
managedObjectContext:self.fixtures.managedObjectContext
completionBlock:^(NSDictionary *responseMessage, NSError *error) {
hasReceivedResponse = [self receivedResponseForRequestMessage:responseMessage error:error
}];
[[NSRunLoop currentRunLoop] runUntilCompletionIndicator:&hasReceivedResponse];
}
Is this kind of refactoring correct by design?
Having a common XCTAssert does have at least one drawback. XCTAssert reports its line number, which enables you to find the failing test and quickly fix it. When your method testRequestForA fails, the failing line will be in receivedResponseForRequestMessage:error:.
One thing you can do in Objective-C is pass in a message after the expression being asserted. According to the docs, this is "An optional description of the failure. A literal NSString, optionally with string format specifiers." Your test results will contain this information, so if you identified the assertion well it will be easy to find the source line.
XCTAssert([response isEqualToString:#"1"], #"%# response failed", name); // Replace name with another string if appropriate
As a side note, Swift also allows you to also pass in the line number and/or file where the test failed, which makes it a lot easier to use assertions in helper methods.
In general, it's helpful to refactor test code to make it more expressive. This includes extracting assertion helpers.
In practice, this isn't easy to do in the Objective-C because assertions like XCTAssert are actually macros. They need to be macros at the call site in order to pick up __FILE__ and __LINE__. Unfortunately Apple chose to implement them entirely as macros, instead of as thin macros which call methods taking file name and line number arguments.
But in How to Structure Tests that Do Real Networking, I recommend not putting assertions within a block. Instead, have the block capture its arguments, and trigger the exit. This avoids issues with timeouts, and moves assertions to the end of the test where they are easier to read.
This would change the first part of your example to this:
- (void)testRequest {
AppType app = #"A";
NSDictionary *requestMessage = #{requestMessage};
__block BOOL hasReceivedResponse = NO;
__block NSDictionary *capturedResponseMessage = nil;
__block NSError *capturedError = nil;
[Class handleRequestMessage:requestMessage
appType:app
managedObjectContext:self.fixtures.managedObjectContext
completionBlock:^(NSDictionary *responseMessage, NSError *error) {
capturedResponseMessage = responseMessage;
capturedError = error;
hasReceivedResponse = YES;
}];
[[NSRunLoop currentRunLoop] runUntilCompletionIndicator:&hasReceivedResponse];
The hasReceivedResponse trigger can be replaced by an XCTestExpectation. This would let you use a timeout.
Now we can perform assertions against the captured arguments. But I'm going to change the assertions:
XCTAssertNil(error, #"error");
XCTAssertEqualObjects(responseMessage[#"name"], #"Search", #"name");
XCTAssertEqualObjects(responseMessage[#"response"], #"1", #"response");
}
First, I replaced the XCTAssert … isEqualToString: with XCTAssertEqualObjects. This is important, because when the assertion fails, it will report the 2 values that weren't equal instead of just "Failed". You can immediately see what the actual value was without using the debugger.
I also added a message to each assertion. This can be important when you have multiple assertions in a single test. It may not matter when sitting in front of your IDE, because you can click on a failure message to see which assertion triggered it. But when tests are running on a CI server, we want to log more information so that any failures can be diagnosed without hunting down line numbers.
If you still want to extract common assertions, I'd recommend either:
Write them entirely as macros so you get __FILE__ and __LINE__. Look at the definition of XCTAssert to get started.
Use OCHamcrest and write a custom matcher. Because OCHamcrest takes care of capturing file name and line number, the matcher is easier to write.
However, pulling the assertions out of the block made them so short they're hardly worth extracting. I'd focus instead on extracting a helper for the first part, capturing the block arguments.
i want to run some operations task with completion continuation- sequentially
for example i want to execute getSomethingsWithResultWithCompletion method for 3 times as serialized tasks (like op1 depend op2 , ... depen op N ) :
[MFLayer getSomethingsWithResultWithCompletion:^(id _Nullable response)Completion {
// it will be run on another thread!**
[MFRequestManager retrivesomeDataWithCompletion:^(id _Nullable response1) {
// it will be run on another thread!**
[MFRequestManager retriveAnothersomeDataWithInfo:response1 WithCompletion:^(id _Nullable response2) {
NSLog(#"Finished with Result : %#",response2);
}];
}];
}];
Problem
if retrive methods execute in another thread (like send a request with AFNetworking) i have a problem with serialize and another task will be start.
i have try With NSOperationQueue and Semaphore but still have a problem
i have implemented something like this with NSOperationQueue and NSOperation but implementation of them have run on the same thread so all of task start sequentially so it's works fine.
operationQueueExample
I strongly discourage that approach, but if you send the task on a background thread, you can use GCD semaphores.
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[MFRequestManager retrivesomeDataWithCompletion:^(id _Nullable response) {
if(Completion)
Completion(response)
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
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
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.
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