Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When are Realm notifications delivered to main thread after writes on a background thread?

Tags:

swift

realm

I'm seeing crashes that either shouldn't be possible, or are very much possible and the documentation just isn't clear enough as to why.

UPDATE:

Although I disagree with the comment below asking me to separate this into multiple SO questions, if someone could focus on this one I think it would help greatly:

When are notifications delivered to the main thread? Is it possible that the results on the main thread are different than they were in a previous runloop without being notified yet of the difference?

If the answer to this question is yes the results could be different than a previous runloop without notifying then I would argue it is CRUCIAL to get this into the documentation somewhere.

Background Writes

First I think it's important to go over what I am already doing for writes. All of my writes are performed through a method that essentially looks like this (error handling aside):

func write(block: @escaping (Realm) -> ()) {
    somePrivateBackgroundSerialQueue.async {
        autoreleasepool {
            let realm = try! Realm()
            realm.refresh()
            try? realm.write { block(realm) }
        }
    }
}

Nothing crazy here, pretty well documented on your end.

Notifications and Table Views

The main question I have here is when are notifications delivered to the main thread after being written from a background thread? I have a complex table view (multiple sections, ads every 5th row) backed by realm results. My main data source looks like:

enum StoryRow {
    case story(Story) // Story is a RealmSwift.Object subclass
    case ad(Int)
}

class StorySection {
    let stories: Results<Story>

    var numberOfRows: Int {
        let count = stories.count
        return count + numberOfAds(before: count)
    }

    func row(at index: Int) -> StoryRow {
        if isAdRow(at: index) {
            return .ad(index)
        } else {
            let storyIndex = index - numberOfAds(before: index)
            return .story(stories[storyIndex])
        }
    }
}

var sections: [StorySection]

... sections[indexPath.section].row(at: indexPath.row) ...

Before building my sections array I fetch the realm results and filter them based on the type of stories for the particular screen, sort them so they are in the proper order for their sections, then I build up the sections by passing in results.filter(...date query...) to the section constructor. Finally, I results.observe(...) the main results object (not any of the results passed into the section) and reload the table view when the notification handler is called. I don't bother observing the results in the sections because if any of those results changed then the parent had to change as well and it should trigger a change notification.

The ad slots have callbacks when an ad is filled or not filled and when that happens instead of calling tableView.reloadData() I am doing something like:

guard tableView.indexPathsForVisibleRows?.contains(indexPath) == true else { return }
tableView.beginUpdates()
tableView.reloadRows(at: [indexPath], with: .automatic)
tableView.endUpdates()

The problem is, I very rarely see a crash either around an index being out of bound when accessing the realm results or an invalid table view update.

QUESTIONS

  1. Is it possible the realm changed on the main thread before any notifications were delivered?
  2. Should table view updates other than reloadData() simply not be used anywhere outside of a realm notification block?
  3. Anything else crucial I am missing?
like image 617
Andrew Avatar asked Oct 28 '22 23:10

Andrew


1 Answers

There's nothing in your code snippets or description of what you're doing that jumps out at me as obviously wrong. Having a separate callback mechanism that updates specific slots independent of Realm change notifications has a lot of potential for timing related bugs, but since you're explicitly checking if the indexPath is visible before reloading the row, I would expect that to at worst manifest as reloading the wrong row and not a crash.


The intended behavior is that refreshing the Realm and delivering notifications is an atomicish operation: anything that causes the read version to advance will deliver all notifications before returning. In simple cases, this means that you'll never see the new data without the associated notification firing first. However, there's some caveats to this:

Nested notification delivery doesn't work correctly, so beginning a write transaction from within a notification block can result in a notification being skipped (merely calling refresh() can't cause this, as it's just a no-op within a notification). If you're performing all writes on background threads you shouldn't be hitting this.

If you have multiple notification blocks, then obviously anything which gets invoked from the first one will run before the second notification block gets a chance to do things, and a call to tableView.reloadData() may result in quite a lot of things happening within the notification block. If this is the source of problems, you would hopefully see exceptions being thrown with a stack trace coming from within a notification block.

like image 181
Thomas Goyne Avatar answered Nov 15 '22 06:11

Thomas Goyne