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

"Realm already in a write transaction" Exception #4511

Closed
TimOliver opened this issue Jan 5, 2017 · 7 comments
Closed

"Realm already in a write transaction" Exception #4511

TimOliver opened this issue Jan 5, 2017 · 7 comments
Assignees
Labels

Comments

@TimOliver
Copy link
Contributor

Reported on the iOS Folks Slack, a user came across a potential regression in Realm's change notification feature.

A change notification block is created for responding to when a Results object changes. That block then also contains a write transaction that further modifies the data.

In earlier versions of Realm, this was fine. However in the latest version of Realm, this now produces a "Realm already in a write transaction" exception.

Goals

To successfully perform a write transaction inside a change notification block.

Expected Results

The write transaction to successfully execute.

Actual Results

Terminating app due to uncaught exception 'RLMException', reason: 'The Realm is already in a write transaction'

Code Sample

The following code sample will 100% trigger the exception when executed:

class SomeObject: Object {
    dynamic var isHappy = false
    dynamic var happinessVerified = false
    dynamic var text = ""
}

class RealmCrash {
    class func crash() {
        let realm = try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "Memory"))
        let token = realm.objects(SomeObject.self).filter("isHappy == true").addNotificationBlock { results in
            switch results {
            case .error(let error):
                print("error: \(error)")
            case .update(let list, deletions: _, insertions: _, modifications: _):
                print("update on: \(list)")
                try! realm.write {
                    for object in list {
                        object.happinessVerified = true
                    }
                }
            case .initial(let list):
                print("initial: \(list)")
            }
        }
        
        let originalObject = SomeObject()
        originalObject.text = "Hello"
        
        try! realm.write {
            realm.add(originalObject)
        }
        
        print("created object")
        
        try! realm.write {
            originalObject.isHappy = true
        }
        
        try! realm.write {
            originalObject.text = "cat"
        }
        
        print("mutated")
    }
}

A sample app demonstrating this code is also available: RealmWriteTest.zip

Version of Realm and Tooling

Realm version: Swift 2.1.2

Xcode version: 8.2

iOS/OSX version: 10.2

Dependency manager + version: CocoaPods 1.1.1

@tgoyne
Copy link
Member

tgoyne commented Jan 5, 2017

This is working as intended. The thing that changed is that previously the notification would have been simply discarded entirely when beginWrite() advanced the read version, while now the notification is called from within the write transaction. Checking if the realm is in a write transaction from within the notification block and returning immediately if so would give back the old behavior (which based on the sample code is almost certainly not actually what the user wants).

@zacwest
Copy link

zacwest commented Jan 6, 2017

I guess I'm confused about the rules when it's safe to do a write transaction from a notification, since I am occasionally in a write transaction already or I'm not in one. I'm using the notifications to transition objects between states (e.g. for network activity) which feels like a pretty common non-UI use-case for notifications.

I've ended up writing something like:

extension Realm {
    public func safeWrite(_ block: (() throws -> Void)) throws {
        if isInWriteTransaction {
            try block()
        } else {
            try write(block)
        }
    }
}

Is this the right path to take? I wish the notifications would happen before the write transaction occurs, always, so I can be entirely sure the behavior that's going to happen.

@tgoyne
Copy link
Member

tgoyne commented Jan 6, 2017

If you never cancel write transactions made on the same thread as your notification block is on then just making the changes in the existing write transaction is a reasonable way to handle it (if you do cancel writes, then the changes made in the notification block would be rolled back as well). If possible you probably want non-UI-related notifications which are making writes to be running in their own thread under your control, which would make this requirement not too bad to enforce.

Sending the notifications before beginning the write transaction doesn't really work. We could implement logic like the following:

  1. Advance to latest version
  2. Send notifications
  3. Acquire write lock
  4. Check if we're still actually at the latest version
  5. If not, release write lock and go to 1

This has a few issues. It's prone to starvation if you have another thread making a steady stream of writes, since in the time it takes to calculate and send the notifications for one write transaction the other thread could have reacquired the write lock and begun another write transaction and then this thread never actually gets to do its own write. More importantly, it also runs into the problem that calling beginWrite() from within the notification would need to produce nested notifications, which breaks awkwardly once you have more than one notification block for the Realm.

One thing we could do is make the Realm pretend it isn't a in a write transaction while calling the notification block. Essentially all it'd have to do is ignore the first call to beginWrite(), and then if the notification left it not in a write transaction begin a new one before returning. This would cause problems with multiple notification blocks (if the first makes a write, the second would be called with the Realm at the version resulting from that write, but with reported changes that don't match that), but would make the simple cases just work.

@evgeny-sureev
Copy link

I had this issue too.

It must be stated in documentation that notifications may come inside write transaction and it is user's responsibility to check realm.isInWriteTransaction before attempting write.

@pigeondotdev
Copy link

@evgeny-sureev's last comment summarizes what needs to be done to correct this issue. Check realm.isInWriteTransaction before attempting a write transaction when you're in a notification block. I'm going to file another issue to start a discussion about adding a note for this into the documentation somewhere. Closing this issue.

@zzdravkin
Copy link

Is this safe?
if you pass realm.add(object) and if it's done in try block(), you will get error "Can only add, remove, or create objects in a Realm in a write transaction - call beginWriteTransaction on an RLMRealm instance first."

extension Realm {
public func safeWrite(_ block: (() throws -> Void)) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}
}

@aehlke
Copy link

aehlke commented Jan 11, 2024

@zzdravkin did you figure it out? I just ran into the error you mentioned, even though it's inside asyncWrite

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 14, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

9 participants