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

Deadlock on main thread when loading dependencies and @Shared values #3355

Closed
1 of 3 tasks
zachwaugh opened this issue Sep 5, 2024 · 1 comment · Fixed by #3356
Closed
1 of 3 tasks

Deadlock on main thread when loading dependencies and @Shared values #3355

zachwaugh opened this issue Sep 5, 2024 · 1 comment · Fixed by #3356
Labels
bug Something isn't working due to a bug in the library.

Comments

@zachwaugh
Copy link

Description

I noticed my app getting into deadlock on the main thread when using @Shared. I think this is probably the same underlying problem and root cause as my other issue (#3311), but this one I managed to make a repro for.

Screenshot 2024-09-05 at 12 57 51 PM image

Checklist

  • I have determined whether this bug is also reproducible in a vanilla SwiftUI project.
  • If possible, I've reproduced the issue using the main branch of this package.
  • This issue hasn't been addressed in an existing GitHub issue or discussion.

Expected behavior

No response

Actual behavior

No response

Steps to reproduce

Here's a repro similar to my app that I was able to finally reduce into something simpler, but still approximating what happens in my app. There's a lot of things at play and changing some subtle things will cause it not to deadlock, but I was able to reliably deadlock every time with this. Just note if you change something and it works, you may need to delete the app and do a fresh install to trigger it again. I didn't have time at the moment to dig further, but I suspect at least some part of it is due to UserDefaults initializing the first time?

Run the app in a simulator, and you'll see Database init() start log and nothing else and the app will be locked. Hit the pause execution button and you can see where its locked. I think as @mbrandonw suggested in the other issue that changing that mainActorNow to DispatchQueue.main.async would fix it and I'll try that later when I have some more time.

import ComposableArchitecture
import SwiftUI

@main
struct TCATestApp: App {
    var body: some Scene {
        WindowGroup {
            DeadlockView(store: Store(initialState: Deadlock.State(), reducer: {
                Deadlock()
            }))
        }
    }
}

struct DeadlockView: View {
    @Bindable var store: StoreOf<Deadlock>
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        NavigationSplitView {
            List(selection: $store.selection) {
                NavigationLink(value: "child") {
                    Text("Go To Child")
                }
            }
            .navigationTitle("Home")
            .listStyle(.sidebar)
            .task {
                await store.send(.task).finish()
            }
        } detail: {
            NavigationStack {
                switch store.destination {
                case .child:
                    if let store = store.scope(state: \.destination?.child, action: \.destination.child) {
                        ChildView(store: store)
                    }
                default:
                    EmptyView()
                }
            }
        }
    }
}


@Reducer
struct Deadlock {
    @Reducer(state: .equatable)
    enum Destination {
        case child(Child)
    }

    @ObservableState
    struct State: Equatable {
        var selection: String? = "child"
        @Presents var destination: Destination.State? = .child(Child.State())
        @Shared(.appStorage("test")) var test: Bool = false
    }

    enum Action: BindableAction {
        case task
        case destination(PresentationAction<Destination.Action>)
        case binding(BindingAction<State>)
    }

    @Dependency(\.database) var database

    var body: some ReducerOf<Self> {
        BindingReducer()
        Reduce { state, action in
            switch action {
            case .task:
                return .merge(
                    .run { _ in
                        print("Querying database…")
                        await database.query()
                        print("… query complete")
                    }.cancellable(id: CancelId.one, cancelInFlight: true)
                )

            case .destination, .binding:
                return .none
            }
        }
        .ifLet(\.$destination, action: \.destination)
    }

    private enum CancelId {
        case one
    }
}

@Reducer
struct Child {
    @ObservableState
    struct State: Equatable {
        @Shared(.appStorage("count")) var count: Int = 0
    }

    enum Action {
        case incrementButtonTapped
    }

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .incrementButtonTapped:
                state.count += 1
                return .none
            }
        }
    }
}

struct ChildView: View {
    let store: StoreOf<Child>

    var body: some View {
        VStack {
            Text(store.count.formatted())
                .navigationTitle("Child")
            Button("Increment") {
                store.send(.incrementButtonTapped)
            }
            .buttonStyle(.borderedProminent)
        }
    }
}


struct Database: DependencyKey {
    init() {
        print("Database init() start")
        UserDefaults.standard.set("foo-bar", forKey: "test-key")
        // Will never be called, will deadlock on the UserDefaults notification
        print("Database init() end")
    }

    func query() async {}

    static var liveValue: Database = Database()
}

extension DependencyValues {
    var database: Database {
        get { self[Database.self] }
        set { self[Database.self] = newValue }
    }
}

The Composable Architecture version information

1.14.0

Destination operating system

iOS 17.5

Xcode version information

Xcode 15.4

Swift Compiler version information

No response

@zachwaugh zachwaugh added the bug Something isn't working due to a bug in the library. label Sep 5, 2024
@mbrandonw
Copy link
Member

Good catch, and thanks for the simple repo! We got a fix with a test in #3356.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working due to a bug in the library.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants