0

I have a publisher, pipelinePublisher, which runs a combine pipeline of various operations, some of which send a state update to a statePublisher passed in as an argument. pipelinePublisher gets removed on completion of its Combine pipeline:

func myFunction(_ request: MyRequest) -> PassthroughSubject<State, Never> {
    let statePublisher = PassthroughSubject<State, Never>()
    let presentationSubject = CurrentValueSubject<MyRequest, Error>(request)

    var pipelinePublisher: AnyCancellable!

    pipelinePublisher = presentationSubject
      .eraseToAnyPublisher()
      .checkSomething(returningStateTo: statePublisher)
      // a few more operators here...
      .sink(
        receiveCompletion: { [weak self] _ in
          self?.cancellables.remove(pipelinePublisher) // Crash happens here
        },
        receiveValue: { _ in }
      )

    pipelinePublisher.store(in: &cancellables)

    return statePublisher
      .receive(on: RunLoop.main)
      .eraseToAnyPublisher()
  }

However, very occasionally when I call the function multiple times in very quick succession, the app crashes on the line self?.cancellables.remove(pipelinePublisher). This usually brings up one of two possible stack traces. This first is this:

2022-12-21 15:24:51.926131+0000 MyApp[23082:12933690] -[_NSCoreDataTaggedObjectID member:]: unrecognized selector sent to instance 0x8000000000000000
2022-12-21 15:24:51.931941+0000 MyApp[23082:12933690] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_NSCoreDataTaggedObjectID member:]: unrecognized selector sent to instance 0x8000000000000000'
*** First throw call stack:
(
    0   CoreFoundation                      0x000000018040e7c8 __exceptionPreprocess + 172
    1   libobjc.A.dylib                     0x0000000180051144 objc_exception_throw + 56
    2   CoreFoundation                      0x000000018041d47c +[NSObject(NSObject) instanceMethodSignatureForSelector:] + 0
    3   CoreFoundation                      0x00000001804126c8 ___forwarding___ + 1308
    4   CoreFoundation                      0x0000000180414b4c _CF_forwarding_prep_0 + 92
    5   libswiftCore.dylib                  0x000000018be6ee68 $sSh8_VariantV6removeyxSgxF + 160
    6   MyApp               0x00000001026c6080 $s12MyApp0A0C17myFunctiony7Combine12AnyPublisherVyAA12StateOs5NeverOGAA19MyRequestVFyAE11SubscribersO10CompletionOy_s5Error_pGcfU_ + 440
    7   Combine                             0x000000019baa2a70 $s7Combine11SubscribersO4SinkC7receive10completionyAC10CompletionOy_q_G_tF + 364
    8   Combine                             0x000000019baa2f28 $s7Combine11SubscribersO4SinkCy_xq_GAA10SubscriberA2aGP7receive10completionyAC10CompletionOy_7FailureQzG_tFTW + 20
    9   Combine                             0x000000019bb541cc $s7Combine10PublishersO7FlatMapV5Outer33_E91C3F00A6DFAAFEA2009FAF507AE039LLC7receive10completionyAA11SubscribersO10CompletionOy_7FailureQzG_tF + 1516
    10  Combine                             0x000000019bb55328 $s7Combine10PublishersO7FlatMapV5Outer33_E91C3F00A6DFAAFEA2009FAF507AE039LLCy_xq__qd__GAA10SubscriberA2aJP7receive10completionyAA11SubscribersO10CompletionOy_7FailureQzG_tFTW + 20
    11  Combine                             0x000000019bb53474 $s7Combine10PublishersO7FlatMapV5Outer33_E91C3F00A6DFAAFEA2009FAF507AE039LLC12receiveInner10completion_yAA11SubscribersO10CompletionOy_7FailureQzG_SitF + 1668
    12  Combine                             0x000000019bb52de4 $s7Combine10PublishersO7FlatMapV5Outer33_E91C3F00A6DFAAFEA2009FAF507AE039LLC4SideV7receive10completionyAA11SubscribersO10CompletionOy_7FailureQzG_tF + 20
    13  Combine                             0x000000019bac45ec $s7Combine6FutureC7Conduit33_3AE68DE9BADC00342FC052FEBC7D3BA6LLC7fulfillyys6ResultOyxq_GF + 1056
    14  Combine                             0x000000019bac4960 $s7Combine6FutureC7Conduit33_3AE68DE9BADC00342FC052FEBC7D3BA6LLC6finish10completionyAA11SubscribersO10CompletionOy_q_G_tF + 336
    15  Combine                             0x000000019bac2de4 $s7Combine6FutureC7promise33_3AE68DE9BADC00342FC052FEBC7D3BA6LLyys6ResultOyxq_GFyAA11ConduitBaseCyxq_GXEfU0_ + 156
    16  Combine                             0x000000019bac6b28 $s7Combine6FutureC7promise33_3AE68DE9BADC00342FC052FEBC7D3BA6LLyys6ResultOyxq_GFyAA11ConduitBaseCyxq_GXEfU0_TA + 16
    17  Combine                             0x000000019bae5140 $s7Combine11ConduitListO7forEachyyyAA0B4BaseCyxq_GKXEKF + 212
    18  Combine                             0x000000019bac2bfc $s7Combine6FutureC7promise33_3AE68DE9BADC00342FC052FEBC7D3BA6LLyys6ResultOyxq_GF + 716
    19  Combine                             0x000000019bac6b08 $s7Combine6FutureCyACyxq_Gyys6ResultOyxq_GcccfcyAGcfU_TA + 20
    20  MyApp               0x0000000102541dd8 $s7Combine6FutureC12MyApps5Error_pRs_rlE9operationACyxsAE_pGxyYaKc_tcfcyys6ResultOyxsAE_pGccfU_yyYaYbcfU_TY2_ + 212
    21  MyApp               0x0000000102542705 $s7Combine6FutureC12MyApps5Error_pRs_rlE9operationACyxsAE_pGxyYaKc_tcfcyys6ResultOyxsAE_pGccfU_yyYaYbcfU_TATQ0_ + 1
    22  MyApp               0x000000010242f1a1 $sxIeghHr_xs5Error_pIegHrzo_s8SendableRzs5NeverORs_r0_lTRTQ0_ + 1
    23  MyApp               0x000000010242f749 $sxIeghHr_xs5Error_pIegHrzo_s8SendableRzs5NeverORs_r0_lTRTA.24TQ0_ + 1
    24  libswift_Concurrency.dylib          0x00000001b03bedcd _ZL23completeTaskWithClosurePN5swift12AsyncContextEPNS_10SwiftErrorE + 1
)
libc++abi: terminating with uncaught exception of type NSException

