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

How to handle client reset error in SwiftUI Realm app #7881

Closed
drmarkpowell opened this issue Jul 12, 2022 · 10 comments
Closed

How to handle client reset error in SwiftUI Realm app #7881

drmarkpowell opened this issue Jul 12, 2022 · 10 comments
Assignees

Comments

@drmarkpowell
Copy link

drmarkpowell commented Jul 12, 2022

How frequently does the bug occur?

All the time

Description

Getting a client reset error in our app, need to remove the local realm and re-open anew automatically for the user when the app enters the .error state in @AutoOpen or @AsyncOpen.

Example:

/// The main screen that determines whether to present the SyncContentView or the LocalOnlyContentView.
@main
struct ContentView: SwiftUI.App {
    var body: some Scene {
        WindowGroup {
            // Using Sync?
            if let app = app {
                SyncContentView(app: app)
            } else {
                LocalOnlyContentView()
            }
        }
    }
}

/// This view observes the Realm app object.
/// Either direct the user to login, or open a realm
/// with a logged-in user.
struct SyncContentView: View {
    // Observe the Realm app object in order to react to login state changes.
    @ObservedObject var app: RealmSwift.App

    var body: some View {
        if let user = app.currentUser {
            // If there is a logged in user, pass the user ID as the
            // partitionValue to the view that opens a realm.
            OpenSyncedRealmView().environment(\.partitionValue, user.id)
        } else {
            // If there is no user logged in, show the login view.
            LoginView()
        }
    }
}

/// This view opens a synced realm.
struct OpenSyncedRealmView: View {
    // Use AsyncOpen to download the latest changes from
    // your App Services app before opening the realm.
    // Leave the `partitionValue` an empty string to get this
    // value from the environment object passed in above.
    @AsyncOpen(appId: YOUR_APP_SERVICES_APP_ID_HERE, partitionValue: "", timeout: 4000) var asyncOpen
    
    var body: some View {
        
        switch asyncOpen {
        // Starting the Realm.asyncOpen process.
        // Show a progress view.
        case .connecting:
            ProgressView()
        // Waiting for a user to be logged in before executing
        // Realm.asyncOpen.
        case .waitingForUser:
            ProgressView("Waiting for user to log in...")
        // The realm has been opened and is ready for use.
        // Show the content view.
        case .open(let realm):
            ItemsView(itemGroup: {
                if realm.objects(ItemGroup.self).count == 0 {
                    try! realm.write {
                        realm.add(ItemGroup())
                    }
                }
                return realm.objects(ItemGroup.self).first!
            }(), leadingBarButton: AnyView(LogoutButton())).environment(\.realm, realm)
            // The realm is currently being downloaded from the server.
            // Show a progress view.
            case .progress(let progress):
                ProgressView(progress)
            // Opening the Realm failed.
            // Show an error view.
            case .error(let error):
                ErrorView(error: error)
        }
    }
}

Instead of the ErrorView simply displaying the text of the Client Reset error, I need it to invalidate the realm(s), delete all local files, and recover into the .connecting state to open the realm(s) anew all over again after the error is handled and the invalid realms are all removed, ready to start fresh.

I'm not concerned with backup/restore.

Stacktrace & log output

Error Domain=io.realm.sync Code=7 "Bad client file identifier (IDENT) Logs: https://realm.mongodb.com/groups/61fc13d94d01df7dc9eb62ac/apps/61fc49701845100d8099553e/logs?co_id=62cd969272e0f744096bf88c" UserInfo={recovered_realm_location_path=/Users/mpowell/Library/Developer/CoreSimulator/Devices/D4846DFE-71DF-472A-922E-5776FE3B65F0/data/Containers/Data/Application/8A03733F-A191-4635-B776-5B38C44480EF/Documents/mongodb-realm/.../recovered-realms/recovered_realm-20220712-084314-04MLNqrW, statusCode=208, error_action_token=<RLMSyncErrorActionToken: 0x600001231380>, NSLocalizedDescription=Bad client file identifier (IDENT) Logs: https://realm.mongodb.com/groups/61fc13d94d01df7dc9eb62ac/apps/61fc49701845100d8099553e/logs?co_id=62cd969272e0f744096bf88c}

Can you reproduce the bug?

Yes, always

Reproduction Steps

open a realm
making a breaking schema change
error occurs during client sync

Version

10.28

What SDK flavour are you using?

MongoDB Realm (i.e. Sync, auth, functions)

Are you using encryption?

No, not using encryption

Platform OS and version(s)

iOS 15 lateest

Build environment

Xcode version: 13 latest
Dependency manager and version: SPM

@drmarkpowell
Copy link
Author

I tried adding this to my app:

app.syncManager.errorHandler = { error, _ in
            guard let syncError = error as? SyncError else {
                print("Unexpected error type passed to sync error handler! \(error.localizedDescription)")
                return
            }
            switch syncError.code {
            case .clientResetError:
                print("Got CLIENT RESET ERROR")
                if let (_, clientResetToken) = syncError.clientResetInfo() {
                    AppController.deleteRealm()
                    print("DELETING REALM METADATA")
                    SyncSession.immediatelyHandleError(clientResetToken, syncManager: self.app.syncManager)
                }
            default:
                print("Realm sync error: \(error.localizedDescription)")
            }
        }

    static func deleteRealm() {
        realm.invalidate()
        if let user = AppController.shared.app.currentUser {
                let config = user.configuration(partitionValue: partitionValue)
                do {
                    print("DELETING REALM FILES")
                    _ = try Realm.deleteFiles(for: config)
                } catch {
                    print("clear realm error: \(error.localizedDescription)")
                }
        } else {
            print("UNABLE TO DELETE REALM: NO CURRENT USER")
        }
    }

and then adding this line to my OpenSyncedRealmView:

        case .error(let error):
            Text(error.localizedDescription)
            let _ = AppController.deleteRealm()
        }

