Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Potential bug with iOS 14 beta 3 - crash when cancelling an effect #232

Closed
lukeredpath opened this issue Jul 23, 2020 · 22 comments · Fixed by #244
Closed

Potential bug with iOS 14 beta 3 - crash when cancelling an effect #232

lukeredpath opened this issue Jul 23, 2020 · 22 comments · Fixed by #244

Comments

@lukeredpath
Copy link
Contributor

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)
@mbrandonw
Copy link
Member

Dang, this is a bummer. If you find a way to get a repro we'd love to see it. Maybe we can file a bug with Apple.

@jakobmygind
Copy link

@mbrandonw I am getting this in the Case studies as well in the Elm-like subscriptions: Start timer, stop timer, observe crash 🤔
Also got this in Long living effects by just opening the screen

@mbrandonw
Copy link
Member

Ah thanks @jakobmygind! I can definitely reproduce now, and can even reproduce in our effect cancellation tests. It seems that the newest version of Swift is more strict with simultaneous access to a value, in this case a cancellable. We are looking into it now.

Also the bug of immediately crashing in the long living effects case study is due a SwiftUI bug where onDisappear is being called immediately on that view 😕 I've filed an FB for it: https://gist.github.com/mbrandonw/2b1eefef3d939074fa040a7a0c7ab377

@mbrandonw
Copy link
Member

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.

@damirstuhec
Copy link

Any known workarounds so far? Me, and my users, are experiencing this too.

@elkraneo
Copy link
Contributor

elkraneo commented Jul 28, 2020

With the same beta target, I see a similar crash when using .debounce. It can be reproduced with the "Search" example as is currently on fe1a4c2 , start searching and will crash as soon the Effect gets to the receiveCompletion

@mbrandonw
Copy link
Member

Just to be sure @damirstuhec and @elkraneo, this is only happening in the newest Xcode beta right? Not in Xcode 11?

@damirstuhec
Copy link

@mbrandonw for me, it's happening on devices running iOS 14 beta 3, and yes, Xcode beta.

@elkraneo
Copy link
Contributor

elkraneo commented Jul 28, 2020

@mbrandonw yes, full beta environment (macOS Big Sur and Xcode with the latest beta releases).
On my case only bringing more data to the issue, no major impact...

@lukeredpath
Copy link
Contributor Author

Just to confirm I have only seen this on Xcode 12/iOS 14 beta 3. No issues in previous betas or iOS 13/Xcode 11.

@abdelmajidrajad
Copy link

For me, It's happening on iOS 14 beta 3 on both Xcode 12 Beta & Xcode 11.6 .

@mbrandonw
Copy link
Member

For me, It's happening on iOS 14 beta 3 on both Xcode 12 Beta & Xcode 11.6 .

I wasn't able to reproduce this bug on 11.6. Are you sure it's happening on that version of Xcode?

@damirstuhec
Copy link

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

@ohitsdaniel
Copy link
Contributor

ohitsdaniel commented Jul 30, 2020

The EffectCancellationTests in the SCA package fail with the error @lukeredpath mentioned, if anyone needs a way to reproduce this and wants to give fixing a try. 👀

@abdelmajidrajad
Copy link

@mbrandonw, Yes I was able to reproduce it, check it out here .
TimerCancellation.zip

@mycroftcanner
Copy link

@mycroftcanner
Copy link

mycroftcanner commented Jul 31, 2020

https://swift.org/blog/swift-5-exclusivity/

The Swift 5 release enables runtime checking of “Exclusive Access to Memory” by default in Release builds, further enhancing Swift’s capabilities as a safe language. In Swift 4, these runtime checks were only enabled in Debug builds. In this post, I’ll first explain what this change means for Swift developers before delving into why it is essential to Swift’s strategy for safety and performance.

Compile-time (static) diagnostics catch many common exclusivity violations, but run-time (dynamic) diagnostics are also required to catch violations involving escaping closures, properties of class types, static properties, and global variables. Swift 4.0 provided both compile-time and run-time enforcement, but run-time enforcement was only enabled in Debug builds.

A quick and dirty workaround is to set Exclusive Access to Memory to Compile-time Enforcement Only.

image

The corresponding swiftc compiler flags are -enforce-exclusivity=unchecked and -enforce-exclusivity=none.

Swift 5 fixes the remaining holes in the language model and fully enforces that model1. Since run-time exclusivity enforcement is now enabled by default in Release builds, some Swift programs that previously appeared well-behaved, but weren’t fully tested in Debug mode, could be affected.

@lukeredpath
Copy link
Contributor Author

@mycroftcanner why would this have only become a problem in the latest beta rather than in previous betas or earlier Swift 5 releases?

Also, I'm seeing this in debug builds, I haven't even tried a release build.

@stephencelis
Copy link
Member

We also have only seen this memory exclusivity issue crop up in iOS and not macOS. The Combine stack is also different across platforms. We're hoping that this will be fixed in the next beta, but the more Feedback filed the better...

@stephencelis
Copy link
Member

Just pushed this here: #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.

@lukeredpath
Copy link
Contributor Author

@stephencelis thanks for finding a workaround to this, I'm going to see if the previously failing release starts working again on the new Xcode b4.

@lukeredpath
Copy link
Contributor Author

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. 🤞🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants