Meet distributed actors in Swift
Description: Discover distributed actors — an extension of Swift’s actor model that simplifies development of distributed systems. We'll explore how distributed actor isolation and location transparency can help you avoid the accidental complexity of networking, serialization, and other transport concerns when working with distributed apps and systems. To get the most out of this session, watch “Protect mutable state with Swift actors” from WWDC21.
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.id
s are assigned by the distributed actor system as the actor is initialized, and later managed by that system (we cannot declare or assign theid
property 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
ActorSystem
to be remote - Setup 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
Distributed
module (api docs) - add the
distributed
keyword in front of theactor
keyword, this way your actor will:- conform to
DistributedActor
protocol - enable a number of additional compile time checks
- conform to
- The compiler will asks as to declare which
ActorSystem
our distributed actor can be used with- we can use one of the systems that come with the
Distributed
module, such asLocalTestingDistributedActor
, or define our own - we can declare a module-wide
DefaultDistributedActorSystem
typealias (used by all distributed actors), or anActorSystem
typealias in the body of the specific actor
- we can use one of the systems that come with the
- each
distributed actor
needs to declare anactorSystem
compiler synthesized property - accept an actor system in the initializer, and pass it through to the property - add the
distributed
keyboard to instance methods that you'd like to expose for remote calls- ensure that all
distributed
methods parameters and return values conform to the serialization requirement of the actor system (e.g.,Codable
)
- ensure that all
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
ActorSystem
to be remote
- This step requires you to define your own
ActorSystem
, which will be used in both your app and your server - see sample app for a
SampleWebSocketActorSystem
example
- 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.