realm-swift icon indicating copy to clipboard operation
realm-swift copied to clipboard

Thaw not usable with actor-based Realms

Open ldomaradzki opened this issue 8 months ago • 9 comments

How frequently does the bug occur?

Always

Description

I am getting Realm accessed from incorrect thread fatal error crash when using .thaw-ed object in actor-based Realm write block. Looking at the Realm source code (obj-c part of Realm) it seems that your code doesn't take into account from which Realm we call .thaw on object and uses it's internal .realm property (so in my example that would be main thread). Workaround is to grab that object in actor-based context e.g. from its primary key, but it gets problematic with embedded objects or lists/results (which can also be frozen).

Screenshot of the crash: IMG_9104

Stacktrace & log output

No response

Can you reproduce the bug?

Always

Reproduction Steps

Code to recreate this issue:

import RealmSwift

class TestClass {
    let config = Realm.Configuration(inMemoryIdentifier: "inMemory", schemaVersion: 1, deleteRealmIfMigrationNeeded: true, objectTypes: [TestObject.self])
    
    init() {
        let realm = try! Realm(configuration: config)
        
        let newTest = TestObject()
        newTest.identifier = "id1"
        newTest.name = "name1"

        try! realm.write({
            realm.add(newTest)
        })
        
        let fetchNewTest = realm.objects(TestObject.self).first!
        
        Task { [self] in
            try await mutatingFunction(objectToChange: fetchNewTest.freeze())
        }
    }
    
    @MyGlobalActor
    func mutatingFunction(objectToChange: TestObject) async throws {
        let actorRealm = try await Realm(configuration: config, actor: MyGlobalActor.shared)
        let unthawed = actorRealm.thaw(objectToChange)
        
        try await actorRealm.asyncWrite {
            unthawed?.name = "new name"
        }
    }
}

class TestObject: Object {
    @Persisted(primaryKey: true) var identifier: String = ""
    @Persisted var name: String = ""
}

@globalActor actor MyGlobalActor: GlobalActor {
    static var shared = MyGlobalActor()
}

Version

10.44.0

What Atlas Services are you using?

Local Database only

Are you using encryption?

No

Platform OS and version(s)

17.0

Build environment

Xcode version: 15.0 Dependency manager and version: SPM

ldomaradzki avatar Nov 01 '23 06:11 ldomaradzki

Why is a frozen object being passed, frozen and thawed when using .asyncWrite? I don't believe that's necessary

Jaycyn avatar Nov 01 '23 17:11 Jaycyn

Its just to showcase the crash. We use frozen objects for UI stuff and we want to offload most writes to actor based realms, but with latest SDK this crashes the application.

W dniu śr., 1.11.2023 o 18:10 Jay @.***> napisał(a):

Why is a frozen object being passed and thawed when using .asyncWrite? I don't believe that's necessary

— Reply to this email directly, view it on GitHub https://github.com/realm/realm-swift/issues/8409#issuecomment-1789334594, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABAHIITXWCBTAKHPLETNSMLYCJ66PAVCNFSM6AAAAAA6Y2EBDSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTOOBZGMZTINJZGQ . You are receiving this because you authored the thread.Message ID: @.***>

ldomaradzki avatar Nov 01 '23 17:11 ldomaradzki

Hmm. Realm objects are not sendable so this message may be of importance

Passing argument of non-sendable type 'RealmSwiftObject' into global actor 'MyGlobalActor'-isolated context may introduce data races

Interestingly enough, I copy and pasted your code into a project and found the crash happens in a different spot. I added two print statements to isolate the line, but it essentially crashes here

try await mutatingFunction(objectToChange: fetchNewTest.freeze())

before going into the mutatingFunction. It actually crashes on the freeze() function.

Freeze

So.

Jaycyn avatar Nov 01 '23 20:11 Jaycyn

It crashes because you already switched to a different context (async Task) and original Realm object was created on main thread. You can check how it behaves by changing it e.g. this way:

// ...
// .freeze() is still done on main thread Realm
let fetchNewTest = realm.objects(TestObject.self).first!.freeze()
        
Task { [self] in
    // we already pass frozen object so it doesnt care about being inside of `Task` context
    try await mutatingFunction(objectToChange: fetchNewTest)
}

For me this is still an issue, because documentation doesn't mention anything about limitations for freeze/thaw and actor-based Realms.

ldomaradzki avatar Nov 01 '23 20:11 ldomaradzki

Passing frozen objects between threads or actors is legal, but this isn't something we can express for sendability checking since frozen objects and live objects currently have the same static type. You have to freeze before doing anything which would switch threads, though.

The implementation of thaw() is currently weird and it'll always give you an object confined to the current thread even if you call it on a Realm isolated to an actor or queue. ThreadSafeReference does not have the same problem, so ThreadSafeReference(to: obj).resolve(in: realm) is a roundabout but correct version of realm.thaw(obj).

tgoyne avatar Nov 01 '23 20:11 tgoyne

@tgoyne thanks for checking this issue. I tested approach with using ThreadSafeReference and it works. Are there plans to "fix" .thaw() in future release? Should this limitation be documented somewhere? Should I close this issue?

ldomaradzki avatar Nov 02 '23 08:11 ldomaradzki

I've made a ticket to add details around this to the documentation. I hope to get the relevant pages updated in the next week or two.

dacharyc avatar Nov 02 '23 14:11 dacharyc

Oh, sorry, the public API is realm.resolve(ThreadSafeReference(to: obj)). I think we needed the extra wrapper function to get type inference with older versions of Swift.

We should fix realm.thaw(), and it shouldn't be too hard to do.

tgoyne avatar Nov 02 '23 15:11 tgoyne

a use case for thaw vs threadsafereference is when using some ... types, threadsafereference reference is unusable but freeze/thaw still works

aehlke avatar Dec 18 '23 21:12 aehlke