4

I'm subclassing NSOperation for http post in background thread. Those specific http posts doesn't require any value to return.

What I'm trying to do is when I've an error or timeout I want it to send after an increasing delay (fibonacci).

So far I've done this:

NSInternetOperation.h:

#import <Foundation/Foundation.h>

@interface NSInternetOperation : NSOperation
@property (nonatomic) BOOL executing;
@property (nonatomic) BOOL finished;
@property (nonatomic) BOOL completed;
@property (nonatomic) BOOL cancelled;
- (id)initWebServiceName:(NSString*)webServiceName andPerameters:(NSString*)parameters;
- (void)start;
@end

NSInternetOperation.m:

#import "NSInternetOperation.h"

static NSString * const kFinishedKey = @"isFinished";
static NSString * const kExecutingKey = @"isExecuting";

@interface NSInternetOperation ()
@property (strong, nonatomic) NSString *serviceName;
@property (strong, nonatomic) NSString *params;
- (void)completeOperation;
@end

@implementation NSInternetOperation

- (id)initWebServiceName:(NSString*)webServiceName andPerameters:(NSString*)parameters
{
    self = [super init];
    if (self) {
        _serviceName = webServiceName;
        _params = parameters;
        _executing = NO;
        _finished = NO;
        _completed = NO;
    }
    return self;
}

- (BOOL)isExecuting { return self.executing; }
- (BOOL)isFinished { return self.finished; }
- (BOOL)isCompleted { return self.completed; }
- (BOOL)isCancelled { return self.cancelled; }
- (BOOL)isConcurrent { return YES; }

- (void)start
{
    if ([self isCancelled]) {
        [self willChangeValueForKey:kFinishedKey];
        self.finished = YES;
        [self didChangeValueForKey:kFinishedKey];
        return;
    }

    // If the operation is not cancelled, begin executing the task
    [self willChangeValueForKey:kExecutingKey];
    self.executing = YES;
    [self didChangeValueForKey:kExecutingKey];

    [self main];
}

- (void)main
{
    @try {
        //
        // Here we add our asynchronized code
        //
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSURL *completeURL = [NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", kWEB_SERVICE_URL, self.serviceName]];
            NSData *body = [self.params dataUsingEncoding:NSUTF8StringEncoding];
            NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:completeURL];
            [request setHTTPMethod:@"POST"];
            [request setValue:kAPP_PASSWORD_VALUE forHTTPHeaderField:kAPP_PASSWORD_HEADER];
            [request setHTTPBody:body];
            [request setValue:[NSString stringWithFormat:@"%lu", (unsigned long)body.length] forHTTPHeaderField:@"Content-Length"];
            [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];


            if (__iOS_7_AND_HIGHER)
            {
                NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
                NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:[Netroads sharedInstance] delegateQueue:[NSOperationQueue new]];
                NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
                    if (error)
                    {
                        NSLog(@"%@ Error: %@", self.serviceName, error.localizedDescription);
                    }
                    else
                    {
                        //NSString *responseXML = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                        //NSLog(@"\n\nResponseXML(%@):\n%@", webServiceName, responseXML);
                    }
                }];
                [dataTask resume];
            }
            else
            {
                [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue new] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
                    if (connectionError)
                    {
                        NSLog(@"%@ Error: %@", self.serviceName, connectionError.localizedDescription);
                    }
                    else
                    {
                        //NSString *responseXML = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                        //NSLog(@"\n\nResponseXML(%@):\n%@", webServiceName, responseXML);
                    }
                }];
            }
        });

        [self completeOperation];
    }
    @catch (NSException *exception) {
        NSLog(@"%s exception.reason: %@", __PRETTY_FUNCTION__, exception.reason);
        [self completeOperation];
    }
}

- (void)completeOperation
{
    [self willChangeValueForKey:kFinishedKey];
    [self willChangeValueForKey:kExecutingKey];

    self.executing = NO;
    self.finished = YES;

    [self didChangeValueForKey:kExecutingKey];
    [self didChangeValueForKey:kFinishedKey];
}

@end
tshepang
  • 12,111
  • 21
  • 91
  • 136
