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

(DOCSP-33483): Swift: Actor and Concurrency updates and fixes #3072

Merged
merged 11 commits into from
Nov 14, 2023
4 changes: 3 additions & 1 deletion .github/workflows/readability.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ jobs:
uses: actions/checkout@v3
- name: Get changed files.
id: changed-files
uses: tj-actions/[email protected]
uses: tj-actions/changed-files@v40
with:
files: source/sdk/**
- name: List changed files (debugging log statement).
run: |
echo ${{ steps.changed-files.outputs.all_changed_files }}
Expand Down
4 changes: 4 additions & 0 deletions config/redirects
Original file line number Diff line number Diff line change
Expand Up @@ -1262,3 +1262,7 @@ raw: ${prefix}/sdk/swift/sync/network-connection -> ${base}/sdk/swift/sync/sync-
# DOCSP-30339: Move Configure & Open a Realm page up a level

raw: ${prefix}/sdk/kotlin/realm-database/realm-files/open-and-close-a-realm -> ${base}/sdk/kotlin/realm-database/open-and-close-a-realm/

# Update Swift Actor support documentation

raw: ${prefix}/sdk/swift/actor-isolated-realm -> ${base}/sdk/swift/use-realm-with-actors/
339 changes: 249 additions & 90 deletions examples/ios/Examples/RealmActor.swift

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions examples/ios/RealmExamples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = minimal;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealmExamplesHostApp.app/RealmExamplesHostApp";
Expand All @@ -1053,6 +1054,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.mongodb.docs.RealmExamples;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "";
SWIFT_STRICT_CONCURRENCY = minimal;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/RealmExamplesHostApp.app/RealmExamplesHostApp";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,6 @@
ReferencedContainer = "container:RealmExamples.xcodeproj">
</BuildableReference>
<SkippedTests>
<Test
Identifier = "RealmActorTests/testActorIsolatedRealmDelete()">
</Test>
<Test
Identifier = "RealmActorTests/testActorIsolatedRealmUpdate()">
</Test>
<Test
Identifier = "RealmActorTests/testObserveCollectionOnActor()">
</Test>
<Test
Identifier = "RealmActorTests/testObserveObjectOnActor()">
</Test>
<Test
Identifier = "Sync/testSetCustomLogger()">
</Test>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,25 @@ actor RealmActor {
}
}

func getTodoOwner(forTodoNamed name: String) -> String {
let todo = realm.objects(Todo.self).where {
$0.name == name
}.first!
return todo.owner
}

struct TodoStruct {
var id: ObjectId
var name, owner, status: String
}

func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct {
let todo = realm.objects(Todo.self).where {
$0.name == name
}.first!
return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status)
}

func updateTodo(_id: ObjectId, name: String, owner: String, status: String) async throws {
try await realm.asyncWrite {
realm.create(Todo.self, value: [
Expand All @@ -32,9 +51,10 @@ actor RealmActor {
}
}

func deleteTodo(todo: Todo) async throws {
func deleteTodo(id: ObjectId) async throws {
try await realm.asyncWrite {
realm.delete(todo)
let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id)
realm.delete(todoToDelete!)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
func deleteTodo(todo: Todo) async throws {
func deleteTodo(id: ObjectId) async throws {
try await realm.asyncWrite {
realm.delete(todo)
let todoToDelete = realm.object(ofType: Todo.self, forPrimaryKey: id)
realm.delete(todoToDelete!)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
let actor = try await RealmActor()
let todoId = await actor.getObjectId(forTodoNamed: "Keep Mr. Frodo safe from that Gollum")

let todo = try await createObject(in: actor)
print("Successfully created an object with id: \(todo._id)")
let todoCount = await actor.count

try await actor.deleteTodo(todo: todo)
try await actor.deleteTodo(id: todoId)
let updatedTodoCount = await actor.count
if updatedTodoCount == todoCount - 1 {
print("Successfully deleted the todo")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@MainActor
func mainThreadFunction() async throws {
// Create an object in an actor-isolated realm.
let actor = try await RealmActor()
try await actor.createTodo(name: "Leave the ring on the mantle", owner: "Bilbo", status: "In Progress")

// Get information as a struct or other Sendable type.
let todoAsStruct = await actor.getTodoAsStruct(forTodoNamed: "Leave the ring on the mantle")
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,65 @@
let actor = try await RealmActor()

// Add a todo to the realm so the collection has something to observe
try await actor.createTodo(name: "Arrive safely in Bree", owner: "Merry", status: "In Progress")
let todoCount = await actor.count
print("The actor currently has \(todoCount) tasks")

// Get a collection
let todos = await actor.realm.objects(Todo.self)

// Register a notification token, providing the actor
let token = await todos.observe(on: actor, { actor, changes in
print("A change occurred on actor: \(actor)")
switch changes {
case .initial:
print("The initial value of the changed object was: \(changes)")
case .update(_, let deletions, let insertions, let modifications):
if !deletions.isEmpty {
print("An object was deleted: \(changes)")
} else if !insertions.isEmpty {
print("An object was inserted: \(changes)")
} else if !modifications.isEmpty {
print("An object was modified: \(changes)")
// Create a simple actor
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()

public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws {
let realm = try! Realm()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you can put functions on a global actor, it'd be more common to have this be a top-level @BackgroudActor func, and would serve better as documentation for global actor vs regular actor.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've simplified a few of these examples to remove the global actor aspect. Is this more in line with what you had in mind, @tgoyne ?

try realm.write {
// Resolve the thread safe reference on the Actor where you want to use it.
// Then, do something with the object.
let todoOnActor = realm.resolve(tsr)
realm.delete(todoOnActor!)
}
case .error(let error):
print("An error occurred: \(error.localizedDescription)")
}
})

// Make an update to an object to trigger the notification
await actor.realm.writeAsync {
todos.first!.status = "Completed"
}

// Invalidate the token when done observing
token.invalidate()
// Execute some code on a different actor - in this case, the MainActor
@MainActor
func mainThreadFunction() async throws {
let realm = try! await Realm()

// Create a todo item so there is something to observe
try await realm.asyncWrite {
return realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: return isn't doing anything here

"name": "Arrive safely in Bree",
"owner": "Merry",
"status": "In Progress"
])
}

// Get the collection of todos on the current actor
let todoCollection = realm.objects(Todo.self)

// Register a notification token, providing the actor where you want to observe changes.
// This is only required if you want to observe on a different actor.
let token = await todoCollection.observe(on: BackgroundActor.shared, { actor, changes in
print("A change occurred on actor: \(actor)")
switch changes {
case .initial:
print("The initial value of the changed object was: \(changes)")
case .update(_, let deletions, let insertions, let modifications):
if !deletions.isEmpty {
print("An object was deleted: \(changes)")
} else if !insertions.isEmpty {
print("An object was inserted: \(changes)")
} else if !modifications.isEmpty {
print("An object was modified: \(changes)")
}
case .error(let error):
print("An error occurred: \(error.localizedDescription)")
}
})

// Update an object to trigger the notification.
// This example triggers a notification that the object is deleted.
// We can pass a thread-safe reference to an object to update it on a different actor.
let todo = todoCollection.where {
$0.name == "Arrive safely in Bree"
}.first!
let threadSafeReferenceToTodo = ThreadSafeReference(to: todo)
try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)

// Invalidate the token when done observing
token.invalidate()
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,43 @@
let actor = try await RealmActor()

// Add a todo to the realm so we can observe it
try await actor.createTodo(name: "Scour the Shire", owner: "Merry", status: "In Progress")
let todoCount = await actor.count
print("The actor currently has \(todoCount) tasks")

// Get an object
let todo = await actor.realm.objects(Todo.self).where {
$0.name == "Scour the Shire"
}.first!
// Create a simple actor
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()
}

// Register a notification token, providing the actor
let token = await todo.observe(on: actor, { actor, change in
print("A change occurred on actor: \(actor)")
switch change {
case .change(let object, let properties):
for property in properties {
print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'")
// Execute some code on a different actor - in this case, the MainActor
@MainActor
func mainThreadFunction() async throws {
// Create a todo item so there is something to observe
let realm = try! await Realm()
let scourTheShire = try await realm.asyncWrite {
return realm.create(Todo.self, value: [
"_id": ObjectId.generate(),
"name": "Scour the Shire",
"owner": "Merry",
"status": "In Progress"
])
}

// Register a notification token, providing the actor
let token = await scourTheShire.observe(on: BackgroundActor.shared, { actor, change in
print("A change occurred on actor: \(actor)")
switch change {
case .change(let object, let properties):
for property in properties {
print("Property '\(property.name)' of object \(object) changed to '\(property.newValue!)'")
}
case .error(let error):
print("An error occurred: \(error)")
case .deleted:
print("The object was deleted.")
}
case .error(let error):
print("An error occurred: \(error)")
case .deleted:
print("The object was deleted.")
})

// Update the object to trigger the notification.
// This triggers a notification that the object's `status` property has been changed.
try await realm.asyncWrite {
scourTheShire.status = "Complete"
}
})

// Make an update to an object to trigger the notification
await actor.realm.writeAsync {
todo.status = "Completed"

// Invalidate the token when done observing
token.invalidate()
}

// Invalidate the token when done observing
token.invalidate()
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
struct TodoStruct {
var id: ObjectId
var name, owner, status: String
}

func getTodoAsStruct(forTodoNamed name: String) -> TodoStruct {
let todo = realm.objects(Todo.self).where {
$0.name == name
}.first!
return TodoStruct(id: todo._id, name: todo.name, owner: todo.owner, status: todo.status)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@MainActor
func mainThreadFunction() async throws {
// Create an object in an actor-isolated realm.
// Pass primitive data to the actor instead of
// creating the object here and passing the object.
let actor = try await RealmActor()
try await actor.createTodo(name: "Prepare fireworks for birthday party", owner: "Gandalf", status: "In Progress")

// Later, get information off the actor-confined realm
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might just be a snooty glitch but this doesn't format as a comment in the staged build, just fyi

let todoOwner = await actor.getTodoOwner(forTodoNamed: "Prepare fireworks for birthday party")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// We can pass a thread-safe reference to an object to update it on a different actor.
let todo = todoCollection.where {
$0.name == "Arrive safely in Bree"
}.first!
let threadSafeReferenceToTodo = ThreadSafeReference(to: todo)
try await BackgroundActor.shared.deleteTodo(tsrToTodo: threadSafeReferenceToTodo)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// A simple example of a custom global actor
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()
}

@BackgroundActor
func createObjectOnBackgroundActor() async throws -> ObjectId {
// Explicitly specifying the actor is required for anything that is not MainActor
let realm = try await Realm(actor: BackgroundActor.shared)
let newTodo = try await realm.asyncWrite {
return realm.create(Todo.self, value: [
"name": "Pledge fealty and service to Gondor",
"owner": "Pippin",
"status": "In Progress"
])
}
// Share the todo's primary key so we can easily query for it on another actor
return newTodo._id
}

@MainActor
func mainThreadFunction() async throws {
let newTodoId = try await createObjectOnBackgroundActor()
let realm = try await Realm()
let todoOnMainActor = realm.object(ofType: Todo.self, forPrimaryKey: newTodoId)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
let actor = try await RealmActor()

try await createObject(in: actor)

let todo = await actor.realm.objects(Todo.self).where {
$0.name == "Keep it safe"
}.first!
// Read objects in functions isolated to the actor and pass primitive values to the caller
func getObjectId(in actor: isolated RealmActor, forTodoNamed name: String) async -> ObjectId {
let todo = actor.realm.objects(Todo.self).where {
$0.name == name
}.first!
return todo._id
}
let objectId = await getObjectId(in: actor, forTodoNamed: "Keep it safe")
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@globalActor actor BackgroundActor: GlobalActor {
static var shared = BackgroundActor()

public func deleteTodo(tsrToTodo tsr: ThreadSafeReference<Todo>) throws {
let realm = try! Realm()
try realm.write {
// Resolve the thread safe reference on the Actor where you want to use it.
// Then, do something with the object.
let todoOnActor = realm.resolve(tsr)
realm.delete(todoOnActor!)
}
}
}
Loading
Loading