realm-swift: "Cannot register notification blocks from within write transactions." when making changes in quick succession

How frequently does the bug occur?

Always

Description

I have a List of TextField. When I add items and edit their names quickly, I get a “Cannot register notification blocks from within write transactions” error.

The error doesn’t happen if I only update the item name while typing fast. If I press the plus button while typing fast, the error sometimes occurs.

Stacktrace & log output

2023-08-07 21:24:05.203359-0400 TestFocus[28018:1317372] *** Terminating app due to uncaught exception 'RLMException', reason: 'Cannot register notification blocks from within write transactions.'
*** First throw call stack:
(0x1ce0c948c 0x1c7421050 0x10128395c 0x101104638 0x1012ff7fc 0x10131a6b4 0x10131a7f0 0x10140ce2c 0x10140d480 0x1d17d363c 0x1d17b2364 0x1d17be934 0x1d17fb804 0x1d17bb7ac 0x1d1cb0380 0x1efafcef8 0x1efafc5c0 0x1efafb478 0x1d178cf04 0x1d23f2ffc 0x1d17b7ba0 0x1d1794514 0x1d18a7754 0x1d179502c 0x1d6044edc 0x1d6035ca8 0x10131ddf4 0x10131dd10 0x10108a3e0 0x10108a24c 0x1016d96e8 0x1016d9638 0x1016cc900 0x1016cc828 0x1016cd440 0x101719db0 0x101719a1c 0x1016f9d64 0x101822860 0x101823df4 0x101284340 0x10128430c 0x1013d9268 0x1013d90bc 0x101408a2c 0x10141d8c4 0x10141f14c 0x100ffbc98 0x1d1f48aa8 0x1d1f48f90 0x1d1f48eb8 0x1d18328d0 0x1d17a4b50 0x1d18328d0 0x1d204db94 0x1d1788f90 0x1d178583c 0x1d27aa7c4 0x1d1788504 0x1d178abac 0x1d010b748 0x1d0108418 0x1d098a190 0x1d0146cdc 0x1d014b5e8 0x1d014a8e4 0x1d0148b58 0x1d018e524 0x1d046e884 0x1ce189154 0x1ce194dc8 0x1ce12009c 0x1ce1351b8 0x1ce139da0 0x204c4b998 0x1d03cefd8 0x1d03cec50 0x1d18f54f0 0x1d186f7ac 0x1d185c7fc 0x10100a8a4 0x10100a950 0x1eb87f344)
libc++abi: terminating due to uncaught exception of type NSException
(lldb)

Can you reproduce the bug?

Always

Reproduction Steps

Click on the “+” button on the top right repeatedly until the error occurs (on a “iPhone 14 Pro Max - iOS 16.4” emulator) Optionally, type at the same time (in this case, the plus button doesn’t need to be pressed as quickly)

struct ContentView: View {
    
    @FocusState var focusedId: ObjectId?
    
    var body: some View {
        NavigationStack {
            ItemsView2(focusedId: $focusedId)
                .navigationTitle("Items")
        }
    }
}
class Item: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: ObjectId
    @Persisted var name: String
}

struct ItemsView2: View {
    @ObservedResults(Item.self) var items
    var focusedId: FocusState<ObjectId?>.Binding

    var body: some View {
        List {
            // Items
            ForEach(items) { item in
                ItemView(item: item, focusedId: focusedId)
            }
            .onDelete(perform: $items.remove)
        }
        .toolbar {
            // Add
            Button {
                let item = Item()
                item.name = "Item"
                $items.append(item)
                focusedId.wrappedValue = item._id
            } label: {
                Label("Add", systemImage: "plus")
            }
        }
    }
}
struct ItemView: View {
    @ObservedRealmObject var item: Item
    
    var focusedId: FocusState<ObjectId?>.Binding
    
    var body: some View {
        TextField("Item", text: $item.name)
            .focused(focusedId, equals: item._id) // focus
    }
}

Version

13.17.1

