ioscombinereactive-swift

os_unfair_lock_recursive_abort on Subscriber.receive(completion: .finished) in iOS 18


I have the following code for mapping ReactiveSwift SignalProducer to Combine Publisher:

import Combine
import Foundation
import ReactiveSwift

/// This is a custom Publisher that wraps SignalProducer
/// and allows to map ReactiveSwift.SignalProducer to Combine.Publisher
public struct ReactivePublisher<Output, Failure>: Publisher where Failure: Error {
    public let signal: SignalProducer<Output, Failure>

    public init(_ signal: SignalProducer<Output, Failure>) {
        self.signal = signal
    }

    public func receive<S: Subscriber>(subscriber: S) where S.Input == Output, S.Failure == Failure {
        let subscription = Subscription(output: signal, subscriber: subscriber)
        subscriber.receive(subscription: subscription)
    }
}

private extension ReactivePublisher {
    final class Subscription<S: Subscriber> where S.Input == Output, S.Failure == Failure {
        private let output: SignalProducer<Output, Failure>
        private var subscriber: S?

        init(output: SignalProducer<Output, Failure>, subscriber: S) {
            self.output = output
            self.subscriber = subscriber
        }
    }
}

extension ReactivePublisher.Subscription: Cancellable {
    func cancel() {
        subscriber = nil
    }
}

extension ReactivePublisher.Subscription: Subscription {
    func request(_: Subscribers.Demand) {
        guard let subscriber = subscriber else { return }
        output.take(duringLifetimeOf: self)
            .start { [weak self] event in
                switch event {
                case let .value(next):
                    _ = subscriber.receive(next)
                case .interrupted:
                    self?.cancel()
                case let .failed(error):
                    subscriber.receive(completion: .failure(error))
                case .completed:
                    subscriber.receive(completion: .finished)
                }
            }
    }
}

extension SignalProducer {
    func asPublisher() -> AnyPublisher<Value, Error> {
        ReactivePublisher(self).eraseToAnyPublisher()
    }
} 

