5
\$\begingroup\$

I want to check the existence of various API endpoints by doing serial URL request. If the first one fails, I want to try the second one, if the second fails I want to try the third one, etc.

Here is some code I use to do this:

self.apiEndpoint = kApiAjaxEndpoint;
[self testEndpointWithBlock:^(id response, NSError *error) {
    if (error) {
        self.apiEndpoint = kApiAjaxLegacyEndpoint;
        [self testEndpointWithBlock:^(id response, NSError *error) {
            if (error) {
                self.apiEndpoint = kApiRestEndpoint;
            }
            [self saveApiEndpoint];
        }];
    } else {
        [self saveApiEndpoint];
    }
}];

So for now I'm only testing for 2 API endpoints but the moment I add a new endpoint I would need to add another call to testEndpointWithBlock: with error handling code.

I was wondering if you can think of a more elegant way of doing this?

\$\endgroup\$
3
\$\begingroup\$
  1. Create several instances of NSOperation
  2. Set dependencies so that one runs after another.
  3. Add to a private NSOperationQueue.
  4. Cancel all operations in a queue if a match is found.
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ This seems like a nice, lightweight solution, and gets the URL requests off the main thread if testEndpointWithBlock: doesn't. \$\endgroup\$ – boztalay Jan 7 '14 at 7:26
  • \$\begingroup\$ Even if you use NSOperations and dependencies, you must run asynchronous NSOperations, please see this blog post (it's for Swift, but you can get the idea) \$\endgroup\$ – webo80 Mar 14 '17 at 16:46
1
\$\begingroup\$

I was wondering if you can think of a more elegant way of doing this?

Asynchronous problems getting quickly more complicated than one would expect. If you don't mind to utilize a third party library, you can find an easy solution to those kind of problems:

The third party library would implement a Promise. A promise represents the eventual result of an asynchronous operation. Your asynchronous method for testing any endpoint may then look as shown below:

- (Promise*) testEndpoint:(Endpoint*)endpoint;

Note: no completion handler. Notifying the call-site is solved differently, through registering a success and an error handler via a then method (or property). The success handler passes the result back to the call-site, and the error handler passes the error back to the call-site:

Promise* promise = [self testEndpoint:endpoint];

promise.then(mySuccessHandler, myErrorHandler);

And shorter:

[self testEndpoint:endpoint]
.then(id^(id result){
    // result is the eventual result of the asynchronous task
    return nil; 
}, id^(NSError* error){
   // error is the failure reason of the asynchronous task
   return nil;
});

Note: then(mySuccessHandler, myErrorHandler) will return a Promise. The returned Promise's value becomes the return value of the handler (either of the success or failure handler).

So, given

Promise* promise = task().then(^(id result){ return @"OK"}, nil);

then, when the task succeeds, the below statement will eventually become true:

`[promise get] isEqualToString: @"OK"`

Otherwise, if the error handler equals nil as above, errors will be propagated to the child promise:

`[[promise get] isKindOfClass:[NSError class]]`

For example, using a helper method all::

A) Run all tests in parallel and return an array of the results:

If the test succeeds, return a string @"OK", otherwise return @"not reachable".

Create an array of Promises, whose associated task is already running:

NSArray* requests = @{
    [self testEndpoint:A].then(^id(id result){return @"OK";}, ^id(NSError* error){return @"not reachable";}),
    [self testEndpoint:B].then(^id(id result){return @"OK";}, ^id(NSError* error){return @"not reachable";}),
    [self testEndpoint:C].then(^id(id result){return @"OK";}, ^id(NSError* error){return @"not reachable";}),
    [self testEndpoint:D].then(^id(id result){return @"OK";}, ^id(NSError* error){return @"not reachable";}),
};

When all tests are finished then execute the handler:

[Promise all:requests].then(^id(id results){
    // results is a NSArray containing the result of each 
    // task above in the same order .
    ...
    return nil;
}, nil);

Likewise, using a helper method any::

B) Run all tests in parallel. When the first finished return its result and stop the other tasks:

NSArray* requests = ...; // same as above

[Promise any:requests].then(^id(id result){
    // Parameter result is the result of the first task that finished.
    ...
    return nil;
}, nil);

When the first task finished, all other promises will be send a cancel message, which (if implemented) cancels the underlying asynchronous task.

Or,

C) Run tasks in sequence until a break condition becomes true:

NSArray* endpoints = ...;

const NSUInteger count = [endpoints count];
__block NSUInteger i = 0;
__block BOOL done = NO;
[RXPromise repeat:^RXPromise *{
    if (i >= count || done) {
        return nil;  // returning nil breaks the asynchronous loop
    }
    return [self testEndpoint:endpoints[i]].then(^id(id result){
        // found endpoint
        ++i;
        done = YES;
        return nil;
    }, ^id(NSError* error){
       ++i;
        return nil; // nil continues, returning an error would break the loop
    });
}];

For a more detailed overview of Promises and a list of implementations you may read here: https://softwareengineering.stackexchange.com/questions/184597/success-failure-blocks-vs-completion-block/219929#219929

Disclosure: the code snippets above used the API of the RXPromise library. Other libraries may have slightly differing APIs, and may or may not provide the same functionality. I'm the author of RXPromise.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.