swift-composable-architecture: Potential bug with iOS 14 beta 3 - crash when cancelling an effect

Describe the bug I have an app that has a timer which can be paused and unpaused. Starting the timer uses a cancellable effect, pausing the timer cancels the effect.

This was working fine in previous betas, but it now crashes with the following:

2020-07-23 18:08:59.309105+0100 Poker Clock[15652:1557176] libMobileGestalt MobileGestaltCache.c:38: No persisted cache on this platform.
Simultaneous accesses to 0x6000028a37b0, but modification requires exclusive access.
Previous access (a modification) started at Combine`AnyCancellable.cancel() + 43 (0x104c023bb).
Current access (a modification) started at:
0    libswiftCore.dylib                 0x00000001083fde50 swift_beginAccess + 568
1    Combine                            0x0000000104c02390 AnyCancellable.cancel() + 43
2    PokerTimerCore                     0x000000010421b0b0 closure #2 in closure #1 in Effect.cancellable(id:cancelInFlight:) + 292
3    Combine                            0x0000000104c99460 Publishers.HandleEvents.Inner.receive(completion:) + 85
4    Combine                            0x0000000104c99640 protocol witness for Subscriber.receive(completion:) in conformance Publishers.HandleEvents<A>.Inner<A1> + 16
5    Combine                            0x0000000104c22960 PassthroughSubject.Conduit.finish(completion:) + 506
6    Combine                            0x0000000104c23b40 partial apply for closure #1 in PassthroughSubject.send(completion:) + 23
7    Combine                            0x0000000104c58f90 ConduitList.forEach(_:) + 196
8    Combine                            0x0000000104c22390 PassthroughSubject.send(completion:) + 395
9    PokerTimerCore                     0x000000010421a910 closure #1 in closure #1 in closure #1 in Effect.cancellable(id:cancelInFlight:) + 353
10   PokerTimerCore                     0x000000010421b010 thunk for @callee_guaranteed () -> () + 12
11   PokerTimerCore                     0x000000010421b9f0 partial apply for thunk for @callee_guaranteed () -> () + 17
12   PokerTimerCore                     0x00000001042357e0 NSRecursiveLock.sync<A>(work:) + 106
13   PokerTimerCore                     0x000000010421a750 closure #1 in closure #1 in Effect.cancellable(id:cancelInFlight:) + 363
14   Combine                            0x0000000104c01f20 AnyCancellable.Storage.cancel(_:) + 119
15   Combine                            0x0000000104c02390 AnyCancellable.cancel() + 51
16   PokerTimerCore                     0x000000010421b8b0 closure #1 in closure #1 in closure #1 in static Effect.cancel(id:) + 44
17   PokerTimerCore                     0x000000010421b8f0 thunk for @callee_guaranteed (@guaranteed AnyCancellable) -> (@error @owned Error) + 18
18   PokerTimerCore                     0x000000010421bb80 partial apply for thunk for @callee_guaranteed (@guaranteed AnyCancellable) -> (@error @owned Error) + 20
19   libswiftCore.dylib                 0x0000000108193e50 Sequence.forEach(_:) + 377
20   PokerTimerCore                     0x000000010421b670 closure #1 in closure #1 in static Effect.cancel(id:) + 464
21   PokerTimerCore                     0x000000010421b010 thunk for @callee_guaranteed () -> () + 12
22   PokerTimerCore                     0x000000010421bb60 thunk for @callee_guaranteed () -> ()partial apply + 17
23   PokerTimerCore                     0x00000001042357e0 NSRecursiveLock.sync<A>(work:) + 106
24   PokerTimerCore                     0x000000010421b590 closure #1 in static Effect.cancel(id:) + 171
25   PokerTimerCore                     0x0000000104218280 closure #1 in static Effect.fireAndForget(_:) + 96
26   PokerTimerCore                     0x0000000104218350 partial apply for closure #1 in static Effect.fireAndForget(_:) + 52
27   Combine                            0x0000000104c14bf0 Deferred.receive<A>(subscriber:) + 75
28   Combine                            0x0000000104ca35e0 PublisherBox.receive<A>(subscriber:) + 33
29   Combine                            0x0000000104ca37f0 AnyPublisher.receive<A>(subscriber:) + 22
30   Combine                            0x0000000104c3e6b0 Publisher.subscribe<A>(_:) + 1019
31   PokerTimerCore                     0x0000000104216250 Effect.receive<A>(subscriber:) + 231
32   PokerTimerCore                     0x0000000104218510 protocol witness for Publisher.receive<A>(subscriber:) in conformance Effect<A, B> + 47
33   Combine                            0x0000000104c3e6b0 Publisher.subscribe<A>(_:) + 1019
34   Combine                            0x0000000104ca3bc0 Publishers.Map.receive<A>(subscriber:) + 346
35   Combine                            0x0000000104ca35e0 PublisherBox.receive<A>(subscriber:) + 33
36   Combine                            0x0000000104ca37f0 AnyPublisher.receive<A>(subscriber:) + 22
37   Combine                            0x0000000104c3e6b0 Publisher.subscribe<A>(_:) + 1019
38   PokerTimerCore                     0x0000000104216250 Effect.receive<A>(subscriber:) + 231
39   PokerTimerCore                     0x0000000104218510 protocol witness for Publisher.receive<A>(subscriber:) in conformance Effect<A, B> + 47
40   Combine                            0x0000000104c3e6b0 Publisher.subscribe<A>(_:) + 1019
41   Combine                            0x0000000104c66820 Publishers.MergeMany.receive<A>(subscriber:) + 1274
42   Combine                            0x0000000104ca35e0 PublisherBox.receive<A>(subscriber:) + 33
43   Combine                            0x0000000104ca37f0 AnyPublisher.receive<A>(subscriber:) + 22
44   Combine                            0x0000000104c3e6b0 Publisher.subscribe<A>(_:) + 1019
45   PokerTimerCore                     0x0000000104216250 Effect.receive<A>(subscriber:) + 231
46   PokerTimerCore                     0x0000000104218510 protocol witness for Publisher.receive<A>(subscriber:) in conformance Effect<A, B> + 47
47   Combine                            0x0000000104c3e6b0 Publisher.subscribe<A>(_:) + 1019
48   Combine                            0x0000000104c15d70 Publisher.sink(receiveCompletion:receiveValue:) + 431
49   PokerTimerCore                     0x0000000104240560 Store.send(_:) + 2579
50   PokerTimerCore                     0x00000001042402d0 closure #1 in Store.scope<A, B>(state:action:) + 419
51   PokerTimerCore                     0x0000000104241250 partial apply for closure #1 in Store.scope<A, B>(state:action:) + 73
52   PokerTimerCore                     0x0000000104240560 Store.send(_:) + 1396
53   PokerTimerCore                     0x0000000104269830 implicit closure #2 in implicit closure #1 in ViewStore.init(_:removeDuplicates:) + 78
54   PokerTimerCore                     0x000000010426a4e0 ViewStore.send(_:) + 127
55   Poker Clock                        0x0000000103e07f20 closure #4 in closure #1 in closure #1 in LevelControls.body.getter + 113
56   SwiftUI                            0x0000000105283d80 partial apply for implicit closure #2 in implicit closure #1 in WrappedButtonStyle.Body.body.getter + 17
57   SwiftUI                            0x000000010553dfe0 closure #1 in PressableGestureCallbacks.dispatch(phase:state:) + 32
58   SwiftUI                            0x00000001052f9ba0 thunk for @escaping @callee_guaranteed () -> () + 12
59   SwiftUI                            0x000000010517e8f0 partial apply for thunk for @escaping @callee_guaranteed () -> () + 17
60   SwiftUI                            0x000000010519dcd0 thunk for @escaping @callee_guaranteed () -> ()partial apply + 9
61   SwiftUI                            0x00000001052f9bc0 thunk for @escaping @callee_guaranteed () -> (@out ()) + 12
62   SwiftUI                            0x00000001052f9ba0 thunk for @escaping @callee_guaranteed () -> () + 12
63   SwiftUI                            0x00000001052ec780 partial apply for thunk for @escaping @callee_guaranteed () -> () + 17
64   SwiftUI                            0x00000001052ebe90 static Update.end() + 436
65   SwiftUI                            0x000000010532a970 EventBindingManager.send(_:) + 301
66   SwiftUI                            0x0000000105764290 specialized EventBindingBridge.send(_:source:) + 2060
67   SwiftUI                            0x0000000105762740 UIKitGestureRecognizer.send(touches:event:phase:) + 66
68   SwiftUI                            0x0000000105763560 @objc UIKitGestureRecognizer.touchesBegan(_:with:) + 131
69   SwiftUI                            0x0000000105762830 @objc UIKitGestureRecognizer.touchesEnded(_:with:) + 40
70   UIKitCore                          0x000000010ff8621c -[UIGestureRecognizer _componentsEnded:withEvent:] + 217
71   UIKitCore                          0x00000001104ccec0 -[UITouchesEvent _sendEventToGestureRecognizer:] + 674
72   UIKitCore                          0x000000010ff7a6b5 __47-[UIGestureEnvironment _updateForEvent:window:]_block_invoke + 70
73   UIKitCore                          0x000000010ff7a197 -[UIGestureEnvironment _updateForEvent:window:] + 489
74   UIKitCore                          0x000000011047e928 -[UIWindow sendEvent:] + 4752
75   UIKitCore                          0x0000000110459645 -[UIApplication sendEvent:] + 408
76   UIKitCore                          0x00000001104e5e21 __processEventQueue + 15007
77   UIKitCore                          0x00000001104e032e __eventFetcherSourceCallback + 106
78   CoreFoundation                     0x0000000108f63af3 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
79   CoreFoundation                     0x0000000108f639a6 __CFRunLoopDoSource0 + 157
80   CoreFoundation                     0x0000000108f630a9 __CFRunLoopDoSources0 + 222
81   CoreFoundation                     0x0000000108f5d8f6 __CFRunLoopRun + 882
82   CoreFoundation                     0x0000000108f5d328 CFRunLoopRunSpecific + 538
83   GraphicsServices                   0x000000010b884d28 GSEventRunModal + 139
84   UIKitCore                          0x000000011043adbe -[UIApplication _run] + 912
85   UIKitCore                          0x000000011044014c UIApplicationMain + 101
86   Poker Clock                        0x0000000103dec530 main + 75
87   libdyld.dylib                      0x000000010a5aa410 start + 1
(lldb) 

To Reproduce

I don’t have a small reproducer yet, but here’s the relevant timer code from my app:

extension Effect {
    static func gameClock(environment: GameEnvironment) -> Effect<GameAction, Never> {
        Effect<GameAction, Never>.merge(
            foregroundClockEffect(
                mainQueue: environment.mainQueue
            ),
            backgroundClockEffect(
                currentDate: environment.currentDate,
                scheduler: environment.mainQueue,
                backgroundNotificationName: environment.backgroundNotificationName,
                foregroundNotificationName: environment.foregroundNotificationName
            )
        ).cancellable(id: GameClockId(), cancelInFlight: true)
    }
}

func foregroundClockEffect(mainQueue: AnySchedulerOf<DispatchQueue>) -> Effect<GameAction, Never> {
    mainQueue.timerPublisher(every: 1, tolerance: .zero)
        .autoconnect()
        .eraseToEffect()
        .map { _ in GameAction.gameClockTicked(by: 1) }
}

func backgroundClockEffect(
    currentDate: @escaping () -> Date,
    scheduler: AnySchedulerOf<DispatchQueue>,
    backgroundNotificationName: Notification.Name,
    foregroundNotificationName: Notification.Name) -> Effect<GameAction, Never>
{
    let backgroundDatePublisher = NotificationCenter.default
        .publisher(for: backgroundNotificationName)
        .map { _ in currentDate() }
    
    let foregroundDatePublisher = NotificationCenter.default
        .publisher(for: foregroundNotificationName)
        .drop(untilOutputFrom: backgroundDatePublisher)
        .map { _ in currentDate() }
    
    let backgroundTimePublisher = backgroundDatePublisher
        .zip(foregroundDatePublisher)
        .map { backgroundDate, foregroundDate in
            foregroundDate.timeIntervalSince(backgroundDate).rounded()
        }
    
    return backgroundTimePublisher
        .eraseToEffect()
        .map { GameAction.gameClockTicked(by: $0) }
}

The background is unlikely to be relevant as this is happening in the foreground but its part of the same overall effect.

The action handler in the reducer is straightforward and is triggered by tapping the play/pause button:

case .togglePaused:
        state.isPaused.toggle()
        
        if state.isPaused { return Effect.cancel(id: GameClockId()) }
        
        return .gameClock(environment: environment)

Expected behavior I expect the timer to be cancelled and the app not to crash, as before.

Environment

  • Xcode 12beta3
  • Swift 4.3
  • iOS 14beta3 (simulator)

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 6
  • Comments: 22 (14 by maintainers)

Most upvoted comments

Just pushed this here: https://github.com/pointfreeco/swift-composable-architecture/pull/244

Will probably merge soon to unblock folks that want to bring in the main branch, but any feedback would be helpful! Also, if anyone is running on Big Sur, if they can run the test suite and see if it passes, that’d be helpful.

Weirdly the TCA test suite passes on macOS and crashes on iOS 🤔 Not sure how that is possible.

Also, the stack trace of where it crashes on iOS and slightly different from that same point on macOS. Very strange.

With more and more users using iOS 14 betas, this is becoming a quite pressing issue. 😞 I personally haven’t found a workaround yet.

Good news! The bug appears to have been resolved in Xcode 12 beta 4. I can pause and unpause my in-game timer successfully, so you may be able to revert #244 after a bit more testing. 🤞🏻