On iOS 18 only, when I get .completed on a SignalProducer and call subscriber.receive(completion: .finished), I see the following stack trace:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=1, subcode=0x21881b0c8)
    frame #0: 0x000000021881b0c8 libsystem_platform.dylib`_os_unfair_lock_recursive_abort + 36
    frame #1: 0x00000002188182d8 libsystem_platform.dylib`_os_unfair_lock_lock_slow + 308
    frame #2: 0x000000019a869c08 Combine`Combine.AbstractZip.receive(completion: Combine.Subscribers.Completion<τ_0_1>, index: Swift.Int) -> () + 280
  * frame #3: 0x000000019a869ae4 Combine`Combine.AbstractZip.Side.receive(completion: Combine.Subscribers.Completion<τ_0_1>) -> () + 24
    frame #4: 0x000000010134fa84 myfirstam`closure #1 in ReactivePublisher.Subscription.request(event=completed, subscriber=<unavailable>, self=nil) at ReactivePublisher.swift:52:32
    frame #5: 0x000000010804ad84 ReactiveSwift`Signal.Observer.send(event=completed, self=0x00000003013cc780) at Signal.Observer.swift:111:4
    frame #6: 0x000000010802dee0 ReactiveSwift`Signal.Core.tryToCommitTermination(self=0x000000030057e600) at Signal.swift:231:17
    frame #7: 0x000000010802cf10 ReactiveSwift`Signal.Core.send(event=completed, self=0x000000030057e600) at Signal.swift:115:5
    frame #8: 0x000000010802ca2c ReactiveSwift`implicit closure #2 in implicit closure #1 in Signal.Core.init(event=completed, self=0x000000030057e600) at Signal.swift:80:36
    frame #9: 0x000000010804ad84 ReactiveSwift`Signal.Observer.send(event=completed, self=0x00000003013cc570) at Signal.Observer.swift:111:4
    frame #10: 0x000000010802dee0 ReactiveSwift`Signal.Core.tryToCommitTermination(self=0x0000000303c1f700) at Signal.swift:231:17
    frame #11: 0x000000010802cf10 ReactiveSwift`Signal.Core.send(event=completed, self=0x0000000303c1f700) at Signal.swift:115:5
    frame #12: 0x000000010802ca2c ReactiveSwift`implicit closure #2 in implicit closure #1 in Signal.Core.init(event=completed, self=0x0000000303c1f700) at Signal.swift:80:36
    frame #13: 0x000000010804ace8 ReactiveSwift`Signal.Observer.sendCompleted(self=0x00000003013ccab0) at Signal.Observer.swift:132:4
    frame #14: 0x000000010803609c ReactiveSwift`implicit closure #2 in implicit closure #1 in closure #1 in Signal.take(self=0x00000003013ccab0) at Signal.swift:1326:52
    frame #15: 0x0000000107fe1a38 ReactiveSwift`AnyDisposable.ActionDisposable.dispose(self=0x00000003013ccc00) at Disposable.swift:87:11
    frame #16: 0x0000000107fe1acc ReactiveSwift`protocol witness for Disposable.dispose() in conformance AnyDisposable.ActionDisposable at <compiler-generated>:0
    frame #17: 0x0000000107fe1e44 ReactiveSwift`AnyDisposable.dispose(self=0x00000003019dd140) at Disposable.swift:121:8
    frame #18: 0x0000000107fe1ef0 ReactiveSwift`protocol witness for Disposable.dispose() in conformance AnyDisposable at <compiler-generated>:0
    frame #19: 0x0000000107fe25e8 ReactiveSwift`CompositeDisposable.dispose(self=0x00000003019dc0a0) at Disposable.swift:166:16
    frame #20: 0x0000000108009234 ReactiveSwift`Lifetime.Token.dispose(self=0x00000003019dc900) at Lifetime.swift:107:16
    frame #21: 0x0000000108009280 ReactiveSwift`Lifetime.Token.deinit(self=0x00000003019dc900) at Lifetime.swift:111:4
    frame #22: 0x00000001080092bc ReactiveSwift`Lifetime.Token.__deallocating_deinit(self=0x00000003019dc900) at Lifetime.swift:0
    frame #23: 0x000000018ff13e44 libswiftCore.dylib`_swift_release_dealloc + 56
    frame #24: 0x000000018ff14f58 libswiftCore.dylib`bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 160
    frame #25: 0x000000018e8553a0 libobjc.A.dylib`_object_remove_associations + 1020
    frame #26: 0x000000018e854f4c libobjc.A.dylib`objc_destructInstance + 96
    frame #27: 0x000000018ff13ef8 libswiftCore.dylib`swift_deallocClassInstance + 120
    frame #28: 0x000000010134ef40 myfirstam`ReactivePublisher.Subscription.__deallocating_deinit(self=0x00000003013d77b0) at ReactivePublisher.swift:0
    frame #29: 0x000000018ff13e44 libswiftCore.dylib`_swift_release_dealloc + 56
    frame #30: 0x000000018ff14f58 libswiftCore.dylib`bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 160
    frame #31: 0x000000018fef1e94 libswiftCore.dylib`swift_arrayDestroy + 196
    frame #32: 0x000000018fbdf1ec libswiftCore.dylib`Swift._ContiguousArrayStorage.__deallocating_deinit + 96
    frame #33: 0x000000018ff13e44 libswiftCore.dylib`_swift_release_dealloc + 56
    frame #34: 0x000000018ff14f58 libswiftCore.dylib`bool swift::RefCounts<swift::RefCountBitsT<(swift::RefCountInlinedness)1>>::doDecrementSlow<(swift::PerformDeinit)1>(swift::RefCountBitsT<(swift::RefCountInlinedness)1>, unsigned int) + 160
    frame #35: 0x000000019a900488 Combine`Combine.AbstractZip.lockedSendCompletion(completion: Combine.Subscribers.Completion<τ_0_1>) -> () + 1464
    frame #36: 0x000000019a869d8c Combine`Combine.AbstractZip.receive(completion: Combine.Subscribers.Completion<τ_0_1>, index: Swift.Int) -> () + 668
    frame #37: 0x000000019a869ae4 Combine`Combine.AbstractZip.Side.receive(completion: Combine.Subscribers.Completion<τ_0_1>) -> () + 24
    frame #38: 0x000000010134f918 myfirstam`closure #1 in ReactivePublisher.Subscription.request(event=failed, subscriber=<unavailable>, self=nil) at ReactivePublisher.swift:49:32
    frame #39: 0x000000010804ad84 ReactiveSwift`Signal.Observer.send(event=failed, self=0x00000003013d7810) at Signal.Observer.swift:111:4
    frame #40: 0x000000010802dee0 ReactiveSwift`Signal.Core.tryToCommitTermination(self=0x0000000300571740) at Signal.swift:231:17
    frame #41: 0x000000010802cf10 ReactiveSwift`Signal.Core.send(event=failed, self=0x0000000300571740) at Signal.swift:115:5
    frame #42: 0x000000010802ca2c ReactiveSwift`implicit closure #2 in implicit closure #1 in Signal.Core.init(event=failed, self=0x0000000300571740) at Signal.swift:80:36
    frame #43: 0x000000010804ad84 ReactiveSwift`Signal.Observer.send(event=failed, self=0x00000003013d78a0) at Signal.Observer.swift:111:4
    frame #44: 0x000000010802dee0 ReactiveSwift`Signal.Core.tryToCommitTermination(self=0x0000000303c14fa0) at Signal.swift:231:17
    frame #45: 0x000000010802cf10 ReactiveSwift`Signal.Core.send(event=failed, self=0x0000000303c14fa0) at Signal.swift:115:5
    frame #46: 0x000000010802ca2c ReactiveSwift`implicit closure #2 in implicit closure #1 in Signal.Core.init(event=failed, self=0x0000000303c14fa0) at Signal.swift:80:36
    frame #47: 0x000000010804ad84 ReactiveSwift`Signal.Observer.send(event=failed, self=0x00000003013d7c60) at Signal.Observer.swift:111:4
    frame #48: 0x000000010802dee0 ReactiveSwift`Signal.Core.tryToCommitTermination(self=0x0000000303c14f50) at Signal.swift:231:17
    frame #49: 0x000000010802cf10 ReactiveSwift`Signal.Core.send(event=failed, self=0x0000000303c14f50) at Signal.swift:115:5
    frame #50: 0x000000010802ca2c ReactiveSwift`implicit closure #2 in implicit closure #1 in Signal.Core.init(event=failed, self=0x0000000303c14f50) at Signal.swift:80:36
    frame #51: 0x000000010804b480 ReactiveSwift`Signal.Observer.send(error=0x0000000303b64780, self=0x00000003013d7a80) at Signal.Observer.swift:127:4
    frame #52: 0x0000000101772634 myfirstam`RemoteRequest.handleError(error=0x0000000303b649c0, response=Alamofire.DataResponse<τ_0_0, Alamofire.AFError> @ 0x000000016f099bf0, self=myfirstam.RemoteRequest<myfirstam.EagleProDocumentModel> @ 0x0000000302dd4890) at RemoteRequest.swift:128:22
    frame #53: 0x0000000101771b9c myfirstam`closure #1 in RemoteRequest.start(response=Alamofire.DataResponse<τ_0_0, Alamofire.AFError> @ 0x00000003024d0ac0, self=myfirstam.RemoteRequest<myfirstam.EagleProDocumentModel> @ 0x0000000302dd4890) at RemoteRequest.swift:48:17
    frame #54: 0x0000000106a1ec24 Alamofire`closure #1 in closure #1 in closure #3 in closure #1 in DataRequest._response<τ_0_0>(completionHandler=0x101772004, response=Alamofire.DataResponse<τ_0_0.SerializedObject, Alamofire.AFError> @ 0x00000003024d0ac0) at DataRequest.swift:268:72
    frame #55: 0x00000001069eba64 Alamofire`thunk for @escaping @callee_guaranteed @Sendable () -> () at <compiler-generated>:0
    frame #56: 0x0000000107808a30 libdispatch.dylib`_dispatch_call_block_and_release + 32
    frame #57: 0x000000010780a71c libdispatch.dylib`_dispatch_client_callout + 20
    frame #58: 0x000000010781ade8 libdispatch.dylib`_dispatch_main_queue_drain + 1076
    frame #59: 0x000000010781a9a4 libdispatch.dylib`_dispatch_main_queue_callback_4CF + 44
    frame #60: 0x0000000191537f64 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 16
    frame #61: 0x0000000191535188 CoreFoundation`__CFRunLoopRun + 1996
    frame #62: 0x00000001915345b8 CoreFoundation`CFRunLoopRunSpecific + 572
    frame #63: 0x00000001dcfca1c4 GraphicsServices`GSEventRunModal + 164
    frame #64: 0x000000019408a5f0 UIKitCore`-[UIApplication _run] + 816
    frame #65: 0x000000019413910c UIKitCore`UIApplicationMain + 340
    frame #66: 0x0000000101065dbc myfirstam`main at main.swift:6:1
    frame #67: 0x00000001b6d07d34 dyld`start + 2724

As in iOS 17, I would expect calling subscriber.receive(completion: .finished) to not result in a crash. Any ideas?


Solution

  • Something about a .failure case was also triggering a .completed case, but my subscriber object was nil when the .completed case would trigger. So to fix the issue, I had to move the guard line into the .start block:

    diff