realm / realm-swift

Realm is a mobile database: a replacement for Core Data & SQLite
https://realm.io
Apache License 2.0
16.32k stars 2.15k forks source link

Collection change notification delivers deletion for empty collection #7754

Open jadar opened 2 years ago

jadar commented 2 years ago

How frequently does the bug occur?

All the time

Description

I've been trying to debug a UITableView crash where there is an inconsistent number of rows. I can consistently cause this after the entities that were previously displayed are all deleted in another view. When the list view appears, a change listener is created for a Results collection. The .initial update is delivered, and the count is zero. Soon after (though I'm not sure if in the same run-loop or not,) an update is delivered with 1 deletion of row 0. However, I can confirm in the debugger that the collection had zero rows before the update. I'm confused why this update would be delivered if there was nothing there in the first place. I think I've ruled out programmer-errors on my side, so I'm pretty sure it's an issue in Realm's change notifications.

Let me know if you need any more information.

Stacktrace & log output

Recieved update for section 1, with 1 deletions 0 insertions and 0 modifications.
2022-04-21 11:41:14.047794-0400 Traction Field[30049:1902452] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], UITableView.m:2171
2022-04-21 11:41:14.048436-0400 Traction Field[30049:1902452] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete row 0 from section 1 which only contains 0 rows before the update'
*** First throw call stack:
(0x1c9dadd78 0x1e2a12734 0x1cb6331f0 0x1cc666944 0x1cc4189d4 0x1cc62e7e0 0x1cc2300d4 0x1046561a0 0x1045ae8b4 0x1045ae2e0 0x1045ae408 0x1c9d55bb4 0x1c9d24b70 0x1c9d1fc2c 0x1c9d336b8 0x1e5dcd374 0x1cc698e88 0x1cc41a5ec 0x1e1a0fecc 0x10460749c 0x104607424 0x1046075a8 0x109881ce4)
libc++abi: terminating with uncaught exception of type NSException
dyld4 config: DYLD_LIBRARY_PATH=/usr/lib/system/introspection DYLD_INSERT_LIBRARIES=/Developer/usr/lib/libBacktraceRecording.dylib:/Developer/usr/lib/libMainThreadChecker.dylib:/Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete row 0 from section 1 which only contains 0 rows before the update'
terminating with uncaught exception of type NSException

Can you reproduce the bug?

Yes, always

Reproduction Steps

  1. Hydrate Realm with some records.
  2. Set up a UITableView backed by a Results, with a collection observer.
  3. Invalidate collection observer (in my case, on viewDidDisappear)
  4. On a background thread, delete all records.
  5. Re-setup collection observer for the same Results instance.
  6. Receive an .initial update, confirm there RLMCollection.count is 0.
  7. Receive the .update update with the deletion.
  8. Crash

Version

10.24.2, 10.25.1

What SDK flavour are you using?

Local Database only

Are you using encryption?

No, not using encryption

Platform OS and version(s)

iOS 15.4.1

Build environment

Xcode version: 13.3.0 Dependency manager and version: SPM

jadar commented 2 years ago

I've managed to recreate the issue in a new project. Here is a link to the repo. https://github.com/jadar/RealmNotificationIssue

Once you launch the app, follow these steps to reproduce.

  1. Go to list tab
  2. Go to actions tab
  3. Press Hydrate
  4. Go to list tab
  5. Go to actions tab
  6. Press Delete
  7. Go to list tab
  8. Wait a second and watch it crash
tgoyne commented 2 years ago

Thanks for the repro case! I think I see what the problem is. We don't handle the pattern "remove last observer from collection -> perform a write which modifies the collection -> add a new observer to the collection" correctly. We skip doing the changeset calculation for the collection while it doesn't have any observers, but don't fully revert into the state we had before the first observer was added, and so the first changeset calculated after adding an observer again is wrong.

This should be fairly easy to fix, and you can work around it for now by moving collection = realm.objects(Animal.self) from viewDidLoad() to viewWillAppear() so that you aren't observing a previously-but-no-longer-observed collection on the second appearance of the view.

jadar commented 2 years ago

Thanks, that solution worked! It would be nice to keep using the same collection, but I suppose that is a reasonable workaround. I use that pattern all across my codebase and also keep seeing these crashes in Sentry. Perhaps this isn't the only area that it's happening.

jadar commented 1 year ago

Recently I updated to a recent version of Realm (v10.35.0) and this seems still to be happening.

aehlke commented 1 month ago

@jadar did you figure this out?