Idan Moshe
  • 1,675
  • 4
  • 28
  • 65
  • I would recommend you not to reinvent the circle and look at [AFNetworking](https://github.com/AFNetworking/AFNetworking). It is capable to handle all issues around networking you're going to implement, and much more. – nalexn Mar 02 '14 at 20:35

2 Answers2

6

A couple of reactions:

  1. Before you tackle the retry logic, you should probably move your call to [self completeOperation] to inside the completion block of the NSURLSessionDataTask or sendAsynchronousRequest. Your current operation class is completing prematurely (and therefore would not honor dependencies and your network operation queue's intended maxConcurrentOperationCount).

  2. The retry logic seems unremarkable. Perhaps something like:

    - (void)main
    {
        NSURLRequest *request = [self createRequest]; // maybe move the request creation stuff into its own method
    
        [self tryRequest:request currentDelay:1.0];
    }
    
    - (void)tryRequest:(NSURLRequest *)request currentDelay:(NSTimeInterval)delay
    {
        [NSURLConnection sendAsynchronousRequest:request queue:[self networkOperationCompletionQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    
            BOOL success = NO;
    
            if (connectionError) {
                NSLog(@"%@ Error: %@", self.serviceName, connectionError.localizedDescription);
            } else {
                if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
                    NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
                    if (statusCode == 200) {
                        // parse XML response here; if successful, set `success` to `YES`
                    }
                }
            }
    
            if (success) {
                [self completeOperation];
            } else {
                dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC));
                dispatch_after(popTime, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
                    NSTimeInterval nextDelay = [self nextDelayFromCurrentDelay:delay];
                    [self tryRequest:request currentDelay:nextDelay];
                });
            }
        }];
    }
    
  3. Personally, I'm wary about this entire endeavor. It strikes me that you should be employing logic conditional upon the type of error. Notably, if the error is a failure resulting from lacking of internet connectivity, you should use Reachability to determine connectivity and respond to notifications to retry automatically when connectivity is restored, not simply retrying at prescribed mathematical progression of retry intervals.

    Other than network connectivity (which is better addressed with Reachability), I'm unclear as to what other network failures warrant a retry logic.

Some unrelated observations:

  1. Note, I eliminated the dispatch_async of the issuing of the request in main to a background queue because you're using asynchronous methods already (and even if you weren't, you've presumably added this operation to a background queue, anyway).

  2. I've also removed the try/catch logic because, unlike other languages/platforms, exception handling is not the preferred method of handling runtime errors. Typically runtime errors in Cocoa are handled via NSError. In Cocoa, exceptions are generally used solely to handle programmer errors, but not to handle the runtime errors that a user would encounter. See Apple's discussion Dealing with Errors in the Programming with Objective-C guide.

  3. You can get rid of your manually implemented isExecuting and isFinished getter methods if you just define the appropriate getter method for your properties during their respective declarations:

    @property (nonatomic, readwrite, getter=isExecuting) BOOL executing;
    @property (nonatomic, readwrite, getter=isFinished)  BOOL finished;
    
  4. You might, though, want to write your own setExecuting and setFinished setter methods, which do the notification for you, if you want, e.g.:

    @synthesize finished  = _finished;
    @synthesize executing = _executing;
    
    - (void)setExecuting:(BOOL)executing
    {
        [self willChangeValueForKey:kExecutingKey];
        _executing = executing;
        [self didChangeValueForKey:kExecutingKey];
    }
    
    - (void)setFinished:(BOOL)finished
    {
        [self willChangeValueForKey:kFinishedKey];
        _finished = finished;
        [self didChangeValueForKey:kFinishedKey];
    }
    

    Then, when you use the setter it will do the notifications for you, and you can remove the willChangeValueForKey and didChangeValueForKey that you have scattered about your code.

  5. Also, I don't think you need to implement isCancelled method (as that's already implemented for you). But you really should override a cancel method which calls its super implementation, but also cancels your network request and completes your operation. Or, instead of implementing cancel method, you could move to the delegate based rendition of the network requests but make sure you check for [self isCancelled] inside the didReceiveData method.

    And isCompleted strikes me as redundant with isFinished. It seems like you could entirely eliminate completed property and isCompleted method.

  6. You're probably unnecessarily duplicating the amount of network code by supporting both NSURLSession and NSURLConnection. You can do that if you really want, but they assure us that NSURLConnection is still supported, so it strikes me as unnecessary (unless you wanted to enjoy some NSURLSession specific features for iOS 7+ devices, which you're not currently doing). Do whatever you want, but personally, I'm using NSURLConnection where I need to support earlier iOS versions, and NSURLSession where I don't, but I wouldn't be inclined to implement both unless there was some compelling business requirement to do so.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • "You can get rid of your manually implemented isExecuting and isFinished." Actually, no. Synthesized property accessor aren't enough for concurrent NSOperations. – Gwendal Roué Jun 13 '14 at 16:06
  • @GwendalRoué Perhaps you can clarify why you contend that the automatically synthesized getters are inadequate. If your property declaration specifies the `getter=isExecuting` and `getter=isFinished`, as appropriate, then the getters bear the right name and everything works brilliantly. I've used this pattern extensively for years without incident. I've never written manually getters for my concurrent operations. Maybe you can explain why you think the synthesized getters "aren't enough." – Rob Jun 13 '14 at 16:37
  • Sure. The issue is only visible when there are several operations in a NSOperationQueue, and those operations depend on each other. For instance, one is a dependency of another. Or the queue won't run more than N operations simultaneously (some operations must wait others to complete before they can start). Synthesized accessors won't notify NSOperationQueue of the operation states. And the scheduling of operations does not work: dependencies don't get ready, and never start, queue won't get empty, etc. – Gwendal Roué Jun 13 '14 at 16:43
  • Oh I see what you mean. My answer could have been misconstrued as suggesting that you didn't need to do the `will`/`didChangeValueForKey`. No, you're right that you need those. My point was merely that you don't have to write manual getters. I've tried to clarify my answer. – Rob Jun 14 '14 at 18:43
  • Synthesized getters work fine. Your answer is sending notifications for "isExecuting" and "isFinished", which are the incorrect key paths. "executing" and "finished" would be correct, and send automatic notifications - allowing dependancies, etc. to work correctly. – quellish Sep 20 '14 at 22:14
  • executing, finished etc. are not properties. You can't override them as properties. You need to override "isExecuting", "isFinished", making them dependent on some instance variable, and call will/didChangeValueForKey before/after changing these instance variables. – gnasher729 Sep 20 '14 at 22:59
  • @gnasher729 I disagree. If you look at the iOS 8 `NSOperation.h` header (which give us a glimpse to what Apple is doing behind the scenes), and you'll see that there _are_ `executing` and `finished` properties (that presumably do `isExecuting` and `isFinished` notifications internally, but only expose the `readonly` property). – Rob Sep 21 '14 at 01:25
  • @quellish No, that's not correct. For `NSOperation` dependencies to work properly, you must post notifications of `isFinished` and `isExecuting` as outlined in _Defining a Custom Operation Object_ section of the [Operation Queue](https://developer.apple.com/library/mac/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101-SW1) chapter of the _Concurrency Programming Guide._ They're very clear on the notifications necessary and it's not `finished` and `executing`, but rather `isFinished` and `isExecuting`. – Rob Sep 21 '14 at 01:28
  • @Rob, the documentation is incorrect. You pointed another user to the header - where you should see `asynchronous` , which isn't in the documentation but has been present for over a year. NSOperation should be sending a notification for both `executing` and `isExecuting` to preserve compatibility with pre-iOS 4 applications. If you do not see it doing so, file a bug. `executing` is the preferred of the two. – quellish Sep 21 '14 at 02:04
  • @Rob, then it isn't KVO compliant, is it? File a bug. I tested it and get different behavior than you describe. – quellish Sep 21 '14 at 02:40
  • @Rob you are saying that it emits KVO notifications for `isFinished`, but not `finished`, with `finished` being the declared property. To be KVO compliant, an class must be first KVC compliant, then it the class must emit KVO change notifications *for the property*, and register dependent keys. You are saying NSOperation does not meet the criteria for KVO compliance. That would justify a bug. KVO notifications should be sent for the *property* key path - this is what KVO compliant classes do. – quellish Sep 21 '14 at 04:20
  • No. I'm saying that if you have properties called `finished` and `executing`, that they will not automatically issue KVO notifications for the keys named `isFinished` and `isExecuting` (nor would you expect it to, regardless of what the getter is called). But the documentation is correct, that proper `NSOperation` handling requires those two notifications, specifically for asynchronous operations (i.e. those that only finish _after_ you return from `main`/`start`). That's why you _must_ issue these notifications manually and to suggest otherwise is simply incorrect. – Rob Sep 21 '14 at 19:01
