Closed drmarkpowell closed 1 year ago
Hi @drmarkpowell can you please attach the info of the exception you're getting. Also can you please share the code on your SearchView
, and how you inject/create the Realm used by the @ObservedResults
.
From the look of it, seems like you are accessing the results from a different thread that the one the realm was created.
The issue occurs randomly for users in production and I haven't duplicated it locally yet...so I can't share any additional details about the RLMException since I can't get them. The @ObservedResults getters doesn't throw any Exceptions, so I can't catch them and dump them (AFAIK).
Providing a bunch more code, redacted as little as possible to protect private details. I also have an open MongoDB support ticket on this where I am providing additional details if that is helpful at all.
struct MyApp: SwiftUI.App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var login = LoginViewModel.shared
var body: some Scene {
return WindowGroup {
VStack {
if login.cognitoIdToken.isEmpty {
LoginView(login: login)
.frame(height: 0)
} else if let realmConfig = RealmController.realmConfiguration("abc") {
OpenSyncedRealmView(idToken: login.cognitoIdToken)
.environment(\.realmConfiguration, realmConfig)
} else {
OpenSyncedRealmView(idToken: login.cognitoIdToken)
}
}
}
}
struct OpenSyncedRealmView: View {
@StateObject var progressViewModel = SyncProgressViewModel()
@AutoOpen(
appId: RealmAppId.shared.id,
partitionValue: "abc",
configuration: RealmController.realmConfiguration("abc"),
timeout: 5000
) var abcRealm
var idToken: String
var body: some View {
switch abcRealm {
case .connecting:
ProgressView("Connecting...")
case .waitingForUser:
loginView()
case .open(let realm):
if let realmConfig = RealmController.realmConfiguration("abc") {
tabView(realm, realmConfig)
.environmentObject(AppController.shared.app)
} else {
tabView(realm)
.environmentObject(AppController.shared.app)
}
case .progress(let progress):
PleaseWaitLoadingDataView(progress: progressViewModel)
.task { @MainActor in
progressViewModel.progress = progress
}
case .error(let error):
Text(error.localizedDescription)
}
}
func tabView(_ realm: Realm, _ realmConfig: Realm.Configuration) -> some View {
RealmController.shared.realm = realm
return ABCTabView()
.environment(\.realm, realm)
.environment(\.realmConfiguration, realmConfig)
}
func tabView(_ realm: Realm) -> some View {
RealmController.shared.realm = realm
return ABCTabView()
.environment(\.realm, realm)
}
func loginView() -> some View {
AppController.shared.app.login(credentials: .jwt(token: idToken)) { result in
if case let .failure(error) = result {
print("Login failed: \(error.localizedDescription)")
} else {
print("Login success: \(result)")
}
}
return ProgressView("Logging in...")
}
}
class RealmController: ObservableObject {
static var shared = RealmController()
static let resetModeHandler = ClientResetMode.recoverOrDiscardUnsyncedChanges(beforeReset: { _ in
os_log(.error, "before client reset")
}, afterReset: { (_, _) in
os_log(.error, "after client reset")
})
@Published var lastDownload: Date? = UserDefaults.standard.value(forKey: lastDownloadKey) as? Date ?? nil
@Published var online: Bool = false
var realm: Realm? {
didSet {
do {
xyzRealm = try RealmController.configureRealm(.xyz)
} catch let error {
print("Error configuring xyz realm: \(error.localizedDescription)")
}
do {
defRealm = try RealmController.configureRealm(.def)
} catch let error {
print("Error configuring def realm: \(error.localizedDescription)")
}
do {
userRealm = try RealmController.configureRealm(.user)
} catch let error {
print("Error configuring user realm: \(error.localizedDescription)")
}
do {
usRealm = try RealmController.configureRealm(.us)
} catch let error {
print("Error configuring us realm: \(error.localizedDescription)")
}
// clean up any existing network reachability or download monitors
stopReachabilty()
stopRealmNotification()
// set up the network reachability and download monitors
setupReachability()
setupRealmNotification()
}
}
var userRealm: Realm?
var xyzRealm: Realm?
var vwRealm: Realm?
var defRealm: Realm?
var downloadProgressToken: SyncSession.ProgressNotificationToken?
let reachability = try? Reachability(hostname: "www.amazon.com")
private init() {
}
deinit {
stopReachabilty()
stopRealmNotification()
}
static func realmConfiguration(_ partitionId: String) -> Realm.Configuration? {
if let user = AppController.shared.app.currentUser {
var realmConfiguration = user.configuration(
partitionValue: partitionId,
clientResetMode: resetModeHandler
)
realmConfiguration.schemaVersion = AppController.SCHEMA_VERSION
return realmConfiguration
}
return nil
}
static func configureRealm(_ realmType: RealmType) throws -> Realm? {
guard let user = AppController.shared.app.currentUser else {
return nil
}
switch realmType {
case .abc:
return try createRealm("abc", user)
case .def:
return isdef() ? try createRealm("def", user) : nil
case .xyz:
return isxyz() ? try createRealm("xyz", user) : nil
case .vw:
return isvw() ? try createRealm("vw", user) : nil
case .user:
return !username().isEmpty ? try createRealm(username(), user) : nil
}
}
static func createRealm(_ partition: String, _ user: RealmSwift.User) throws -> Realm {
return try Realm(configuration: user.configuration(
partitionValue: partition,
clientResetMode: resetModeHandler
))
}
func resumeAllSessions() {
realm?.syncSession?.resume()
userRealm?.syncSession?.resume()
xyzRealm?.syncSession?.resume()
defRealm?.syncSession?.resume()
vwRealm?.syncSession?.resume()
}
func invalidateRealms() {
realm?.invalidate()
userRealm?.invalidate()
xyzRealm?.invalidate()
defRealm?.invalidate()
vwRealm?.invalidate()
}
}
struct SearchView: View {
@ObservedResults(Favorite.self, configuration: userConfig()) var favorites
@ObservedResults(RecentSearchResult.self, configuration: userConfig(), sortDescriptor: RecentSearchResult.sortDescriptor()) var recents
@ObservedResults(Person.self, configuration: abcConfig(), filter: Person.defaultPredicate, sortDescriptor: Person.sortDescriptor()) var peopleResults
@ObservedResults(Cafeteria.self, configuration: abcConfig(), filter: Cafeteria.defaultPredicate, sortDescriptor: Cafeteria.sortDescriptor()) var cafeResults
@ObservedResults(Building.self, configuration: abcConfig(), filter: Building.defaultPredicate, sortDescriptor: Building.sortDescriptor()) var buildingResults
@ObservedResults(Org.self, configuration: abcConfig(), filter: Org.defaultPredicate, sortDescriptor: Org.sortDescriptor()) var orgResults
@ObservedResults(FloorSpace.self, configuration: abcConfig(), filter: FloorSpace.defaultPredicate, sortDescriptor: FloorSpace.sortDescriptor()) var abcRoomResults
@ObservedResults(FloorSpace.self, configuration: vwConfig(), filter: FloorSpace.defaultPredicate, sortDescriptor: FloorSpace.sortDescriptor()) var vwRoomResults
@State private var query = ""
var body: some View {
NavigationSplitView {
VStack {
ScrollViewReader { value in
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
Section {
if noSearchResults() { // CRASH HERE: this calls @ObservedResult getters and throws fatal RLMException
Text("No results found")
.listRowSeparator(.hidden)
} else {
searchResultsSection(favoriteSearchResults(), "Favorites", self)
searchResultsSection(recentSearchResults(), "Recents", self)
searchResultsSection(cafeteriaSearchResults(), "Cafeterias", self)
buildingsSection(buildingResults.lazy, favorites, self, selectBuildingOnMap)
roomsSection(roomResults(), favorites, self, selectRoomOnMap)
searchResultsSection(orgSearchResults(), "Orgs", self)
searchResultsSection(peopleSearchResults(), "People", self)
}
} header: {
Searchbar(query: $query, searchFocused: $searchFocus)
.background(Color.background)
.padding(EdgeInsets(top: 0, leading: 8, bottom: 4, trailing: 8))
}
}
.onChange(of: query) { _ in
if let firstResultId = firstResultId() {
value.scrollTo(firstResultId, anchor: .bottom)
}
Task { @MainActor in
filterSearchResults()
}
}
}
}
}
.onAppear {
if query.isEmpty {
$peopleResults.filter = search.defaultPeopleFilter
$cafeResults.filter = search.defaultCafeteriaFilter
$buildingResults.filter = search.defaultBuildingFilter
$orgResults.filter = Org.defaultPredicate
$abcRoomResults.filter = FloorSpace.defaultPredicate
$vwRoomResults.filter = FloorSpace.defaultPredicate
} else {
filterSearchResults()
}
}
}
func noSearchResults() -> Bool {
return favorites.isEmpty &&
recents.isEmpty &&
cafeResults.isEmpty &&
buildingResults.isEmpty &&
orgResults.isEmpty &&
peopleResults.isEmpty &&
abcRoomResults.isEmpty &&
vwRoomResults.isEmpty
}
func filterSearchResults() {
$favorites.filter = query.isEmpty ? nil : search.favoriteQuery(query)
$recents.filter = query.isEmpty ? nil : search.recentsQuery(query)
$cafeResults.filter = query.isEmpty ? Cafeteria.defaultPredicate : search.cafeQuery(query)
$buildingResults.filter = query.isEmpty ? Building.defaultPredicate : search.buildingQuery(query)
$abcRoomResults.filter = query.isEmpty ? FloorSpace.defaultPredicate : search.roomQuery(query)
$vwRoomResults.filter = query.isEmpty ? FloorSpace.defaultPredicate : search.roomQuery(query)
$orgResults.filter = query.isEmpty ? Org.defaultPredicate : search.orgQuery(query)
$peopleResults.filter = query.isEmpty ? Person.defaultPredicate : search.peopleQuery(query)
}
static func userConfig() -> Realm.Configuration? {
return RealmController.shared.userRealm?.configuration
}
static func abcConfig() -> Realm.Configuration? {
return RealmController.shared.realm?.configuration
}
}
https://github.com/realm/realm-core/pull/6411 might be related to this. It seems like something that'd cause problems very rarely, but could possibly cause freezing an object to throw a BadVersion exception.
I was finally yesterday able to reproduce the bug in development and it appears to stem from an invalid session authorization token being used to (re)initiate the Realm session.
To you question, I did try 10.38.0 and it did not address the issue.
I have a fix to prevent invalid access tokens from entering the workflow and we’re preparing to test it out.
On Apr 11, 2023, at 6:44 AM, Diana Perez Afanador @.***> wrote:
@drmarkpowell https://github.com/drmarkpowell we released a version (v10.38.0 https://github.com/realm/realm-swift/releases/tag/v10.38.0) with a possible fix for this, did you try the fix?, are you still getting this issue?
— Reply to this email directly, view it on GitHub https://github.com/realm/realm-swift/issues/8180#issuecomment-1503383238, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAH2GP4P3ICIFTMR2JAIIFLXAVN5HANCNFSM6AAAAAAWBT3X7I. You are receiving this because you were mentioned.
@drmarkpowell thanks for getting back, let us know any update on the fix.
I just discovered that if a SwiftUI view is running and is listening to a ObservedResults
and I call .invalidate()
on the realm instance, this crash happens 100% of the time.
It seems likely that we found the cause of the error I got here. There was a path through our code that it could very occasionally take that called realm.invalidate()
on the active realm during sign out and sign in.
As this looked like it was not just unnecessary but also a bad idea altogether, I removed that from the code and deployed an update post-removal. So far this looks like it resolves our problem and I should know more early in the coming week.
On Apr 12, 2023, at 3:02 AM, Diana Perez Afanador @.***> wrote:
@drmarkpowell https://github.com/drmarkpowell thanks for getting back, let us know any update on the fix.
— Reply to this email directly, view it on GitHub https://github.com/realm/realm-swift/issues/8180#issuecomment-1505003100, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAH2GP5K3SXXIMDGPUSN5E3XAZ4UDANCNFSM6AAAAAAWBT3X7I. You are receiving this because you were mentioned.
@drmarkpowell let us know if this completely solves your issue or if there is something we can do from our side.
Not calling invalidate() on the realm has solved it!The only thing I can think of to improve the SDK is to add more even more docs ( there are great docs already but always room to improve) for error handling and recovery, which I’ve already recommend to the docs team. Also, maybe a great big warning on invalidate() to not call it on a live Realm instance, ever, because it does bad things?Thanks for the help!On Apr 20, 2023, at 4:38 AM, Diana Perez Afanador @.***> wrote: @drmarkpowell let us know if this completely solves your issue or if there is something we can do from our side.
—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>
How frequently does the bug occur?
Sometimes
Description
Users running our Realm app run
.isEmpty()
on @ObservedResults and this throws an internal RLMException that crashes the app.Stacktrace & log output
Can you reproduce the bug?
Sometimes
Reproduction Steps
These stack traces are collected from users running our app in production and we have not yet reproduced it manually, yet.
Version
10.37.0
What Atlas Services are you using?
Atlas Device Sync
Are you using encryption?
No
Platform OS and version(s)
iOS 13.2.1
Build environment
Xcode version: 14.2 Dependency manager and version: SPM