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.

Sample app

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 the id 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

  1. Pick a local actor that you'd like to move to distribute actor
  2. Turn it into a (still local) distributed actor
  3. Move the distributed actor ActorSystem to be remote
  4. Setup server side app

Example

  1. 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)
  }
}
  1. Turn it into a (still local) distributed actor
  • import the Distributed module (api docs)
  • add the distributed keyword in front of the actor keyword, this way your actor will:
    • conform to DistributedActor protocol
    • enable a number of additional compile time checks
  • 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 as LocalTestingDistributedActor, or define our own
    • we can declare a module-wide DefaultDistributedActorSystem typealias (used by all distributed actors), or an ActorSystem typealias in the body of the specific actor
  • each distributed actor needs to declare an actorSystem 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)
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)
  }
}
  1. 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
  1. 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.

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.