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