Zip of two Futures crashes if the first Future fail

I have this weird example that crashes when the Zip is called.
The more weird issue is that changing the order of Zip parameters it works.

It seems that when the first future fails, Zip tries to cancels the second one, but it is already deallocated.
Is it a bug, or is there a catch?

    enum Error: Swift.Error {
        case error
    }

    let one = Future<String, Error> { promise in
        promise(.failure(.error))
    }

    let two = Future<String, Error> { promise in
        promise(.success("world"))
    }

    let cancelabel = Publishers.Zip(two, one).sink(receiveCompletion: {
        print($0)
    }, receiveValue: {
        print($0)
    })

    print(cancelabel)

That seems like a bug. The zip should always retain both its dependent publishers (future or otherwise), and the sink should be retaining the zip. This should hold regardless of its completion state.

Related Feedback FB7621739

Wrapping the first promise in an async prevent the crash:

   enum Error: Swift.Error {
        case error
    }

    let one = Future<String, Error> { promise in
        DispatchQueue.main.async {
            promise(.failure(.error))
        }
    }

    let two = Future<String, Error> { promise in
        promise(.success("world"))
    }

    let cancelabel = Publishers.Zip(one, two).sink(receiveCompletion: {
        print($0)
    }, receiveValue: {
        print($0)
    })

    print(cancelabel)

After some tests It seems that the problem is with the Future, so as workaround I create a Single publisher to be used as synchronous Future.

struct Single<Output, Failure>: Publisher where Failure: Swift.Error {

    let result: Result<Output, Failure>

    init(_ block: () -> Result<Output, Failure>) {
        result = block()
    }

    init(result: Result<Output, Failure>) {
        self.result = result
    }

    func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S: Subscriber {
        subscriber.receive(subscription: CustomSubscription(result: result, downstream: subscriber))
    }

    class CustomSubscription<Downstream: Subscriber>: Subscription where Output == Downstream.Input, Failure == Downstream.Failure {

        private(set) var downstream: Downstream?
        let result: Result<Output, Failure>

        init(result: Result<Output, Failure>, downstream: Downstream) {
            self.downstream = downstream
            self.result = result
        }

        func request(_ demand: Subscribers.Demand) {
            guard let downstream = self.downstream else {
                return
            }

            self.downstream = nil

            switch result {
            case .failure(let error):
                downstream.receive(completion: .failure(error))
            case .success(let value):
                if demand != .none {
                    _ = downstream.receive(value)
                }
                downstream.receive(completion: .finished)
            }
        }

        func cancel() {
            downstream = nil
        }
    }
}

I created a repository with a sample project and the workaround CombineFutureBug.

1 Like

It's fixed on Xcode 11.4 beta 3.

2 Likes