The second is in the same place but with the error: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber member:]: unrecognized selector sent to instance 0x8000000000000000'

What is causing this? I've tried making pipelinePublisher optional and having a check that it exists before it gets removed but it does actually exist and still crashes. I can't figure this out, please help!

EDIT

I am calling myFunction(_:) by using the method foo which gets a publisher.

static func publisher(forParam: String) -> AnyPublisher<State, Never> {
    return Future {
      // Do some stuff here
      return objects
    }
    .flatMap { objects in
      let request = MyRequest(objects)
      return shared.myFunction(request)
    }
    .eraseToAnyPublisher()
  }

static func foo(
    param: String,
    handler: ((State) -> Void)? = nil
  ) {
    var cancellable: AnyCancellable!
    cancellable = publisher(forParam: param)
    .sink(
      receiveCompletion: { _ in
        self.shared.fooItems.cancellables.remove(cancellable) // sometimes crashes here too with the exact same crash!
      }, receiveValue: { state in
        handler?(state)
      }
    )

    cancellable.store(in: &shared.fooItems.cancellables)
  }

A few things to note:

  • Foo has to use completion block as part of an API.
  • publisher(forParam:) and therefore myFunction(_:) are only called from foo. However foo is called from many places.
  • myFunction’s pipelinePublisher cancels by an error that’s thrown at the same time as sending a state and a completion through to foo.
  • state can be sent to foo without a follow up completion too.

As you can see above, foo also sometimes gets the same error when removing the cancellable.