What Atlas Services are you using?

Local Database only

Are you using encryption?

No

Platform OS and version(s)

macOS 13.4.1

Build environment

Xcode version: 14.3.1 Dependency manager and version: …

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 32 (2 by maintainers)

Most upvoted comments

We usually encounter this when trying to start change observation as a result of a write, but it does seem to be limited to situations where the presentation mode is changing. The most common occurrence for us is when a button is clicked that:

  • dismisses a sheet
  • writes some data to realm

We are also encountering this issue. When asnyWrite (or normal writes, issue happens either way) are triggered in rapid succession (in our case caused by quick server requests) and there is a notification block observing this collection (no writes in the notification block). This happens even with everything realm on the @MainActor.

I tried asyncWrite based on this comment, but it didn’t make a difference.

struct ContentView2: View {
    
    @ObservedResults(Item.self) var items
    @Environment(\.realm) var realm

    @FocusState var focusedId: ObjectId?

    var body: some View {
        NavigationStack {
            List {
                // Items
                ForEach(items) { item in
                    ItemView(item: item, focusedId: $focusedId)
                        .onSubmit {
                            addItem()
                        }
                }
            }
            .navigationTitle("Items")
            .toolbar {
                // Add
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        addItem()
                    } label: {
                        Label("Add item", systemImage: "plus")
                    }
                }
                
                // Delete
                ToolbarItem(placement: .navigationBarLeading) {
                    Button(role: .destructive){
                        deleteAll()
                    } label: {
                        Label("Delete all", systemImage: "trash")
                    }
                }
            }
        }
    }
    
    func addItem(){
        Task {
            try await realm.asyncWrite {
                let item = Item()
                realm.add(item)
                focusedId = item._id
            }
        }
    }
    
    func deleteAll(){
        $items.remove(atOffsets: IndexSet(integersIn: items.indices))
    }
}

Affirm. We only use asyncWrite actually, and still see this bug. All of our realms are used with actor isolation.

This no longer happens for us when performing all writes on a (single) background actor. We use a global actor for this like so:

@globalActor
actor RealmBackgroundActor {
    static var shared = RealmBackgroundActor()
}

We also observe on this actor only, and use exclusively asyncWrite though I believe we have also tested with normal write and it did also work.

So we observe using:

@RealmBackgroundActor
func observeCollections() async throws {
        let realm = try await Realm(actor: RealmBackgroundActor.shared)
        let results = realm.objects(SomeProtocol.self)
        resultNotificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
            self?.objects = Array(results.sorted(by: \.createdAt).freeze())
        }
}

and write using:

@RealmBackgroundActor
func write(object: SomeProtocol) async throws {
        let realm = try await Realm(actor: RealmBackgroundActor.shared)
        try await realm.asyncWrite {
            realm.create(type(of: object), value: object, update: .modified)
        }
    }

Hope this helps someone:)

There’s currently no debouncing. We looked into adding it and concluded that it was going to be sufficiently complicated that we might as well put the effort into making small writes work better instead.

Write transactions can’t happen “at the same time” as they involve a global (per-Realm) lock. However, in that code you’re checking for the max outside of a write transaction, and so may be reading a stale value and end up with a duplicate. Wrapping the whole thing in an explicit write transaction is required for correct results:

realm.write {
    let order = (items.max(of: \Item.order) ?? 0) + 1
    let item = Item(name: name, order: order, userId: app.currentUser?.id)
    $items.append(item)
}

Async writes are the mechanism to queue writes. If you call asyncWrite from within a write transaction the block will be invoked after the current write transaction completes.

@bogdan-pechounov I was able to reproduce your issue. The error mentioned on this issue can be a little bit misgiving, this is not happening because you are registering a notification in a write transaction, is because your write is happening while the realm is in a transaction, which we verify later during the write commit and throw an exception. @jeffypooo do you have the same use case as the one described above, do you get this error while on SwiftUI View while using ObservedResults and ObservedRealmObject