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

Crash when deleting objects that are being observed #6536

Closed
anlaital opened this issue May 27, 2020 · 12 comments · Fixed by realm/realm-core#3945
Closed

Crash when deleting objects that are being observed #6536

anlaital opened this issue May 27, 2020 · 12 comments · Fixed by realm/realm-core#3945

Comments

@anlaital
Copy link

anlaital commented May 27, 2020

Goals

Listen to changes to Results that has a filter included and get notified when the results are changed.

Expected Results

Get notified of changes without crashing.

Actual Results

Realm v5.0.0 crashes when objects are deleted and there is an attached Results listener for them. This seems to happen only after a second round of deletion. This means that first when objects are deleted everything works as expected, but if you then run another round of deletions to those new objects, Realm crashes with the following stack trace:

*** Terminating app due to uncaught exception 'RLMException', reason: 'Key not found'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff23e3cf0e __exceptionPreprocess + 350
	1   libobjc.A.dylib                     0x00007fff50ba89b2 objc_exception_throw + 48
	2   Realm                               0x000000010e4cc156 _Z20RLMThrowResultsErrorP8NSString + 982
	3   Realm                               0x000000010e4cc780 _ZL25translateRLMResultsErrorsIZ19-[RLMResults count]E3$_1EDaOT_P8NSString + 96
	4   Realm                               0x000000010e4cc718 -[RLMResults count] + 40
	5   RealmSwift                          0x000000010fa0ea8b $s10RealmSwift7ResultsV5countSivg + 59
	6   RealmSwift                          0x000000010fa119a8 $s10RealmSwift7ResultsVyxGSlAASl5countSivgTW + 24
	7   libswiftCore.dylib                  0x00000001120f5e23 $ss32_copyCollectionToContiguousArrayys0dE0Vy7ElementQzGxSlRzlF + 243
	8   libswiftCore.dylib                  0x00000001120e5d78 $sSTsE22_copyToContiguousArrays0cD0Vy7ElementQzGyFTm + 24
	9   RealmSwift                          0x000000010fa11c65 $s10RealmSwift7ResultsVyxGSTAAST22_copyToContiguousArrays0fG0Vy7ElementQzGyFTW + 53
	10  libswiftCore.dylib                  0x00000001120ea2d3 $sSaySayxGqd__c7ElementQyd__RszSTRd__lufC + 19
	11  AppName                              0x000000010980b43b $s6APpName3DayC15observeMessages33_50119EB0AAD00B0757E30A06D82E89CBLLyyFy10RealmSwift0N16CollectionChangeOyAF7ResultsVyAA10RdbMessageCGGcfU_ + 283
	12  RealmSwift                          0x000000010fa10904 $s10RealmSwift0A16CollectionChangeOyAA7ResultsVyxGGIegg_AGIegn_AA0aC5ValueRzlTR + 52
	13  RealmSwift                          0x000000010fa00678 $s10RealmSwift20ObservableCollectionPAAE16wrapObserveBlockyy011BackingObjcD0QzSg_So19RLMCollectionChangeCSgs5Error_pSgtcyAA0adK0OyxGcFyAG_AjLtcfU_ + 1656
	14  RealmSwift                          0x000000010fa00be1 $s10RealmSwift20ObservableCollectionPAAE16wrapObserveBlockyy011BackingObjcD0QzSg_So19RLMCollectionChangeCSgs5Error_pSgtcyAA0adK0OyxGcFyAG_AjLtcfU_TA + 65
	15  RealmSwift                          0x000000010f9d0914 $sSo10RLMResultsCyyXlGSgSo19RLMCollectionChangeCSgs5Error_pSgIegngg_AdgIIegggg_TR + 84
	16  RealmSwift                          0x000000010f9d0a21 $sSo10RLMResultsCyyXlGSgSo19RLMCollectionChangeCSgs5Error_pSgIegggg_AdGSo7NSErrorCSgIeyByyy_TR + 161
	17  Realm                               0x000000010e38b9c7 _ZN12_GLOBAL__N_125CollectionCallbackWrapperclERKN5realm19CollectionChangeSetESt13exception_ptr + 663
	18  Realm                               0x000000010e38b545 _ZN5realm24CollectionChangeCallback4ImplIN12_GLOBAL__N_125CollectionCallbackWrapperEE5afterERKNS_19CollectionChangeSetE + 69
	19  Realm                               0x000000010e23dbb6 _ZN5realm24CollectionChangeCallback5afterERKNS_19CollectionChangeSetE + 38
	20  Realm                               0x000000010e23db19 _ZZN5realm5_impl18CollectionNotifier13after_advanceEvENK4$_10clINS_4util17CheckedUniqueLockENS1_8CallbackEEEDaRT_RT0_ + 169
	21  Realm                               0x000000010e21cdb4 _ZN5realm5_impl18CollectionNotifier17for_each_callbackIZNS1_13after_advanceEvE4$_10EEvOT_ + 148
	22  Realm                               0x000000010e21cd19 _ZN5realm5_impl18CollectionNotifier13after_advanceEv + 25
	23  Realm                               0x000000010e21dfa0 _ZN5realm5_impl15NotifierPackage13after_advanceEv + 336
	24  Realm                               0x000000010e5f76c8 _ZN12_GLOBAL__N_126advance_with_notificationsIZN5realm5_impl11transaction5beginERKNSt3__110shared_ptrINS1_11TransactionEEEPNS1_14BindingContextERNS2_15NotifierPackageEE3$_2EEvSB_S9_OT_SD_ + 1208
	25  Realm                               0x000000010e5f7201 _ZN5realm5_impl11transaction5beginERKNSt3__110shared_ptrINS_11TransactionEEEPNS_14BindingContextERNS0_15NotifierPackageE + 49
	26  Realm                               0x000000010e2f253c _ZN5realm5_impl16RealmCoordinator16promote_to_writeERNS_5RealmE + 316
	27  Realm                               0x000000010e55bc21 _ZN5realm5Realm17begin_transactionEv + 545
	28  Realm                               0x000000010e4a308c -[RLMRealm beginWriteTransactionWithError:] + 44
	29  Realm                               0x000000010e4a3058 -[RLMRealm beginWriteTransaction] + 40
	30  RealmSwift                          0x000000010f9f23f5 $s10RealmSwift0A0V10beginWriteyyF + 53
	31  RealmSwift                          0x000000010f9f2055 $s10RealmSwift0A0V5write16withoutNotifying_xSaySo20RLMNotificationTokenCG_xyKXEtKlF + 213

This crash occurs if you try to read the results either from the Results collection directly or from the Results that are passed as the first argument to Results.observe's case .update.

The workaround for this is to not use Results at all and always refetch the data from Realm explicitly when a change notification is fired.

Steps to Reproduce

Listen to changes and delete objects. Second round of deletion always crashes.

Code Sample

No code sample as of yet. I can try to isolate the issue and generate a reproducible sample from our massive code base.

Version of Realm and Tooling

Realm framework version: 5.0.0

Realm Object Server version: N/A

Xcode version: 11.5

iOS/OSX version: 13.5

Dependency manager + version: Cocoapods 1.9.1

@tgoyne tgoyne self-assigned this Jun 1, 2020
@tgoyne
Copy link
Member

tgoyne commented Jun 1, 2020

'Key not found' indicates that some part of getting the count from the query is trying to look up an object in a table and not finding it, presumably because that object was deleted. There aren't very many places where queries do that, but I tried to check them all and so far haven't found a way to reproduce this (or spotted anything obviously suspicious). If you can figure out what the query is that causes this that'd probably help; it most likely involves either filtering a List or LinkingObjects or doing some sort of query over a link.

@anlaital
Copy link
Author

anlaital commented Jun 2, 2020

The query that crashes and that we use to observe changes looks something like this:

var results = Results<SomeObjectClass>!

var resultsToken: NotificationToken?

func getResults() {
  results = realm().objects(SomeObjectClass.self).filter("stringKey = %@", functionThatCalculatesTheStringKey())
} 

func observeResults() {
  resultsToken = results.observe {
    changes in 
    // This notifies internally of changes, which in turn cause look-ups into `results` as we generate UI elements out of those.
  }
}