Tometoyou
  • 7,792
  • 12
  • 62
  • 108
  • Have you tried storing the `AnyCancellable` in its own variable rather than in a `Set`? – Dávid Pásztor Dec 21 '22 at 15:46
  • What if you start the pipeline by receiving `presentationSubject` on the main queue? Also I would recommend changing your RunLoop usage to `.receiveOn(DispatchQueue.main)` as well. – matt Dec 21 '22 at 15:46
  • @DávidPásztor I can't do that unfortunately as this function can be called multiple times and I don't want the variable to be overwritten every time it's called. – Tometoyou Dec 21 '22 at 15:48
  • @matt In my first pipeline operator I actually subscribed on `DispatchQueue.global(qos: .userInitiated)`. Because there are operations I don't want to perform on the main thread within that. Also thanks I changed to DispatchQueue.main from the runloop, but that didn't affect it – Tometoyou Dec 21 '22 at 15:56
  • Tried changing to receive on main now instead but still happens. It's weird, I've really gotta call the function a crazy number of times by mashing a button that fires it, only very occasionally happens. Some kind of race but don't know what – Tometoyou Dec 21 '22 at 16:01
  • You could debounce so that can't happen, I suppose... – matt Dec 21 '22 at 16:03
  • @matt Well the buttons not directly connected to the publisher because this function is called from many places. It just fires this function which creates a new publisher every time it's called so debouncing wouldn't do anything because it's a new publisher every time – Tometoyou Dec 21 '22 at 16:08
  • Well, isn't that part of the problem? Declaring a subject as a local variable inside a function is Really Weird. – matt Dec 21 '22 at 16:21
  • I'm using the subject as a starting point for a pipeline that takes the subjects value and manipulates it. If it encounters errors along the way, the pipeline throws and cancels the pipeline. If you know of a better way then I'm happy to hear it. – Tometoyou Dec 21 '22 at 16:32
  • Maybe this is a long shot, but there is a window of opportunity between the time the pipeline is created and the time the subscription is assigned to the `pipelinePublisher` variable. Perhaps the pipeline runs and tries to complete before `pipelinePubisher` receives a value. Maybe you can use `makeConnectable` to be sure the pipeline is built and assigned to the variable then immediately use `connect` to execute the pipeline. – Scott Thompson Dec 21 '22 at 22:48
  • @ScottThompson that’s what I was thinking too. I had tried using makeConnectable but I’ll try again. The weird thing is `pipelinePublisher` isn’t nil but it’s as if it’s not fully initialised, if that can be a thing – Tometoyou Dec 22 '22 at 00:15
  • 1
    Why are you removing the cancellable? Especially, before it has completed? – Daniel T. Dec 23 '22 at 19:25
  • @DanielT. How do you mean before its completed? I'm removing it in the `receiveCompletion` block – Tometoyou Dec 23 '22 at 23:12
  • Exactly. You are removing it *before* the receiveCompletion has returned. It's in the middle of executing its block. And why remove it at all? It's completed so there's no point. – Daniel T. Dec 24 '22 at 02:34
  • @DanielT. receive completion is the final piece of info emitted from a publisher though. There could be many of these finished publishers, which could eat up memory for no reason. This stack overflow answer suggested it: https://stackoverflow.com/a/66268242/362840 – Tometoyou Dec 24 '22 at 12:34
  • @Tometoyou yes it's the final piece of info emitted, but you are canceling the chain when it is in the middle of emitting it. I think the rationale in that answer is unfounded. If you actually saw problems from storing cancellable then maybe clearing the Set would be valuable, but there's no problem keeping them around. The Set will be deinited when the object goes out of scope anyway so no worries about a huge number of them hanging around. – Daniel T. Dec 24 '22 at 21:05
  • Of course it would be better if you weren't mixing non-reactive and reactive code like this. For example if whatever event that calls `myFunction(_:)` was itself a Publisher, then you would only need a single cancellable for all requests. – Daniel T. Dec 24 '22 at 21:05
  • The Set is actually stored in a shared variable that’s around forever and it’s not possible to change that. I haven’t actually noticed issues with the memory btw, I’m just trying to reduce memory usage as much as possible. I’ve updated my question with more context of how it’s being called which might make it more understandable? It sounds like there could be some simplification done here – Tometoyou Dec 24 '22 at 23:16
  • I agree with Daniel T. Don't remove the cancellable from its set at all. There's no need. And it's dangerous, because Set is not thread safe. I recommend reading this answer (as well as mine in the same question): https://stackoverflow.com/a/62095636/341994 – matt Dec 24 '22 at 23:53
  • @matt Thanks, that's actually solved it for me. Have changed to using `Subscribers.Sink` (because I don't actually need to cancel) and letting swift clean everything up for me. Cannot get this crash anymore! Didn't realise Set was not thread safe either! – Tometoyou Jan 03 '23 at 11:10
  • If the problem is solved, please delete the question (or create an Answer for it). Don't leave it hanging open. – matt Jan 04 '23 at 03:30

1 Answers1

0

With help from people in the comments of my question, it looks like I had a race condition possibly caused by threads interacting with each other. I was removing from a Set, but Sets aren't thread safe. I solved this by changing the code to use Subscribers.Sink:

func myFunction(_ request: MyRequest) -> PassthroughSubject<State, Never> {
    let statePublisher = PassthroughSubject<State, Never>()
    let presentationSubject = CurrentValueSubject<MyRequest, Error>(request)

    var pipelinePublisher: AnyCancellable!

    pipelinePublisher = presentationSubject
      .eraseToAnyPublisher()
      .checkSomething(returningStateTo: statePublisher)
      // a few more operators here...
      .subscribe(Subscribers.Sink(
        receiveCompletion: { _ in },
        receiveValue: { _ in }
      ))

    pipelinePublisher.store(in: &cancellables)

    return statePublisher
      .receive(on: RunLoop.main)
      .eraseToAnyPublisher()
  }

This fixed the problem. Combine handles the clean up of the subscription and the subscriber when the pipeline returns.

This was taken from the answer given here: With Combine, how to deallocate the Subscription after a network request

Tometoyou
  • 7,792
  • 12
  • 62
  • 108