By Casey Liss
Under Pressure

I’ve been spending the last several days discussing the differences between RxSwift and Apple’s new Combine framework:

Today, we’ll discuss backpressure.

An Illustrated Example

Do you remember this famous scene from I Love Lucy?

If you’re one of the six people on the planet who hasn’t seen it, Lucy and Ethel are attempting to wrap chocolates as they come down a conveyor belt. Before long, the chocolates come far faster than the women can handle, and things get interesting. And hilarious.

This short video is actually a phenomenal example of backpressure.

Backpressure

In the video above, the chocolates coming down the conveyor belt are basically an Observable/Producer. The chocolates were being emitted at whatever speed they wanted to be, and that was that.

The conveyor/Observable/Producer operating at whatever speed it sees fit makes for great comedy. However, it can make for some complicated circumstances in code.

Let’s suppose you’re writing a banking iOS app. Perhaps somewhere in your app you have a mechanism for depositing a check by taking a picture of it. At the end of that process is a button which the user taps to actually commit the deposit.

A nefarious user wants to see if they can get some free money. They decide to mash down on that button a zillion times in a row, hoping your app won’t be smart enough to handle it appropriately. They hope that instead you’ll repeat the deposit a zillion times, and effectively make it rain for them. Suddenly, we have an I Love Lucy scenario: the button taps are coming down the conveyor belt far faster than we can handle them.

(Naturally there a zillion other ways to handle this — most notably immediately disabling the button — but just roll with me on this, m’kay?)

What Lucy needed, and what we need in this contrived example, is a way to say “I’ll take just one pleaseandthankyou”. We need a way to throttle the speed with which chocolates are sent down the conveyor belt, and clicks are sent down that stream.

I’ve Got Your Backpressure Right Here

RxSwift takes an interesting approach to backpressure.

🚨🚨🚨🚨🚨🚨🚨

RxSwift does not include any affordances
for dealing with backpressure.

🚨🚨🚨🚨🚨🚨🚨

In RxSwift, we would have been no better off than Lucy. Those chocolates would have kept coming, whether or not we could handle them. Some of the projects under the ReactiveX umbrella do handle backpressure, but RxSwift is not one of them. For more, you can read the official ReactiveX entry on backpressure. In short, it pretty much says “good luck”.

Combining Flow and Pressure

Likely unsurprisingly by now, Combine takes a different approach to backpressure: it’s built into the system.

Look at the [slightly simplified] definition for protocol Subscriber:

protocol Subscriber {
     associatedtype Input
     associatedtype Failure : Error
     
     // Notifies the subscriber that it has successfully subscribed
     func receive(subscription: Subscription)
     
     // Notifies the subscriber that there is a new element; the
     // equivalent of RxSwift's onNext()
     func receive(_ input: Self.Input) -> Subscribers.Demand
     
     // Notifies the subscriber that it has completed; the
     // equivalent of both RxSwift's onCompleted() and onError()
     func receive(completion:)
}

Wait a second. In RxSwift’s Observer, things looked a little different:

protocol Observer {
    func onNext(Element)
    func onComplete()
    func onError(Error)
}

Do you see the difference there? I don’t mean the splitting out of onError() and onComplete(). Look at the return types. Specifically, look at the return types for receive(input:) and onNext(Element):

// Combine
func receive(_ input: Self.Input) -> Subscribers.Demand

// RxSwift
func onNext(Element)

RxSwift’s onNext() doesn’t return anything, whereas Combine’s receive(input:) does. So what the hell is Subscribers.Demand

Simplified, here it is:

public enum Demand {
    case unlimited
    case max(Int)
}

There’s your backpressure.

When a Subscriber is notified by a Producer that there is a new element available in Combine, the Subscriber is expected to return a Subscribers.Demand. By doing so, the Subscriber is indicating to the Producer how many more elements it’s willing to accept.

✅ ✅ ✅ ✅ ✅ ✅ ✅

Combine accounts for backpressure at its core.

✅ ✅ ✅ ✅ ✅ ✅ ✅

The number of elements a subscriber is willing to accept can be effectively infinite (.unlimited), or a specific number (.max(1)). In Lucy’s case, she may return .max(3), knowing she can do about three chocolates at a time. In the case of our deposit button handler, we may return .max(1), thereby preventing more than one deposit.

[Still the] Same as It Ever Was

Just like the difference in error handling, there’s not really a clearly right or wrong answer between RxSwift’s and Combine’s approaches. Both projects have made design decisions, all of which are completely reasonable. To me, this is what makes engineering fun: balancing the pros and cons to different approaches and coming up with a solution that makes the best possible trade offs.

As with the discussion on error handling, the Combine approach leads to a little bit more bookkeeping, but a more robust solution.

In my experience, I can’t say I’ve had many occasions where I’ve thought “oh man, I wish RxSwift had backpressure”. However, I can pretty easily eliminate backpressure from Combine by simply using Demand.unlimited whenever I’m asked for a Demand. Thus, this design decision I find less bothersome than the choices Combine makes about error handling. Handling backpressure is far less of a bookkeeping burden.

Wrapping Up

In my next post, I’ll summarize the differences between the projects, and give a[n initial] ruling on what I plan to do in Vignette, and other projects going forward.