Retrying an asynchronous operation using ReactiveC

2019-02-07 03:25发布

问题:

I'm using ReactiveCocoa signals to represent calls to RESTful backend in our system. Each RESTful invocation should receive a token as one of the parameters. The token itself is received from authentication API call.

All works fine and we're now introduced token expiration, so the backend access class may need to reauthorize itself if the API call fails with HTTP code 403. I want to make this operation completely transparent for the callers, this is the best I came up with:

- (RACSignal *)apiCallWithSession:(Session *)session base:(NSString *)base params:(NSDictionary *)params get:(BOOL)get {
    NSMutableDictionary* p = [params mutableCopy];
    p[@"token"] = session.token;

    RACSubject *subject = [RACReplaySubject subject];

    RACSignal *first = [self apiCall:base params:p get:get];  // this returns the signal representing the asynchronous HTTP operation

    @weakify(self);
    [first subscribeNext:^(id x) {
        [subject sendNext:x];   // if it works, all is fine
    } error:^(NSError *error) {
        @strongify(self);

        // if it doesn't work, try re-requesting a token
        RACSignal *f = [[self action:@"logon" email:session.user.email password:session.user.password]
                         flattenMap:^RACStream *(NSDictionary *json) {  // and map it to the other instance of the original signal to proceed with new token
            NSString *token = json[@"token"];

            p[@"token"] = token;
            session.token = token;

            return [self apiCall:base params:p get:get];
        }];

        // all signal updates are forwarded, we're only re-requesting token once            
        [f subscribeNext:^(id x) {
            [subject sendNext:x];
        } error:^(NSError *error) {
            [subject sendError:error];
        } completed:^{
            [subject sendCompleted];
        }];
    } completed:^{
        [subject sendCompleted];
    }];

    return subject;
}

Is this the right way to do it?

回答1:

First of all, subscriptions and subjects should generally be avoided as much as possible. Nested subscriptions, in particular, are quite an anti-pattern—usually there are signal operators that can replace them.

In this case, we need to take advantage of the fact that signals can represent deferred work, and create only one signal to perform the actual request:

// This was originally the `first` signal.
RACSignal *apiCall = [RACSignal defer:^{
    return [self apiCall:base params:p get:get];
}];

The use of +defer: here ensures that no work will begin until subscription. An important corollary is that the work can be repeated by subscribing multiple times.

For example, if we catch an error, we can try fetching a token, then return the same deferred signal to indicate that it should be attempted again:

return [[apiCall
    catch:^(NSError *error) {
        // If an error occurs, try requesting a token.
        return [[self
            action:@"logon" email:session.user.email password:session.user.password]
            flattenMap:^(NSDictionary *json) {
                NSString *token = json[@"token"];

                p[@"token"] = token;
                session.token = token;

                // Now that we have a token, try the original API call again.
                return apiCall;
            }];
    }]
    replay];

The use of -replay replaces the RACReplaySubject that was there before, and makes the request start immediately; however, it could also be -replayLazily or even eliminated completely (to redo the call once per subscription).

That's it! It's important to point out that no explicit subscription was needed just to set up the work that will be performed. Subscription should generally only occur at the "leaves" of the program—where the caller actually requests that work be performed.