0

Your method:

static NSString * const kFinishedKey = @"isFinished";
static NSString * const kExecutingKey = @"isExecuting";

- (void)completeOperation
{
    [self willChangeValueForKey:kFinishedKey];
    [self willChangeValueForKey:kExecutingKey];

    self.executing = NO;
    self.finished = YES;

    [self didChangeValueForKey:kExecutingKey];
    [self didChangeValueForKey:kFinishedKey];
}

Is manually sending notifications for the key paths "isFinished" and "isExecuting". NSOperationQueue observes the key paths "finished" and "executing" for those states - "isFinished" and "isExecuting" are the names of the get (read) accessors for those properties.

These are not the key paths you were looking for

For an NSOperation subclass KVO notifications should be sent automatically unless your class has opted out of automatic KVO notifications by implementing +automaticallyNotifiesObserversForKey or +automaticallyNotifiesObserversOf<Key> to return NO. You can see this demonstrated in a sample project here.

Your property declarations:

@property (nonatomic) BOOL executing;
@property (nonatomic) BOOL finished;
@property (nonatomic) BOOL cancelled;

Are overriding those in NSOperation without providing the correct get accessor. Change these to:

@property (nonatomic, getter=isExecuting) BOOL executing;
@property (nonatomic, getter=isFinished) BOOL finished;
@property (nonatomic, getter=isCancelled) BOOL cancelled;

To get the correct behavior for an NSOperation. NSOperation declares these as readonly in the public interface, you have the option of making them readwrite in a private class extension.

As far as implementing a connection with retry logic, there is an excellent Apple sample code project that demonstrates this, MVCNetworking

quellish
  • 21,123
  • 4
  • 76
  • 83
  • As discussed above, it is not correct that "`NSOperationQueue` observes the key paths '`finished`' and '`executing`' for those states". The documentation is very clear that the KVN names are `isFinished` and `isExecuting`. I think it's a horrible design, but that's how it works. – Rob Sep 21 '14 at 03:52