The interesting thing about this is that when we do delete a bunch of the objects it doesn't crash. It only crashes after second deletion. The system that handles these first deletes the outdated SomeObjectClass instances and then re-generates them. The first time we run this everything works as normal, but on the second go we get a crash when we try to access the Results in any way.

Workaround for this was to not use Results at all, but rather convert that to an Array instead.

SomeObjectClass is quite simple and only contains native types, so it has no dependencies to other Realm objects. It is not used anywhere in LinkingObjects either.

stringKey is an indexed property that is used to group the objects.

@anlaital
Copy link
Author

anlaital commented Jul 2, 2020

Have you had any success in finding a fix for this issue? It seems to be growing rapidly in numbers in our app with over 2k crashes now. I suppose the only thing we can do is to stop using Results entirely and always deal with arrays instead.

@tgoyne
Copy link
Member

tgoyne commented Jul 3, 2020

No, I haven't been able to reproduce it and I haven't been able to figure out why it might be happening.

@kfound
Copy link

kfound commented Aug 19, 2020

I'm having similar issue in my app.

I updated to Realm 5.3.3 and now if I leave an observer in place on my Results before deleting an object the app crashes. In both my case and the example pasted above, it's when we try to access the count of the Results object.

My workaround for this is to remove my observer before I delete an object, and then immediately refetch into my Results list as soon as the delete completes. Accessing count of the results crashes it. Any chance there might be a fix for this at some point? It was working fine before I updated to Realm v5

This is the query I'm using (parentID is a String):

    var predicateList: [NSPredicate] = []
    predicateList.append(NSPredicate(format: "parentID == %@", parentID))
    let query = NSCompoundPredicate(andPredicateWithSubpredicates: predicateList)
    
    let chartList = realm.objects(Chart.self).filter(query)
        .sorted(byKeyPath: "name",
                ascending: true)

Here's the log from the crash:

