Hey,
I'm currently trying to set up a Realm database observation that's not isolated to the Main Actor. This is proving to be quite a challenge (at least for me) because I've been running into tons of Concurrency warnings, most of which I don't quite understand. The context of this question revolves around Realm, but the underlying problem/topic is all about Swift Concurrency and actors, so I hope this forum is the appropriate place to ask.
To start, I have created a global actor BackgroundActor
:
@globalActor public actor BackgroundActor: GlobalActor {
public static let shared = BackgroundActor()
}
I use this to open/initialize a realm instance that's not isolated to the MainActor
but to this BackgroundActor
:
let realm = try await Realm(actor: BackgroundActor.shared)
Then, I built a small wrapper around RealmCollection.observe()
(which only offers a closure based observation), that is isolated to that global BackgroundActor
and returns an AsyncThrowingStream
.
extension RealmCollection where Element: ObjectBase {
@BackgroundActor func observe() -> AsyncThrowingStream<Void, Error> {
.init { continuation in
let token = observe { change in
switch change {
case .initial, .update:
continuation.yield()
case let .error(error):
continuation.finish(throwing: error)
}
}
continuation.onTermination = { _ in
token.invalidate()
}
}
}
}
With this, I want to observe some database queries, like this:
// Class is isolated
@BackgroundActor public class MyIsolatedClass {
let realm: Realm
init() { /* ... */ } // Initialize realm
func observeDatabase() async throws {
let results = realm.objects(ThoughtDTO.self).where { /* ... */ }
for try await _ in results.observe() {
print("Something changed, results object is now updated")
}
}
}
I use a Void
yielding AsyncStream
, because the Realm
objects themselves are not Sendable
and I could not get it to work without a multitude of Concurrency warnings, so I figured I'd just send "empty signals" to know when the data has changed (in the example above, results
will always be updated by Realm, so I just need to know when something changed).
There are some ways to send Realm objects between actors through some thread safe references but that seems strange, since I don't believe I'm ever actually leaving the @BackgroundActor
(thought I'm likely wrong).
The problem with the above example is the following warning I get on the for try await
loop:
Warning: Passing argument of non-sendable type 'inout AsyncStream.Iterator' outside of global actor 'BackgroundActor'-isolated context may introduce data races
This warning appears when using Swift 5.10 (in Xcode 15.3 Beta 1). Though I have no idea why it appears. Why is the iterator outside of the @BackgroundActor
and how can I fix this?
To illustrate it better, I have a minimal reproducing example here:
@BackgroundActor public func observe() -> AsyncStream<Void> {
.init { continuation in
continuation.yield()
}
}
@BackgroundActor public class MyIsolatedClass {
func doStuff() async throws {
for await _ in observe() { // Warning: Passing argument of non-sendable type 'inout AsyncStream<Void>.Iterator' outside of global actor 'BackgroundActor'-isolated context may introduce data races
print("Yielded")
}
}
}
My understanding around global actors is not too great, I'd love to understand what is happening here any why this might be a problem.
I'd appreciate any help