Reconnection Strategy with Diffusion and iOS Apps

When developing an iOS app with Diffusion, you need to take careful consideration of the app’s life cycle.

Diffusion is designed to maintain a persistent connection to your client apps. In a mobile environment, the app can be backgrounded at any time, meaning that the app will need to reconnect to the server when it is restored. In this post, we’ll explore the default reconnection behavior, and then explain how you can use a custom reconnection strategy tailored to the needs of your particular app.

Consider an iOS client app which connects to the Diffusion server, creates a session, and subscribes to a set of topics.

If the user switches to another app or locks the device, the Diffusion client app is placed in the background, triggering the delegate handler applicationDidEnterBackground: at the AppDelegate level. While the app is in the background, it cannot send or receive any messages from the Diffusion server.

The Diffusion server pings connected clients regularly to check they are still connected (by default, every 90 seconds; this can be configured using the system-ping-frequency property in Connectors.xml). The server determines that a connection should be closed if it pings a client and fails to receive a reply for two pings in a row.

However, the server will keep the session alive and available for reconnection in a DISCONNECTED state for some time before it is closed permanently. By default this is 300 seconds, and can be configured with the keep-alive value in Connectors.xml.

While the app is in the background, iOS can terminate the app at any point.

If the app is resumed by the user, the delegate handler applicationDidBecomeActive: is called.
In this handler, the state and validity of the session need to be checked. For example:

-(void)testConnectionWithServer {
    if (self.session) {
        [self.session.pings pingServerWithCompletionHandler:^(PTDiffusionPingDetails * _Nullable details, NSError * _Nullable error) {
            if (error) {
                // only goes here after attempting all possible solutions in the reconnection strategy
                
                if ([error.domain isEqualToString:PTDiffusionSessionErrorDomain]) {
                    [self.session close];
                    self.session = nil;
                    [self connectToURL:self->_url withCompletionHandler:nil];
                }
            } else {
                NSLog(@"Ping successful (%dms)", (int) round(details.roundTripTime * 1000));
            }
        }];
    } else {
        NSLog(No session detected. Aborting");
    }
}

When the app becomes active, it checks if a session has been stored in the self.session variable. If so, the app will ping the server.

During the ping call, if the client cannot connect to the server, the client session will enter RECOVERING_CONNECT state, and the client’s reconnection strategy will come into action.

By default, Diffusion clients follow a simple reconnection strategy, which is just to try to reconnect at 5 second intervals for 60 seconds.

This default reconnection strategy is not always suitable for every application. For example, you may prefer to attempt the first reconnection immediately rather than waiting 5 seconds. You may also wish to have increasing delays between connection attempts to avoid overloading the server if a lot of clients are trying to reconnect at once (for example, if the server has just restarted).

We can define a custom reconnection strategy for a particular session via the PTDiffusionMutableSessionConfiguration, passing it as a parameter when opening a new session with openWithURL:configuration:completionHandler:.

The chosen reconnection strategy for this example is a back-off reconnection strategy, where clients will try to reconnect immediately, but then have an increasing delay between later attempts.

To implement this, we create a new class that conforms to the PTDiffusionSessionReconnectionStrategy protocol:

@interface BackOffReconnectionStrategy: NSObject<PTDiffusionSessionReconnectionStrategy>

Then we need to implement the procedure diffusionSession:wishesToReconnectWithAttempt::

-(void)diffusionSession:(PTDiffusionSession *)session wishesToReconnectWithAttempt:(PTDiffusionSessionReconnectionAttempt *)attempt {
    // calculate the delay based on the amount of attempts while reconnecting to the server
    NSTimeInterval delay = MIN(_currentAttempt * _delayIncrementation, _maxDelay);
  
    _currentAttempt += 1;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [attempt start];
    });
}

This is a simple reconnection strategy which attempts to reconnect immediately then increments the delay between attempts by one second (determined by _delayIncrementation), up to a maximum of 5 seconds (determined by _maxDelay).

However, once the app has reconnected to the server using this reconnection strategy, the _currentAttempt variable is not cleared so the lingering _currentAttempt value will increase the delay for the following reconnection attempt.

To solve this, we need to create a control variable that determines when the last reconnection attempt was made:

-(void)diffusionSession:(PTDiffusionSession *)session wishesToReconnectWithAttempt:(PTDiffusionSessionReconnectionAttempt *)attempt {
    // calculate the delay based on the amount of attempts while reconnecting to the server
    NSTimeInterval delay = MIN(_currentAttempt * _delayIncrementation, _maxDelay);
    
    // if a connection attempt was made in the past of this session, _lastConnectionAttempt has a value set)
    if (_lastConnectionAttempt) {
        NSTimeInterval elapsedSinceLastConnection = [[NSDate date] timeIntervalSinceDate:_lastConnectionAttempt];
        
        if (elapsedSinceLastConnection > delay * 2) {
            _currentAttempt = 0;
            delay = MIN(_currentAttempt * _delayIncrementation, _maxDelay);
        }
    }
    _lastConnectionAttempt = [NSDate date];
    _currentAttempt += 1;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [attempt start];
    });
}

This way, if the time elapsed from the last reconnection attempt is longer than twice the last applied delay, it is considered that the last attempt was successful, resetting the _currentAttempt value, and enabling an immediate reconnection attempt.

You should pick the values used to determine when to reset _currentAttempt based on the details of your custom reconnection strategy.