*** Terminating app due to uncaught exception 'RLMException', reason: 'Key not found'
*** First throw call stack:
(
0   CoreFoundation                      0x00007fff23c7127e __exceptionPreprocess + 350
1   libobjc.A.dylib                     0x00007fff513fbb20 objc_exception_throw + 48
2   Realm                               0x000000010bd14e95 _Z20RLMThrowResultsErrorP8NSString + 997
3   Realm                               0x000000010bd15512 _ZL25translateRLMResultsErrorsIZ19-[RLMResults count]E3$_1EDaOT_P8NSString + 98
4   Realm                               0x000000010bd154a8 -[RLMResults count] + 40
5   RealmSwift                          0x000000010d21512b $s10RealmSwift7ResultsV5countSivg + 59
6   CWP                                 0x000000010af5efa9 $s3CWP25ChartDetailViewControllerC016updateSortButtonD10Visibility33_77B7EA54206B065AC9EC5D37A7607875LLyyF + 217
7   CWP                                 0x000000010af5959d $s3CWP25ChartDetailViewControllerC18observeDataResults33_77B7EA54206B065AC9EC5D37A7607875LLyyFy10RealmSwift0P16CollectionChangeOyAF0H0VyAA11LibraryItemCGGcfU1_ + 1485
8   RealmSwift                          0x000000010d2170e4 $s10RealmSwift0A16CollectionChangeOyAA7ResultsVyxGGIegg_AGIegn_AA0aC5ValueRzlTR + 52
9   RealmSwift                          0x000000010d20600c $s10RealmSwift20ObservableCollectionPAAE16wrapObserveBlockyy011BackingObjcD0QzSg_So19RLMCollectionChangeCSgs5Error_pSgtcyAA0adK0OyxGcFyAG_AjLtcfU_ + 1676
10  RealmSwift                          0x000000010d2065d2 $s10RealmSwift20ObservableCollectionPAAE16wrapObserveBlockyy011BackingObjcD0QzSg_So19RLMCollectionChangeCSgs5Error_pSgtcyAA0adK0OyxGcFyAG_AjLtcfU_TA + 66
11  RealmSwift                          0x000000010d1d50b4 $sSo10RLMResultsCyyXlGSgSo19RLMCollectionChangeCSgs5Error_pSgIegngg_AdgIIegggg_TR + 84
12  RealmSwift                          0x000000010d1d51c5 $sSo10RLMResultsCyyXlGSgSo19RLMCollectionChangeCSgs5Error_pSgIegggg_AdGSo7NSErrorCSgIeyByyy_TR + 165
13  Realm                               0x000000010bbccb89 _ZN12_GLOBAL__N_125CollectionCallbackWrapperclERKN5realm19CollectionChangeSetESt13exception_ptr + 345
14  Realm                               0x000000010bbcc845 _ZN5realm24CollectionChangeCallback4ImplIN12_GLOBAL__N_125CollectionCallbackWrapperEE5afterERKNS_19CollectionChangeSetE + 69
15  Realm                               0x000000010ba8732e _ZN5realm24CollectionChangeCallback5afterERKNS_19CollectionChangeSetE + 46
16  Realm                               0x000000010ba87289 _ZZN5realm5_impl18CollectionNotifier13after_advanceEvENK4$_10clINS_4util17CheckedUniqueLockENS1_8CallbackEEEDaRT_RT0_ + 169
17  Realm                               0x000000010ba67396 _ZN5realm5_impl18CollectionNotifier17for_each_callbackIZNS1_13after_advanceEvE4$_10EEvOT_ + 166
18  Realm                               0x000000010ba672e9 _ZN5realm5_impl18CollectionNotifier13after_advanceEv + 25
19  Realm                               0x000000010ba685b7 _ZN5realm5_impl15NotifierPackage13after_advanceEv + 343
20  Realm                               0x000000010be3526f _ZN12_GLOBAL__N_126advance_with_notificationsIZN5realm5_impl11transaction7advanceERKNSt3__110shared_ptrINS1_11TransactionEEEPNS1_14BindingContextERNS2_15NotifierPackageEE3$_1EEvSB_S9_OT_SD_ + 831
21  Realm                               0x000000010be34f29 _ZN5realm5_impl11transaction7advanceERKNSt3__110shared_ptrINS_11TransactionEEEPNS_14BindingContextERNS0_15NotifierPackageE + 57

@kfound
Copy link

kfound commented Aug 19, 2020

Wanted to post again to say that I've got two other objects I can delete and leave the observers in place and it seems fine. It's my more complex object that I'm having the observer issues with. With my complex object, I only have to delete one and it crashes with the 'Key not found' error.

I was trying to figure out the difference between the two objects. I have a Chart and a Project.

The Chart has a lot more data in it. It contains lists of other objects, lists of Int. Because it has so many sub objects I do some specific looping through the lists of objects and delete those first vs letting my cascade delete code handle it. That was a speed optimization I did a long time ago and it sped up the delete a lot.

Both Chart and Project are using LinkingObjects so I can find the object that refers to it and delete that as well. In both cases I'm doing all the deleting as part of a single write transaction.

I haven't been able to narrow down the issue yet - but am posting in case this helps someone else.

@DwayneCoussement
Copy link

DwayneCoussement commented Sep 25, 2020

Hey, I am able to reproduce this; I think the issue has to do with indexedProperties somehow. Following class with following test will crash 100% of the time

import RealmSwift
import Foundation

public enum ActivityType: String {
	case rootSync
}

@objcMembers public class Activity: Object {
	public dynamic var id: String = UUID().uuidString
	dynamic var type: String = ""
	public dynamic var createdDate: Date = Date()
		
	public var activityType: ActivityType? {
		return ActivityType(rawValue: type)
	}
	
	public convenience init(type: ActivityType) {
		self.init()
		self.type = type.rawValue
	}
	
	override public static func indexedProperties() -> [String] {
		return ["type"]
	}
}

protocol ActivitiesProviderProtocol {
	var activities: Results<Activity>? { get }
}

class ActivitySyncObserver {
	var onSyncing: (() -> Void)?
	var onFinished: (() -> Void)?
	var onFailure: ((Error) -> Void)?

	private let activitiesProvider: ActivitiesProviderProtocol
	private var isSyncing: Bool = false
	private var realmNotificationToken: NotificationToken?

	init(activitiesProvider: ActivitiesProviderProtocol) {
		self.activitiesProvider = activitiesProvider
		setupObservation()
	}

	private func setupObservation() {
		realmNotificationToken = activitiesProvider.activities?.observe(on: DispatchQueue.main) { [weak self] changes in
			
		}
	}
}
import Foundation
import XCTest
import RealmSwift
import Realm
@testable import RealmTest

private class MockRootSyncActivitiesProvider: ActivitiesProviderProtocol {
	var activities: Results<Activity>? {
		return realm.objects(Activity.self).filter(NSPredicate(format: "type == %@", ActivityType.rootSync.rawValue))
	}

	private var realm: Realm

	init() throws {
		do {
			var config = Realm.Configuration.defaultConfiguration
			config.inMemoryIdentifier = "testrealm"
			realm = try Realm(configuration: config)
		} catch {
			throw error
		}
	}

	deinit {
		do {
			try realm.write {
				realm.deleteAll()
			}
		} catch {
			XCTFail("Could not clear realm")
		}
	}

	func addRootSyncActivities(_ numberOfActivities: Int) {
		do {
			try realm.write {
				for _ in 0..<numberOfActivities {
					realm.add(Activity(type: .rootSync))
				}
			}
		} catch {
			XCTFail("Could not add root sync activities to realm")
		}
	}

	func removeActivies(_ numberToRemove: Int) {
		guard let activities = activities else {
			XCTFail("Could not get root activities result list. something went wrong with realm.")
			return
		}
		do {
			print(activities.count)
			try realm.write {
				for (index, activity) in activities.enumerated() {
					guard index < numberToRemove else {
						break
					}
					realm.delete(activity)
				}
			}
		} catch {
			XCTFail("Could not remove root sync activities from realm")
		}
		print(activities.count)
	}
}

class RealmTest: XCTestCase {
	func testOnFinished() {
		var provider: MockRootSyncActivitiesProvider
		do {
			provider = try MockRootSyncActivitiesProvider()
		} catch {
			XCTFail("Could not create MockRootSyncActivitiesProvider")
			return
		}
		let observer = ActivitySyncObserver(activitiesProvider: provider)
		provider.addRootSyncActivities(5)

		observer.onFinished = {
			XCTFail("Should only finish when all root syncs are complete")
		}

		let failureExpectation = XCTestExpectation(description: "onFinished fails expectation")
		failureExpectation.isInverted = true
		provider.removeActivies(4)
		observer.onFinished = {
			failureExpectation.fulfill()
		}
		wait(for: [failureExpectation], timeout: 2.0)
	}
}

@tgoyne tgoyne self-assigned this Sep 25, 2020
@tgoyne
Copy link
Member

tgoyne commented Sep 25, 2020

Thanks for the repro case! That does fail in the expected way for me as well.

@tgoyne
Copy link
Member

tgoyne commented Sep 25, 2020

A reduced test case:

@objcMembers public class Activity: Object {
    dynamic var type: String = ""
    override public static func indexedProperties() -> [String] {
        return ["type"]
    }
}

class RealmTest: XCTestCase {
    func testOnFinished() {
        var config = Realm.Configuration.defaultConfiguration
        config.inMemoryIdentifier = "testrealm"
        let realm = try! Realm(configuration: config)
        try! realm.write {
            for _ in 0..<5 {
                realm.add(Activity(value: ["rootSync"]))
            }
        }
        let activities = realm.objects(Activity.self).filter("type == %@", "rootSync")
        print(activities.count)
        try! realm.write {
            realm.delete(Array(activities[0..<4]))
        }
        print(activities.count)
    }
}

It appears that the key thing which I hadn't tried is that at least in this case, it only breaks if all but the last item are deleted.

@stephenwoodford
Copy link

@tgoyne Thanks for getting this fix done so quickly! Any reason why using SPM to include Realm does include this fix, but using Carthage does not? Both when using the precompiled binaries and also when building from source?

@tgoyne
Copy link
Member

tgoyne commented Sep 29, 2020

There should not be any difference based on installation method.

@tgoyne
Copy link
Member

tgoyne commented Sep 29, 2020

It looks like the sync release pulled in the wrong core version so the fix isn't present when using the prebuilt library for that.

@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
6 participants