-
Notifications
You must be signed in to change notification settings - Fork 88
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
Changes from 7 commits
8df5c8d
327b8a0
7617e69
b110163
fd34162
c8531ae
04ac989
3c65080
6194407
708e46a
16a9ee7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 }} | ||
|
Large diffs are not rendered by default.
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 |
---|---|---|
@@ -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() | ||
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(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
"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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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!) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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 ?