realm-swift
realm-swift copied to clipboard
Thaw not usable with actor-based Realms
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:
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
Why is a frozen object being passed, frozen and thawed when using .asyncWrite? I don't believe that's necessary
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: @.***>
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.
So.
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.
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 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?
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.
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.
a use case for thaw vs threadsafereference is when using some ...
types, threadsafereference reference is unusable but freeze/thaw still works