Timeouts in ReactiveCocoa

On my current project we are using ReactiveCocoa to manage Core Bluetooth’s asynchronous callbacks when communicating with a Bluetooth Low Energy device in an iOS app. John Fisher recently explained how he’s used ReactiveCocoa to chain asynchronous operations together in this same project.

As with any application that communicates with a remote device/server, we’ve run into the need for timeouts when we don’t get an expected response in a reasonable amount of time. This got me to wondering if there might be something built into ReactiveCocoa to help with this problem. And it turns out there is! There’s a -timeout:onScheduler: operator available for use on any RACSignal. In this post I’ll show a simple example of how -timeout:onScheduler: can be used to manage a long-running asynchronous task.

ll start by showing the entire example, and then provide some explanation of what’s happening.

  RACSignal *signal =
      [RACSignal createSignal:^RACDisposable *(id  subscriber) {

          RACDisposable *disposable = [RACDisposable new];

          dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
              NSLog(@"Start iterating...");
              for (int i = 0; i < 200 && !disposable.isDisposed; i++) {
                  NSLog(@"Send %i to subscriber", i);
                  [subscriber sendNext:@(i)];
                  
                  [NSThread sleepForTimeInterval:0.1];
              }
              
              if (!disposable.isDisposed) {
                  NSLog(@"Send completed to subscriber");
                  [subscriber sendCompleted];
              }
          });

          return disposable;
      }];

  NSLog(@"About to subscribe");

  [[[signal
      deliverOn:[RACScheduler mainThreadScheduler]]
      timeout:1.0 onScheduler:[RACScheduler mainThreadScheduler]]
      subscribeNext:^(id x) {
          NSLog(@"Got next: %@", x);
      } error:^(NSError *error) {
          NSLog(@"Error (timeout): %@", [error localizedDescription]);
      } completed:^{
          NSLog(@"Completed");
      }];

The Subscription

Let’s start with the subscription code first, and then move back up to the signal that wraps the long-running asynchronous work. Line 27 declares that calls to our subscribeNext: block should be on the main thread. And line 28 is where the timeout is declared. The timeout operator needs number of seconds until the timeout will fire (1 second in this example), and the scheduler that the timeout error should be raised on (I’m specifying the main thread again).

The rest is just the standard subscribeNext:error:completed handlers that log the values sent on the signal, an error message if the signal errors out, and a log statement indicating the signal has completed.

This results in a subscription to a signal that will log each value sent on the signal until it either completes, or 1 second has elapsed, at which point the signal will error.

Long-Running Asynchronous Work

The “work” that is being done in this example is just a loop that goes from 0 to 199 with a 0.1 second delay between each iteration. This work is done on a background GCD queue so as not to block the main thread. Each time through the loop, the current value of the counter is sent to the subscriber.

If you take a look at the for loop on line 8, you’ll notice that in addition to the normal check of the value of i, it’s also checking to see that the subscriber hasn’t disposed of its subscription:

for (int i = 0; i < 200 && !disposable.isDisposed; i++) {

And then again on line 15, disposable is checked before informing the subscriber of the completion of the signal:

  if (!disposable.isDisposed) {

When a subscriber to a RACSignal breaks its subscription, for any reason, the RACDisposable associated with that subscription (in this case the one that was created in the -createSignal: call) will be updated to respond to -isDisposed with YES. I am taking advantage of this here, as the timeout I’m applying to our subscription causes the subscription to error out, and results in it being disposed of.

By paying attention to the isDisposed value of the disposable, the worker can immediately stop doing its work when the subscriber goes away. This is a very powerful concept. Imagine a signal that’s wrapping an asynchronous call to the OS. When the callback occurs, a RACDisposable could be checked to see if further processing needs to be done or not.

Results

If you run the example, you’ll see something like

[95902:70b] About to subscribe
[95902:e03] Start iterating...
[95902:e03] Send 0 to subscriber
[95902:70b] Got next: 0
[95902:e03] Send 1 to subscriber
[95902:70b] Got next: 1
[95902:e03] Send 2 to subscriber
[95902:70b] Got next: 2
[95902:e03] Send 3 to subscriber
[95902:70b] Got next: 3
[95902:e03] Send 4 to subscriber
[95902:70b] Got next: 4
[95902:e03] Send 5 to subscriber
[95902:70b] Got next: 5
[95902:e03] Send 6 to subscriber
[95902:70b] Got next: 6
[95902:e03] Send 7 to subscriber
[95902:70b] Got next: 7
[95902:e03] Send 8 to subscriber
[95902:70b] Got next: 8
[95902:e03] Send 9 to subscriber
[95902:70b] Got next: 9
[95902:70b] Error (timeout): The operation couldn’t be completed. (RACSignalErrorDomain error 1.)

After subscribing to the signal, it starts counting up, sending each number on the signal. After one second, a timeout error halts the signal, and the loop incrementing the counter stops immediately (due to isDisposed being flipped to true).

Conclusion

If you are using ReactiveCocoa to manage asynchronous, potentially long-running work — like requests to external devices/APIs — the timeout operator can provide a clean, integrated way of preventing those operations from taking too long to finish when they get hung up.
 

Conversation
  • Brian says:

    Nicely explained!!

  • Comments are closed.