...but although it does appear to attempt to delete all the realm files, it also kills my app with a terminal fault, and after restarting I still get the ErrorView containing the "Bad Client File Identifier (IDENT)..." message as noted above.

@dianaafanador3
Copy link
Contributor

dianaafanador3 commented Jul 14, 2022

You probably know this, but have you tried using our new feature for client reset, you can set it to .manual or .discardLocal https://www.mongodb.com/docs/realm/sdk/swift/sync/handle-sync-errors/#:~:text=A%20client%20reset%20discards%20local,0.
Take a look at this documentation and choose your best option
Then inject it into you open view.

OpenSyncedRealmView()
                    .environment(\.realmConfiguration, user.configuration(partitionValue: "myPartition", clientResetMode: .manual))

@dianaafanador3
Copy link
Contributor

dianaafanador3 commented Jul 14, 2022

For your specific example, you will have to create the view again with the autopen property wrapper to be able to inject the new realm. AutoOpen by itself doesn't detect that we handle the client reset and doesn't retries to open the realm.
@drmarkpowell I'll try to reproduce your code and see if we can suggest another way to go this.

@drmarkpowell
Copy link
Author

Something I don't yet understand about the suggestion above, which seems great if I could fully grasp it:
I am setting the configuration in the @Environment in my code already, like this:

 OpenSyncedRealmView(idToken: login.cognitoIdToken)
                    // for Realm schema migration, increment the version number here
                        .environment(\.realmConfiguration, Realm.Configuration(
                            schemaVersion: AppController.SCHEMA_VERSION
                        ))

although in your example you use user.configuration(...). Can I access the user like that, even before the client connects and authenticates?

@drmarkpowell
Copy link
Author

I'm trying this now, maybe this is going to work:

                    OpenSyncedRealmView(idToken: token)
                    // for Realm schema migration, increment the version number here
                        .environment(\.realmConfiguration,realmConfiguration())
...
    func realmConfiguration() -> Realm.Configuration {
        if let user = AppController.shared.app.currentUser {
            var realmConfiguration = user.configuration(partitionValue: "user", clientResetMode: .discardLocal(nil, nil))
            realmConfiguration.schemaVersion = AppController.SCHEMA_VERSION
            return realmConfiguration
        }
        return Realm.Configuration(
            schemaVersion: AppController.SCHEMA_VERSION
        )
    }

@drmarkpowell
Copy link
Author

I have distilled a solution that seems to be working for me to recover from a client reset error state into a minimal example. I would very much like and appreciate code review and pointing out anything that I might have missed or might consider doing differently. Thank you.

class AppModel: ObservableObject {
    var app: RealmSwift.App = RealmSwift.App(id: realmAppId)
    var partitionId = "user"
 
    init() {
        app.syncManager.errorHandler = { error, _ in
            guard let syncError = error as? SyncError else {
                print("Unexpected error type passed to sync error handler! \(error.localizedDescription)")
                return
            }
            switch syncError.code {
            case .clientResetError:
                if let (_, clientResetToken) = syncError.clientResetInfo() {
                    // delete realm metadata
                    SyncSession.immediatelyHandleError(clientResetToken, syncManager: self.app.syncManager)
                }
            default:
                print("Realm sync error: \(error.localizedDescription)")
            }
        }
    }

   func realmConfig() -> Realm.Configuration {
        if let user = app.currentUser {
            var realmConfiguration = user.configuration(
				partitionValue: partitionId,
				clientResetMode: .discardLocal(nil, nil)
			)
            return realmConfiguration
        }
        return Realm.Configuration()
    }
}

in a SwiftUI app:

struct App: SwiftUI.App {
    @StateObject var appModel = AppModel()

    var body: some Scene {
        return WindowGroup {
            VStack {
                OpenSyncedRealmView(idToken: login.cognitoIdToken)
					.environment(\.realmConfiguration, appModel.realmConfig())
            }
        }
    }
}

@ostatnicky
Copy link

@drmarkpowell Yes, thanks! The realmConfiguration worked for me.

Example for others:

AutoOpenView()
                .environment(\.realmConfiguration, RealmSwift.App(id: realmAppId)
                    .currentUser?
                    .configuration(
                        partitionValue: realmPartition,
                        clientResetMode: .discardLocal(nil, nil)
                    ) ?? Realm.Configuration()
                )

and in AutoOpenView I have that

@AutoOpen(
        appId: realmAppId,
        partitionValue: realmPartition,
        configuration: Realm.Configuration(
            objectTypes: [
                MyObject.self
            ]
        ),
        timeout: 10000
    )
    var autoOpen

@dianaafanador3
Copy link
Contributor

Hi @drmarkpowell the way you inject the configuration into the OpenSyncedRealmView is correct, assuming that in this view (OpenSyncedRealmView) you are using one of our the property wrappers (@AsyncOpen, @ObservedResults, ...) this property wrappers will use the configuration injected to the environment value and will open the realm with the configuration.

@dianaafanador3
Copy link
Contributor

Hey @drmarkpowell is this info good enough for you, do you need more help with this? Let us know so we can close the issue.

@drmarkpowell
Copy link
Author

Yes, my implementation looks to be working well so far. Thanks for confirming the approach is viable. Closing.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 18, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants