Introduction
Swift Actors
designed to protect you from low-level data races in the same process
compile-time enforced actor isolation checks
Distributed actors
designed to protect you from low-level data races across multiple processes
e.g., communication among multiple devices or servers in a cluster
Distributed actors
By using distributed actors, we’re able to establish a channel between two processes and send messages between them
Distributed actors still isolate their state and still can only communicate using asynchronous messages
It’s ok to have multiple distributed actors in the same process
Distributed actors are just actors, but can participate in remote interactions whenever necessary
Distributed actors always belong to some distributed actor system, which handles all the serialization and networking necessary to perform remote calls
Every distributed actor is assigned an
id, uniquely identify said actor in the entire distributed actor system that it is part of.ids are assigned by the distributed actor system as the actor is initialized, and later managed by that system (we cannot declare or assign theidproperty manually)
Location transparency:
ability to be potentially remote without having to change how we interact with such distributed actor
regardless where a distributed actor is located, we can interact with it the same way
allows us to transparently move our actors, without having to change their implementation
Road to distributed actors
Pick a local actor that you’d like to move to distribute actor
Turn it into a (still local) distributed actor
Move the distributed actor
ActorSystemto be remoteSetup server side app
Example
Pick a local actor that you’d like to move to distribute actor
public actor BotPlayer: Identifiable {
nonisolated public let id: ActorIdentity = .random
var ai: RandomPlayerBotAI
var gameState: GameState
public init(team: CharacterTeam) {
self.gameState = .init()
self.ai = RandomPlayerBotAI(playerID: self.id, team: team)
}
public func makeMove() throws -> GameMove {
return try ai.decideNextMove(given: &gameState)
}
public func opponentMoved(_ move: GameMove) async throws {
try gameState.mark(move)
}
}Turn it into a (still local) distributed actor
import the
Distributedmodule (api docs)add the
distributedkeyword in front of theactorkeyword, this way your actor will:conform to
DistributedActorprotocolenable a number of additional compile time checks
The compiler will asks as to declare which
ActorSystemour distributed actor can be used withwe can use one of the systems that come with the
Distributedmodule, such asLocalTestingDistributedActor, or define our ownwe can declare a module-wide
DefaultDistributedActorSystemtypealias (used by all distributed actors), or anActorSystemtypealias in the body of the specific actor
each
distributed actorneeds to declare anactorSystemcompiler synthesized property - accept an actor system in the initializer, and pass it through to the propertyadd the
distributedkeyboard to instance methods that you’d like to expose for remote callsensure that all
distributedmethods parameters and return values conform to the serialization requirement of the actor system (e.g.,Codable)
import Distributed // 👈🏻 import the distributed module
// 👇🏻 add distributed attribute
public distributed actor BotPlayer: Identifiable {
typealias ActorSystem = LocalTestingDistributedActorSystem // 👈🏻 declare the ActorSystem this actor belongs to
var ai: RandomPlayerBotAI
var gameState: GameState
// 👇🏻 accept the actorSystem during init
public init(team: CharacterTeam, actorSystem: ActorSystem) {
self.actorSystem = actorSystem // 👈🏻 set compiler synthesized property
self.gameState = .init()
self.ai = RandomPlayerBotAI(playerID: self.id, team: team)
}
// 👇🏻 add distributed keyword to instance methods that can be called remotely
public distributed func makeMove() throws -> GameMove {
return try ai.decideNextMove(given: &gameState)
}
public distributed func opponentMoved(_ move: GameMove) async throws {
try gameState.mark(move)
}
}Move the distributed actor
ActorSystemto be remote
This step requires you to define your own
ActorSystem, which will be used in both your app and your serversee sample app for a
SampleWebSocketActorSystemexample
Setup server side app
import Distributed
import TicTacFishShared
/// Stand alone server-side swift application, running our SampleWebSocketActorSystem in server mode.
@main
struct Boot {
static func main() {
let system = try! SampleWebSocketActorSystem(mode: .serverOnly(host: "localhost", port: 8888))
// 👇🏻 this is a pattern in sample app, not an API
system.registerOnDemandResolveHandler { id in
// We create new BotPlayers "ad-hoc" as they are requested for.
// Subsequent resolves are able to resolve the same instance.
if system.isBotID(id) {
return system.makeActorWithID(id) {
OnlineBotPlayer(team: .rodents, actorSystem: system)
}
}
return nil // unable to create-on-demand for given id
}
print("========================================================")
print("=== TicTacFish Server Running on: ws://\(system.host):\(system.port) ==")
print("========================================================")
try await server.terminated // waits effectively forever (until we shut down the system)
}
}Cluster Actor System
Apple has made available a reference, server-side focused, cluster actor system implementation.
