From e895266fbbed5447365778fb51ac1414e375476f Mon Sep 17 00:00:00 2001 From: Konrad Malawski Date: Mon, 16 May 2022 22:23:44 -0700 Subject: [PATCH 1/6] Start sketching content about distributed actors. Konrad originally drafted this content in RST, in May 2022. Alex converted it to markdown without preserving the Git history, because that history was only one commit that added this content. The original parent commit was ed422ae5398e4cad50e4827f95919d2536d34982. --- TSPL.docc/LanguageGuide/Concurrency.md | 77 ++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/TSPL.docc/LanguageGuide/Concurrency.md b/TSPL.docc/LanguageGuide/Concurrency.md index 665292571..4b387b8b5 100644 --- a/TSPL.docc/LanguageGuide/Concurrency.md +++ b/TSPL.docc/LanguageGuide/Concurrency.md @@ -990,6 +990,83 @@ This guarantee is known as *actor isolation*. print(await logger.getMax()) --> +## Distributed Actors + +The `Distributed` module, +included with Swift, +provides a number of features which can be used +to make use of Swift actors in distributed systems. + +*Distributed actors* are an extension of Swift's actors +that enable them to be used in distributed systems. + +Similar to actors, they make use of actor isolation, +however unlike them, +they must assume that a distributed actor +may actually be located on a different host, +and therefore are slightly more restrictive in isolation checking +than plain actors. + +An instance of distributed actor type is --- +at compile time --- +assumed to be "potentially remote", +meaning that strong isolation checks are applied to accesses performed on it, +and only `distributed` methods may be called on it. + +### Distributed Actor Isolation + +Distributed actors cannot declared `nonisolated` *stored* properties, +as it is not possible to implement such property +for the case when such actor is "remote". + +Distributed actors can declare `nonisolated` computed properties and functions, +and those work the same way as they would on normal actors, +meaning that they cannot access any of the actor's isolated state. +In the case if a distributed actor, +this effectively means that they can only access +the `actorSystem` and `id` synthesized non-isolated properties of the actor, +or any other nonisolated declarations on the actor. + +### Distributed Methods + +You may make a normal actor method declaration into a distributed method +by prefixing the `func` keyword with the `distributed` contextual keyword, +similar as one prefixes an `actor` to obtain a `distributed actor`. + +Only distributed actors are allowed to declare distributed methods, +and they must be instance methods +(i.e. `static` distributed methods are not allowed). +Computed read-only properties may also be distributed, +and function effectively the same as if +they were no argument taking distributed methods. +However, writable computed properties are note allowed. + +Distributed methods may be called on distributed actor instances at any time, +even as (or rather, especially when) the actor is potentially remote. + +Distributed method invocations are implicitly asynchronous, +same as usual actor calls, +when performed cross actor. +Unlike plain actor methods, +they are also implicitly throwing when the potential of +crossing a network boundary via such call exists, e.g.: + +```swift +let logger: DistributedTemperatureLogger // potentially remote distributed temp. logger +print(try await logger.max) // a remote call might be made here, thus implicitly throwing and async +// Prints "25" +``` + +Since the `DistributedTemperatureLogger` in this snippet +is a `distributed actor` +and we don't know if it is actually local or remote, +the type-system needs to force us to handle the potential remote call, +which might throw. + +In contrast, +in code where the distributed actor instance is "known to be local" +the implicit `throws` effect is not applied. + ## Sendable Types Tasks and actors let you divide a program From 03c7e8daca299678fbc476e468eb8016ca2038e5 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Fri, 26 Jul 2024 16:54:49 +0900 Subject: [PATCH 2/6] Add Distributed module documentation --- .git-order-file | 1 + TSPL.docc/LanguageGuide/Distributed.md | 671 ++++++++++++++++++++ TSPL.docc/The-Swift-Programming-Language.md | 1 + 3 files changed, 673 insertions(+) create mode 100644 TSPL.docc/LanguageGuide/Distributed.md diff --git a/.git-order-file b/.git-order-file index 02fafecd3..f2589bec9 100644 --- a/.git-order-file +++ b/.git-order-file @@ -37,6 +37,7 @@ TSPL.docc/LanguageGuide/Deinitialization.md TSPL.docc/LanguageGuide/OptionalChaining.md TSPL.docc/LanguageGuide/ErrorHandling.md TSPL.docc/LanguageGuide/Concurrency.md +TSPL.docc/LanguageGuide/Distributed.md TSPL.docc/LanguageGuide/Macros.md TSPL.docc/LanguageGuide/TypeCasting.md TSPL.docc/LanguageGuide/NestedTypes.md diff --git a/TSPL.docc/LanguageGuide/Distributed.md b/TSPL.docc/LanguageGuide/Distributed.md new file mode 100644 index 000000000..1a26874a1 --- /dev/null +++ b/TSPL.docc/LanguageGuide/Distributed.md @@ -0,0 +1,671 @@ +# Distributed + +Distributed computing with actors. + +Swift has built-in support for building distributed systems using the actor model. + +The *Distributed* module extends Swift's "local" actor model, providing +the ability to natively express that a piece of code may not be located in +the same process as the calling code. This is expressed using the `distributed` +keyword, which can be used with actor and function declarations. + +Similar to actors, they make use of actor isolation, +however unlike them, +they must assume that a distributed actor +may actually be located on a different host, +and therefore are slightly more restrictive in isolation checking +than plain actors. + +An instance of distributed actor type is --- +at compile time --- +assumed to be "potentially remote", +meaning that strong isolation checks are applied to accesses performed on it, +and only `distributed` methods may be called on it. + +> Note: Swift's distributed actor model features an unique bring-your-own-runtime approach, +> meaning that there exist various specialized transport implementations (distributed actor systems) +> which are specialized for specific transports (e.g. network, ipc, ...) or use-cases, +> which all utilize the shared conceptual programming model. + +## Thinking in Distributed Actors + +In order to build distributed systems successfully you will need to get into the right mindset. + +You can use distributed actors, to break up your program into isolated, independent +pieces of code, which may be located on different processes or hosts at runtime. This extends the notion of isolation +that actors offer from just a concurrency perspective, to a stronger notion of isolation called "_location +transparency_". + +Distributed actors are location transparent, in the sense that by looking at a distributed actor value in code, you +don't +know if it is located on the same or on some different process or host. This allows structuring your application in a +way +which mixes distributed actor instances located on the same and on different locations in the same code. + +Location transparency also makes unit-testing distributed algorithms much simpler, +as the same distributed system can be executed in a local-only unit-test simulation, without having to involve actual +networking. + +While distributed actors make calling methods (i.e. sending messages to them) on +_potentially remote_ actors simple and safe. It is important +to stay in the mindset of "what should happen if this distributed actor were indeed remote...?" + +Distribution comes with the added complexity of _partial failure_ of systems. Messages may be dropped as networks face +issues, or a remote call may be delivered (and processed!) successfully, while only the reply to it may not have been +able to be delivered back to the caller of a distributed function. Swift helps you to remember about those issues by +making distributed methods implicitly `async` and `throws` whenever an invocation is crossing a potential network +boundary. + +## Distributed actors + +Like and classes, distributed actors are reference types. So all semantics of reference types +explained in apply to distributed actors as well. + +Distributed actors can be declared by importing the `Distributed` module, and prefixing the actor keyword with +the `distributed` modifier. +For example, let's declare a GameLobby actor, which can handle various players registering for a game of chess in the +lobby: + +```swift +import Distributed + +distributed actor GameLobby { + + typealias ActorSystem = LocalTestingDistributedActorSystem + + let name = "Chess Lobby #1" + var players: Set = [] +} +``` + +The `ActorSystem` typealias declared inside the distributed actor, defines +what [`DistributedActorSystem`](https://developer.apple.com/documentation/distributed/distributedactorsystem) the actor +is intended to work with. Declaring this is not optional, +as every distributed actor must be associated with some distributed actor system it belongs to. + +The type of actor system describes general semantic expectations and is usually tied to a specific transport mechanism. +It also defines a `SerializationRequirement` which is the type that will be used in checking distributed method +declarations, +that we'll discuss in depth when talking about . + +### Module-wide default actor system typealias + +Instead of declaring the `typealias ActorSystem = SomeSystem` in every distributed actor you declare, we can instead +declare a module-wide `DefaultDistributedActorSystem` typealias instead. + +Generally it is recommended to keep that alias at the default (module wide) access control level, like this: + +```swift +import Distributed +import SomeDistributedSystem // just an example package + +typealias DefaultDistributedActorSystem = SomeDistributedActorSystem +``` + +This way, you no longer need to declare the `ActorSystem` alias every time you declare an actor: + +```swift +import Distributed +import SomeDistributedSystem // just an example package + +typealias DefaultDistributedActorSystem = SomeDistributedActorSystem + +distributed actor Worker { + // no ActorSystem typealias necessary! +} +``` + +When mixing multiple actor systems in a single module, you can either switch to always declaring the `ActorSystem` +explicitly, +or you can declare a default system, and configure the "other" system only for a few specific actors, like this: + +```swift +import Distributed +import DistributedWebSockets // just an example package +import SomeDistributedSystem // just an example package + +typealias DefaultDistributedActorSystem = SomeDistributedActorSystem + +distributed actor Worker { + // no ActorSystem typealias necessary! +} + +distributed actor WebSocketWorker { + typealias ActorSystem = SampleWebSocketActorSystem // just an example system +} +``` + +This way we're able to have most of our distributed actors use one system, +but some of them are actually using a different one. +This may happen in practice when we're building a more advanced application which uses different transport mechanisms, +like websockets to communicate with client apps, but also clustering or some other process isolation mechanism within +the server-side application itself. + +Distributed actors implicitly conform to +the [`DistributedActor`](https://developer.apple.com/documentation/distributed/distributedactor) protocol, which is +similar to the `Actor` protocol that `actor` declarations conform to. + +### Distributed Actor Isolation + +Since a distributed actor instance may be located on the same, or on a different host than the calling code, +Swift enforces additional isolation rules in addition to those enforced for all . + +We can learn about this by attempting to access distributed actor-isolated state (e.g. the `players` property) +of the `GameLobby` that we've declared earlier. From the perspective of other code in the system, we do not +know if the game lobby is located on the same or on a different host, and therefore Swift will prevent us from +accessing the actor's state directly: + +```swift +let lobby = GameLobby(actorSystem: actorSystem) + +await lobby.players // error: distributed actor-isolated property 'players' can not be accessed from a non-isolated context +// note: access to property 'players' is only permitted within distributed actor 'GameLobby' +``` + + +Furthermore, if the game lobby had declared some member functions, those will also not be accessible in cross-actor: + +```swift +extension GameLobby { + func getIdlePlayers() -> Set { + // return only players not currently participating in a game + } +} +``` + +Attempts to invoke such function will result in a distributed actor-isolation violation, like this: + +```swift +let lobby: GameLobby = ... +await lobby.getIdlePlayers() // error: only 'distributed' instance methods can be called on a potentially remote distributed actor +``` + +All this strict distributed actor isolation checking is enforced by the compiler in order to allow us to use +local as well as remote references of the same actor type in the same way. + +Non-distributed methods and distributed actor isolated state can be accessed from the actor itself, +however it is worth calling out that whenever we obtain an `isolated` instance of a distributed actor, +it is guaranteed to be a local reference. It is not possible to obtain an `isolated` remote actor reference. + +It is possible to _dynamically_ check a distributed actor reference, for whether or not it is local, and obtain +an `isolated` reference in order to access non-distributed methods and state on it using the `whenLocal` method +that exists on every `distributed actor`: + +```swift +let lobby: GameLobby = ... + +await lobby.whenLocal { (l: isolated GameLobby) in + lobby.players // can inspect isolated state +} +``` + +Distributed actors cannot declared `nonisolated` *stored* properties, +as it is not possible to implement such property +for the case when such actor is "remote". + +Distributed actors can declare `nonisolated` computed properties and functions, +and those work the same way as they would on normal actors, +meaning that they cannot access any of the actor's isolated state. +In the case if a distributed actor, +this effectively means that they can only access +the `actorSystem` and `id` synthesized non-isolated properties of the actor, +or any other nonisolated declarations on the actor. + +## Distributed Methods + +You may make a normal actor method declaration into a distributed method +by prefixing the `func` keyword with the `distributed` contextual keyword, +similar as one prefixes an `actor` to obtain a `distributed actor`. + +Only distributed actors are allowed to declare distributed methods, +and they must be instance methods +(i.e. `static` distributed methods are not allowed). +Computed read-only properties may also be distributed, +and function effectively the same as if +they were no argument taking distributed methods. +However, writable computed properties are note allowed, +similar to how actors cannot declare writable asynchronous +properties. + +```swift +distributed actor GameLobby { + + // ... + + distributed func join(player: Player) { + if players.insert(player) { + try await player.greet("Welcome to the game lobby \(name), current players: \(players.count)") + } else { + try await player.greet("Welcome back!") + } + } + + distributed var lobbyName: String { + self.name + } +} +``` + +Distributed methods may be called on distributed actor instances at any time, +even as (or rather, especially when) the actor is potentially remote. + +Distributed method invocations are implicitly asynchronous, +same as usual actor calls, +when performed cross actor. +Unlike plain actor methods, +they are also implicitly throwing when the potential of +crossing a network boundary via such call exists, e.g.: + +```swift +let lobby: GameLobby = ... +let somePlayer: Player = ... + +// potentially remote call, thus implicitly throwing and asynchronous: +try await lobby.join(player: somePlayer) +``` + +It is useful to see in Swift source that those are the functions that may be invoked remotely, as potentially we +may need to apply additional authentication or access control on such functions, if they are serving as entry points to our service. + +Distributed functions are also subject to additional type-system restrictions, that don't apply to normal functions. +For example, distributed functions cannot accept closures, or parameters of types that are not serializable. + +The serialization requirement of such methods is checked by the compiler by checking all parameters and result type of +functions against the actor system's `SerializationRequirement` associated type, and as a distributed actor must always +declare an actor system it can be used with, Swift can always check this requirement. This prevents us from accidentally causing runtime failures when serialization would fail handling some type at runtime, and instead informs us about these issues earlier, at compile time. + +Most systems generally default to offering a [`Codable`](https://developer.apple.com/documentation/swift/codable) based serialization implementation, and would therefore declare this requirement as `any Codable`. This is also the case for the [`LocalTestingDistributedActorSystem`](https://developer.apple.com/documentation/distributed/localtestingdistributedactorsystem) which ships with the `Distributed` module. + +If we were to introduce a function that has some not-`Codable` parameters, while using a `Codable` based distributed actor system, we would see the following errors: + +```swift +distributed actor GameLobby { + // typealias ActorSystem = LocalTestingDistributedActorSystem + + distributed func cantAccept(value: NotCodableValue) { /* ... */ } + // error: parameter 'value' of type 'GameLobby.NotCodableValue' in distributed instance method does not conform to serialization requirement 'Codable' + + distributed func computeValue: NotCodableValue { /* ... */ } + // error: result type 'GameLobby.NotCodableValue' of distributed property 'computeValue' does not conform to serialization requirement 'Codable' +} +``` + +The solution here is to ensure that values we need to pass to, or return from, distributed methods, +do conform to `Codable` (or a different protocol, if the system we're using requires a different one). + +> Note: Distributed actors can use any kind of serialization mechanism, including third party serialization libraries, +> as long as the actor system expresses that requirement and implements the serialization accordingly. Actor systems are +> not constrained to be using `Codable`, however it is a nice default mechanism +> as it's Swift's native serialization mechanism. + +### Implicit Effects of Distributed Methods + +Now what we know how to declare distributed functions, and what restrictions then enforce on their declaration sites, +let's discuss the use sites of distributed methods. + +A distributed method invocation is inherently a call that may be crossing a network or process boundary. +Therefore, distributed method invocations are implicitly asynchronous _and_ throwing when invoked cross actor. + +This is similar to how local-only actor method invocations are implicitly asynchronous: + +```swift +actor Score { + var count: Int + func increment(by points: Int) { + precondition(points >= 0) + self.count += points + } +} + +distributed actor DistributedScore { + var count: Int + + distributed func increment(by points: Int) { + precondition(points >= 0) + self.count += points + } +} +``` + +The above two "score" actors handle the same task, of incrementing a managed score counter. +The `Score` actor is local-only, and while the method is not declared as `async`, calling it cross-actor +will result in it being implicitly asynchronous, and forcing us to annotate the call with an `await` to acknowladge +the potential suspension point: + +```swift +func testLocal(score: Score, + distributedScore: DistributedScore) async throws { + await score.increment(by: 10) // implicitly async + try await distributedScore.increment(by: 10) // implicitly async throws +} +``` + +As we can see, an cross-actor call to an actor function caused an implicit `async` effect, +while the same type of call to a `distributed func` caused an additional `throws` effect that needed to be handled +with a `try`. Since the method `increment(by:)` itself is not declared throwing by itself, we know that the only +way this method can fail, is by the underlying transport mechanism failing in sme way. + +> Note: Exact transport semantics can vary between actor system implementations, so you should consult the +> documentation of the `DistributedActorSystem` your actor is using to get a complete picture of its failure +> handling semantics. + +### Conforming to Protocols with Distributed Actors + +Protocols are one of Swift's more powerful ways to abstract and reuse logic. + +Distributed actors can make use of protocols, in the same way as other Swift types, however their "local" and "remote" +sides mean that there are some interesting interactions between them and protocols that we should explain. + +A distributed actor can conform to protocols stating non distributed requirements: + +``` +protocol GameplayProtocol { + func makeMove() async throws +} +``` + +TODO: complete this + +## Distributed Actor Initialization + +Distributed actors are created the same way as other reference types in Swift, however they _must_ +initialize the implicit `actorSystem` property that is synthesized for every concrete distributed actor type. + +The default initializer, i.e. the one synthesized if there isn't an initializer declared explicitly, +is declared as accepting an `actorSystem` of the `ActorSystem` type that the actor is associated with. +This initializer effectively assigns the actor system to self and therefore readies the actor with the +distributed actor system: + +```swift +distributed actor Player { + // synthesized: + // let actorSystem: ActorSystem // synthesized property + // let id: ActorSystem.ActorID + + // synthesized: + // init(actorSystem: ActorSystem) { + // self.actorSystem = actorSystem + // self.id = actorSystem.assignID(Self.self) + // } +} +``` + +When declaring a custom initializer the actor system property must be initialized explicitly: + +```swift +distributed actor Player { + // synthesized: + // let actorSystem: ActorSystem // synthesized property + // let id: ActorSystem.ActorID + + let name: String + + init(name: String, actorSystem: ActorSystem) { + self.name = name + self.actorSystem = actorSystem + // synthesized: self.id = actorSystem.assignID(Self.self) + } +} +``` + +In this initializer, we accept an actor system as a parameter, and initialize the synthesized property +as well as the name property. The `id` property must not, and cannot, be initialized by user code, and will always be +handled by a synthesized call to the actor systems ID assignment method. + +## Resolving Distributed Actors + +While creating a local instance of a distributed actor is exactly the same as with other objects in Swift, +obtaining a reference to a remote distributed actor (i.e. a local actor instance, located on a different process or host +than the caller), takes a slightly different form than just creating an instance. + +In order to obtain a reference to a (potentially) remote distributed actor, we can use the static +`resolve` method, defined on every distributed actor type: + +```swift +distributed actor Player { + typealias ActorSystem = MyActorSystem +} + +let system: MyActorSystem +let playerID: Player.ID = /* obtained using some discovery mechanism */ + +let player: Player = try Player.resolve(id: playerID, using: system) +``` + +This allows us to obtain a reference to a `Player` actor by asking the provided actor system to create a reference. + +The actor system may return a local or remote reference, however it should not perform asynchronous work such as +trying to confirm if the actor exists remotely or not. The resolve method should quickly return either a known +local actor identified by the passed `id` or a remote reference if the actor may exist remotely. + +> Note: The reason for the "may exist remotely" semantics of the resolve method is that many systems +> employ creating actors on-demand, on the first time a request appears targeted towards a specific actor. + +The resolve method is allowed to throw if the passed `id` is illegal, or otherwise known to never yield a correct reference. +For example, if the `ID` contains enough information to know that it is actually a local reference, but the system is unable +to map this id to a known local actor, it is allowed to throw and fail the resolution. + +It should be noted that `resolve(id:using:)` is a fairly low level method used to create proxy objects, +and actor system libraries may choose to provide higher level "find an actor" methods using their own built-in +discovery mechanisms - refer to the actor system's documentation that you are using for specific guidance. + +### Resolvable Distributed Actor Protocols (Client/Server applications) + +Previous examples mostly used the concrete distributed actor type on both sides of a connection, +this is a common scenario when building peer-to-peer systems where all nodes of a distributed system +are running the same, or mostly equivalent, application code. This is useful when building systems +which load balance workloads across a cluster, or building peer-to-peer games where all participants +share the same code, but have local parts, for example every player in a game has some local state +that is unique to them, but the general logic and types involved in the game may be shared between all peers. + +The situation changes when building client/server applications, or otherwise asymmetric systems where some nodes +have code which others do not. Specifically, a "server" may have implementation logic for a `GameSession` while +clients which host only `Player` distributed actors, do not (and should not!) have the GameSession logic, +as it is intended to be only running on the "server" hosting the game. + +In order to facilitate such client/server split, we'll have to introduce a `protocol` for the `GameSession`, +which client systems are aware of, and have it be implemented only in the server component. + +Let us define the game session protocol like this: + +``` +import Distributed + +@Resolvable +public GameSession: DistributedActor where ActorSystem: DistributedActorSystem { + distributed func makeMove(player: Player, move: GameMove) async throws -> GameMoveResponse +} +``` + +Let's say we implement the game session in a way where players are not connected directly to each-other, +but instead have to send their moves to the central server. +The server's game session validates the move and forwards it to the opponent. +For a simple turn-based game like chess this could be a simple way of modeling this interaction: + +```swift +import GameService + +distributed actor GameSessionImpl: GameSession { + var players: Set = [] // players are distributed actors to which the server maintains connections + var state: GameState + + distributed func makeMove(player: Player, move: GameMove) async throws -> GameMoveResponse { + if let illegalMove = state.validateMove(player, move) { + // we return some well typed representation of an illegal move, + // rather than throwing untyped errors + return .illegalMove(illegalMove) + } + + let moved = state.applyMove(player, move) + try await opponent(of: player).opponentMoved(opponent: player, move) + + return .ok(moved) + } +} +``` + +In order to facilitate this shared-protocol but separate implementations, +you would organize your distributed types into + + +## Common Distributed Actor Patterns + +The following section features some common situations, and patterns to use when you encounter them, +while working with distributed actors. The distributed programming paradigm sometimes requires a certain +way of re-thinking of a problem, and the following examples should help you shift your thinking if you +find yourself unsure how to handle a situation while moving from local-only programming, to adopting distributed actors. + +### Distributed actors generic over Actor Systems + +It is possible to declare a distributed actor with a generic actor system, however when doing so, it must declare a +concrete `SerializationRequirement` that the actor system must require in order for this distributed actor to be +compatible with it. + +For example, if we were to implement a distributed actor representing some common distributed algorithm, or just some +simple computation like a `Greeter`, which does not rely on an specific transport specifics, we may declare it as +follows: + +```swift +distributed actor Greeter + where ActorSystem: DistributedActorSystem { + distributed func greet(name: String) -> String { + "Hello \(name)!" + } +} +``` + +While the `ActorSystem` remains generic, we did have to specify +what [`SerializationRequirement`](https://developer.apple.com/documentation/distributed/distributedactorsystem/serializationrequirement) +the system should be using. +This is necessary for the type-checking of distributed functions to remain in-tact, and prevents any attempts of using +distributed actors with actor systems which cannot serialize all of their method's required parameter and result types. + +### Emulating callbacks + +While in-process actors are able to send and work with closures, this is not possible with distributed actors, as a +closure +cannot be serialized over the network. This is one difference in programming for distribution as opposed to local-only +concurrency with actors. + +Let us consider the following "call me later" example expressed in pure local actors: + +```swift +actor Alice { + func call(later: @Sendable (String) -> ()) async -> String { + Task.detached { + // do some asynchronous processing AFTER returning from the 'call' method... + later("Here's the info you asked for!") + } + + return "Thanks for your call!" + } +} +``` + +```swift +actor Bob { + func test(alice: Alice) async { + let immediateReply = await alice.call { laterReply in + print("later: \(laterReply)") + } + + print("immediately: \(immediateReply)") + } +} + +// OUTPUT: +// immediately: Thanks for your call! +// later: Here's the info you asked for! +``` + +This example models a situation where we call the `Alice` actor, and receive a reply immediately, but at the same time +we registered a closure to be called at some later point in time when some additional information was looked up by the +actor. In local-only actors, a common way of modeling this is passing a closure to the actor. + +In distributed actors, the same can be achieved using distributed method calls rather than closures, like this: + +```swift +protocol InfoListener: Codable { + func additionalInfo(_ info: String) async throws +} + +distributed actor Alice { + distributed func call(later: InfoListener) async -> String { + Task.detached { + // do some asynchronous processing AFTER returning from the 'call' method... + later("Here's the info you asked for!") + } + + return "Thanks for your call!" + } +} +``` + +So instead of a closure, the `Alice` distributed actor is now accepting an `InfoListener`, +which we'll implement using a distributed actor on the calling side by having Bob be a distributed actor and conform to +this protocol: + +```swift +distributed actor Bob { + func test(alice: Alice) async { + let immediateReply = try await alice.call(self) + + print("immediately: \(immediateReply)") + } +} + +extension Bob: InfoListener { + distributed func additionalInfo(_ info: String) { + print("later: \(laterReply)") + } +} + + +// OUTPUT: +// immediately: Thanks for your call! +// later: Here's the info you asked for! +``` + +You may also create an per-request actor, rather than having Bob conform to this protocol. Using a per-call actor is +equivalent to using the closure callback from the initial example, because we can capture any additional state we'd +like to associate the reply with, like this: + +```swift +distributed actor Bob { + func test(alice: Alice) async { + let immediateReply = try await alice.call(self) + + print("immediately: \(immediateReply)") + } +} + +distributed actor DistributedCallback: InfoListener { + distributed func additionalInfo(_ info: String) { + print("later(\(name)): \(laterReply)") + } +} + + +// OUTPUT: +// immediately (first): Thanks for your call! +// later (first): Here's the info you asked for! + +// immediately (second): Thanks for your call! +// later (second): Here's the info you asked for! +``` + + +## Implementing Your Own DistributedActorSystem + +TODO: This may be better located in the reference manual actually, because very few developers will be implementing +their own systems. + + diff --git a/TSPL.docc/The-Swift-Programming-Language.md b/TSPL.docc/The-Swift-Programming-Language.md index 84fc1fa43..a6e409749 100644 --- a/TSPL.docc/The-Swift-Programming-Language.md +++ b/TSPL.docc/The-Swift-Programming-Language.md @@ -38,6 +38,7 @@ - - - +- - - - From 85d91b69d0dd77d4375f00f61cb8376888413d57 Mon Sep 17 00:00:00 2001 From: Konrad `ktoso` Malawski Date: Wed, 28 Aug 2024 10:32:05 +0900 Subject: [PATCH 3/6] Apply suggestions from code review Thank you for the input everyone! I'll keep working on this :) Co-authored-by: Jaleel Akbashev Co-authored-by: Martial Lienert <57722447+martialln@users.noreply.github.com> --- TSPL.docc/LanguageGuide/Distributed.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/TSPL.docc/LanguageGuide/Distributed.md b/TSPL.docc/LanguageGuide/Distributed.md index 1a26874a1..3c35918ca 100644 --- a/TSPL.docc/LanguageGuide/Distributed.md +++ b/TSPL.docc/LanguageGuide/Distributed.md @@ -29,7 +29,7 @@ and only `distributed` methods may be called on it. ## Thinking in Distributed Actors -In order to build distributed systems successfully you will need to get into the right mindset. +Building distributed systems is a complex task that requires careful consideration of reliability, maintainability, and scalability. There are various approaches to build such systems, and Swift, as a general-purpose language, offers tools for this, such as actors. However, in order to build distributed systems successfully with this tools, you will need to get into the right mindset. You can use distributed actors, to break up your program into isolated, independent pieces of code, which may be located on different processes or hosts at runtime. This extends the notion of isolation @@ -343,7 +343,7 @@ func testLocal(score: Score, As we can see, an cross-actor call to an actor function caused an implicit `async` effect, while the same type of call to a `distributed func` caused an additional `throws` effect that needed to be handled with a `try`. Since the method `increment(by:)` itself is not declared throwing by itself, we know that the only -way this method can fail, is by the underlying transport mechanism failing in sme way. +way this method can fail, is by the underlying transport mechanism failing in some way. > Note: Exact transport semantics can vary between actor system implementations, so you should consult the > documentation of the `DistributedActorSystem` your actor is using to get a complete picture of its failure @@ -585,15 +585,15 @@ actor. In local-only actors, a common way of modeling this is passing a closure In distributed actors, the same can be achieved using distributed method calls rather than closures, like this: ```swift -protocol InfoListener: Codable { - func additionalInfo(_ info: String) async throws +protocol InfoListener: DistributedActor, Codable { + distributed func additionalInfo(_ info: String) } distributed actor Alice { - distributed func call(later: InfoListener) async -> String { + distributed func call(later: some InfoListener) async -> String { Task.detached { // do some asynchronous processing AFTER returning from the 'call' method... - later("Here's the info you asked for!") + try await later.additionalInfo("Here's the info you asked for!") } return "Thanks for your call!" @@ -616,7 +616,7 @@ distributed actor Bob { extension Bob: InfoListener { distributed func additionalInfo(_ info: String) { - print("later: \(laterReply)") + print("later: \(info)") } } @@ -633,7 +633,8 @@ like to associate the reply with, like this: ```swift distributed actor Bob { func test(alice: Alice) async { - let immediateReply = try await alice.call(self) + // We have to make sure, in some way, that the `DistributedCallback` instance stays alive to receive the callback + let immediateReply = try await alice.call(later: DistributedCallback(name: "Bob", actorSystem: self.actorSystem)) print("immediately: \(immediateReply)") } From 27b317d6e542424dca8a6857a01a59dd98a2f5cf Mon Sep 17 00:00:00 2001 From: Alex Martini Date: Tue, 12 Nov 2024 21:43:42 -0800 Subject: [PATCH 4/6] Break up long lines --- TSPL.docc/LanguageGuide/Distributed.md | 602 +++++++++++++++++-------- 1 file changed, 409 insertions(+), 193 deletions(-) diff --git a/TSPL.docc/LanguageGuide/Distributed.md b/TSPL.docc/LanguageGuide/Distributed.md index 3c35918ca..b9a11edce 100644 --- a/TSPL.docc/LanguageGuide/Distributed.md +++ b/TSPL.docc/LanguageGuide/Distributed.md @@ -4,10 +4,12 @@ Distributed computing with actors. Swift has built-in support for building distributed systems using the actor model. -The *Distributed* module extends Swift's "local" actor model, providing -the ability to natively express that a piece of code may not be located in -the same process as the calling code. This is expressed using the `distributed` -keyword, which can be used with actor and function declarations. +The *Distributed* module extends Swift's "local" actor model, +providing the ability to natively express +that a piece of code may not be located in +the same process as the calling code. +This is expressed using the `distributed` keyword, +which can be used with actor and function declarations. Similar to actors, they make use of actor isolation, however unlike them, @@ -19,52 +21,86 @@ than plain actors. An instance of distributed actor type is --- at compile time --- assumed to be "potentially remote", -meaning that strong isolation checks are applied to accesses performed on it, +meaning that strong isolation checks +are applied to accesses performed on it, and only `distributed` methods may be called on it. -> Note: Swift's distributed actor model features an unique bring-your-own-runtime approach, -> meaning that there exist various specialized transport implementations (distributed actor systems) -> which are specialized for specific transports (e.g. network, ipc, ...) or use-cases, +> Note: Swift's distributed actor model +> features an unique bring-your-own-runtime approach, +> meaning that there exist various specialized transport implementations +> (distributed actor systems) +> which are specialized for specific transports +> (e.g. network, ipc, ...) +> or use-cases, > which all utilize the shared conceptual programming model. ## Thinking in Distributed Actors -Building distributed systems is a complex task that requires careful consideration of reliability, maintainability, and scalability. There are various approaches to build such systems, and Swift, as a general-purpose language, offers tools for this, such as actors. However, in order to build distributed systems successfully with this tools, you will need to get into the right mindset. - -You can use distributed actors, to break up your program into isolated, independent -pieces of code, which may be located on different processes or hosts at runtime. This extends the notion of isolation -that actors offer from just a concurrency perspective, to a stronger notion of isolation called "_location -transparency_". - -Distributed actors are location transparent, in the sense that by looking at a distributed actor value in code, you -don't -know if it is located on the same or on some different process or host. This allows structuring your application in a -way -which mixes distributed actor instances located on the same and on different locations in the same code. - -Location transparency also makes unit-testing distributed algorithms much simpler, -as the same distributed system can be executed in a local-only unit-test simulation, without having to involve actual -networking. - -While distributed actors make calling methods (i.e. sending messages to them) on -_potentially remote_ actors simple and safe. It is important -to stay in the mindset of "what should happen if this distributed actor were indeed remote...?" - -Distribution comes with the added complexity of _partial failure_ of systems. Messages may be dropped as networks face -issues, or a remote call may be delivered (and processed!) successfully, while only the reply to it may not have been -able to be delivered back to the caller of a distributed function. Swift helps you to remember about those issues by -making distributed methods implicitly `async` and `throws` whenever an invocation is crossing a potential network -boundary. +Building distributed systems is a complex task +that requires careful consideration of +reliability, maintainability, and scalability. +There are various approaches to build such systems, +and Swift, +as a general-purpose language, +offers tools for this, such as actors. +However, in order to build distributed systems successfully +with this tools, +you will need to get into the right mindset. + +You can use distributed actors, +to break up your program into +isolated, independent pieces of code, +which may be located on different processes or hosts at runtime. +This extends the notion of isolation that actors offer +from just a concurrency perspective, +to a stronger notion of isolation called "_location transparency_". + +Distributed actors are location transparent, +in the sense that by looking at a distributed actor value in code, +you don't know if it is located +on the same or on some different process or host. +This allows structuring your application in a way +which mixes distributed actor instances +located on the same and on different locations in the same code. + +Location transparency also +makes unit-testing distributed algorithms much simpler, +as the same distributed system +can be executed in a local-only unit-test simulation, +without having to involve actual networking. + +While distributed actors make calling methods +(i.e. sending messages to them) +on _potentially remote_ actors simple and safe. +It is important to stay in the mindset of +"what should happen if this distributed actor were indeed remote...?" + +Distribution comes with +the added complexity of _partial failure_ of systems. +Messages may be dropped as networks face issues, +or a remote call may be delivered (and processed!) successfully, +while only the reply to it +may not have been able to be delivered +back to the caller of a distributed function. +Swift helps you to remember about those issues +by making distributed methods implicitly `async` and `throws` +whenever an invocation is crossing a potential network boundary. ## Distributed actors -Like and classes, distributed actors are reference types. So all semantics of reference types -explained in apply to distributed actors as well. +Like and classes, +distributed actors are reference types. +So all semantics of reference types explained in + +apply to distributed actors as well. -Distributed actors can be declared by importing the `Distributed` module, and prefixing the actor keyword with -the `distributed` modifier. -For example, let's declare a GameLobby actor, which can handle various players registering for a game of chess in the -lobby: +Distributed actors can be declared +by importing the `Distributed` module, +and prefixing the actor keyword with the `distributed` modifier. +For example, +let's declare a GameLobby actor, +which can handle various players registering for a game of chess +in the lobby: ```swift import Distributed @@ -78,22 +114,35 @@ distributed actor GameLobby { } ``` -The `ActorSystem` typealias declared inside the distributed actor, defines -what [`DistributedActorSystem`](https://developer.apple.com/documentation/distributed/distributedactorsystem) the actor -is intended to work with. Declaring this is not optional, -as every distributed actor must be associated with some distributed actor system it belongs to. - -The type of actor system describes general semantic expectations and is usually tied to a specific transport mechanism. -It also defines a `SerializationRequirement` which is the type that will be used in checking distributed method -declarations, -that we'll discuss in depth when talking about . +The `ActorSystem` typealias +declared inside the distributed actor, +defines what +[`DistributedActorSystem`](https://developer.apple.com/documentation/distributed/distributedactorsystem) +the actor is intended to work with. +Declaring this is not optional, +as every distributed actor +must be associated with some distributed actor system it belongs to. + +The type of actor system +describes general semantic expectations +and is usually tied to a specific transport mechanism. +It also defines a `SerializationRequirement` +which is the type that will be used in +checking distributed method declarations, +that we'll discuss in depth when talking about +. ### Module-wide default actor system typealias -Instead of declaring the `typealias ActorSystem = SomeSystem` in every distributed actor you declare, we can instead -declare a module-wide `DefaultDistributedActorSystem` typealias instead. +Instead of declaring the +`typealias ActorSystem = SomeSystem` +in every distributed actor you declare, +we can instead declare a module-wide +`DefaultDistributedActorSystem` typealias instead. -Generally it is recommended to keep that alias at the default (module wide) access control level, like this: +Generally it is recommended +to keep that alias at the default (module wide) access control level, +like this: ```swift import Distributed @@ -102,7 +151,9 @@ import SomeDistributedSystem // just an example package typealias DefaultDistributedActorSystem = SomeDistributedActorSystem ``` -This way, you no longer need to declare the `ActorSystem` alias every time you declare an actor: +This way, +you no longer need to declare the `ActorSystem` alias +every time you declare an actor: ```swift import Distributed @@ -115,9 +166,12 @@ distributed actor Worker { } ``` -When mixing multiple actor systems in a single module, you can either switch to always declaring the `ActorSystem` -explicitly, -or you can declare a default system, and configure the "other" system only for a few specific actors, like this: +When mixing multiple actor systems in a single module, +you can either switch to +always declaring the `ActorSystem` explicitly, +or you can declare a default system, +and configure the "other" system +only for a few specific actors, like this: ```swift import Distributed @@ -137,23 +191,35 @@ distributed actor WebSocketWorker { This way we're able to have most of our distributed actors use one system, but some of them are actually using a different one. -This may happen in practice when we're building a more advanced application which uses different transport mechanisms, -like websockets to communicate with client apps, but also clustering or some other process isolation mechanism within -the server-side application itself. - -Distributed actors implicitly conform to -the [`DistributedActor`](https://developer.apple.com/documentation/distributed/distributedactor) protocol, which is -similar to the `Actor` protocol that `actor` declarations conform to. +This may happen in practice +when we're building a more advanced application +which uses different transport mechanisms, +like websockets to communicate with client apps, +but also clustering or some other process isolation mechanism +within the server-side application itself. + +Distributed actors implicitly conform to the +[`DistributedActor`](https://developer.apple.com/documentation/distributed/distributedactor) +protocol, which is similar to the `Actor` protocol +that `actor` declarations conform to. ### Distributed Actor Isolation -Since a distributed actor instance may be located on the same, or on a different host than the calling code, -Swift enforces additional isolation rules in addition to those enforced for all . - -We can learn about this by attempting to access distributed actor-isolated state (e.g. the `players` property) -of the `GameLobby` that we've declared earlier. From the perspective of other code in the system, we do not -know if the game lobby is located on the same or on a different host, and therefore Swift will prevent us from -accessing the actor's state directly: +Since a distributed actor instance +may be located on the same, +or on a different host than the calling code, +Swift enforces additional isolation rules +in addition to those enforced for all . + +We can learn about this +by attempting to access distributed actor-isolated state +(e.g. the `players` property) +of the `GameLobby` that we've declared earlier. +From the perspective of other code in the system, +we do not know if the game lobby +is located on the same or on a different host, +and therefore Swift will prevent us +from accessing the actor's state directly: ```swift let lobby = GameLobby(actorSystem: actorSystem) @@ -163,7 +229,9 @@ await lobby.players // error: distributed actor-isolated property 'players' can ``` -Furthermore, if the game lobby had declared some member functions, those will also not be accessible in cross-actor: +Furthermore, +if the game lobby had declared some member functions, +those will also not be accessible in cross-actor: ```swift extension GameLobby { @@ -173,22 +241,33 @@ extension GameLobby { } ``` -Attempts to invoke such function will result in a distributed actor-isolation violation, like this: +Attempts to invoke such function +will result in a distributed actor-isolation violation, +like this: ```swift let lobby: GameLobby = ... await lobby.getIdlePlayers() // error: only 'distributed' instance methods can be called on a potentially remote distributed actor ``` -All this strict distributed actor isolation checking is enforced by the compiler in order to allow us to use -local as well as remote references of the same actor type in the same way. - -Non-distributed methods and distributed actor isolated state can be accessed from the actor itself, -however it is worth calling out that whenever we obtain an `isolated` instance of a distributed actor, -it is guaranteed to be a local reference. It is not possible to obtain an `isolated` remote actor reference. - -It is possible to _dynamically_ check a distributed actor reference, for whether or not it is local, and obtain -an `isolated` reference in order to access non-distributed methods and state on it using the `whenLocal` method +All this strict distributed actor isolation checking +is enforced by the compiler +in order to allow us to use local as well as remote references +of the same actor type in the same way. + +Non-distributed methods and distributed actor isolated state +can be accessed from the actor itself, +however it is worth calling out +that whenever we obtain an `isolated` instance of a distributed actor, +it is guaranteed to be a local reference. +It is not possible to obtain an `isolated` remote actor reference. + +It is possible +to _dynamically_ check a distributed actor reference, +for whether or not it is local, +and obtain an `isolated` reference +in order to access non-distributed methods and state on it +using the `whenLocal` method that exists on every `distributed actor`: ```swift @@ -264,19 +343,44 @@ let somePlayer: Player = ... try await lobby.join(player: somePlayer) ``` -It is useful to see in Swift source that those are the functions that may be invoked remotely, as potentially we -may need to apply additional authentication or access control on such functions, if they are serving as entry points to our service. - -Distributed functions are also subject to additional type-system restrictions, that don't apply to normal functions. -For example, distributed functions cannot accept closures, or parameters of types that are not serializable. - -The serialization requirement of such methods is checked by the compiler by checking all parameters and result type of -functions against the actor system's `SerializationRequirement` associated type, and as a distributed actor must always -declare an actor system it can be used with, Swift can always check this requirement. This prevents us from accidentally causing runtime failures when serialization would fail handling some type at runtime, and instead informs us about these issues earlier, at compile time. - -Most systems generally default to offering a [`Codable`](https://developer.apple.com/documentation/swift/codable) based serialization implementation, and would therefore declare this requirement as `any Codable`. This is also the case for the [`LocalTestingDistributedActorSystem`](https://developer.apple.com/documentation/distributed/localtestingdistributedactorsystem) which ships with the `Distributed` module. - -If we were to introduce a function that has some not-`Codable` parameters, while using a `Codable` based distributed actor system, we would see the following errors: +It is useful to see in Swift source +that those are the functions that may be invoked remotely, +as potentially we may need to apply +additional authentication or access control on such functions, +if they are serving as entry points to our service. + +Distributed functions are also +subject to additional type-system restrictions, +that don't apply to normal functions. +For example, +distributed functions cannot accept closures, +or parameters of types that are not serializable. + +The serialization requirement of such methods +is checked by the compiler +by checking all parameters and result type of functions +against the actor system's `SerializationRequirement` associated type, +and as a distributed actor must always +declare an actor system it can be used with, +Swift can always check this requirement. +This prevents us from +accidentally causing runtime failures +when serialization would fail handling some type at runtime, +and instead informs us about these issues earlier, +at compile time. + +Most systems generally default to offering a +[`Codable`](https://developer.apple.com/documentation/swift/codable) +based serialization implementation, +and would therefore declare this requirement as `any Codable`. +This is also the case for the +[`LocalTestingDistributedActorSystem`](https://developer.apple.com/documentation/distributed/localtestingdistributedactorsystem) +which ships with the `Distributed` module. + +If we were to introduce a function +that has some not-`Codable` parameters, +while using a `Codable` based distributed actor system, +we would see the following errors: ```swift distributed actor GameLobby { @@ -290,23 +394,36 @@ distributed actor GameLobby { } ``` -The solution here is to ensure that values we need to pass to, or return from, distributed methods, -do conform to `Codable` (or a different protocol, if the system we're using requires a different one). - -> Note: Distributed actors can use any kind of serialization mechanism, including third party serialization libraries, -> as long as the actor system expresses that requirement and implements the serialization accordingly. Actor systems are -> not constrained to be using `Codable`, however it is a nice default mechanism +The solution here is to ensure +that values we need to pass to, or return from, distributed methods, +do conform to `Codable` +(or a different protocol, +if the system we're using requires a different one). + +> Note: +> Distributed actors +> can use any kind of serialization mechanism, +> including third party serialization libraries, +> as long as the actor system expresses that requirement +> and implements the serialization accordingly. +> Actor systems are not constrained to be using `Codable`, +> however it is a nice default mechanism > as it's Swift's native serialization mechanism. ### Implicit Effects of Distributed Methods -Now what we know how to declare distributed functions, and what restrictions then enforce on their declaration sites, +Now what we know how to declare distributed functions, +and what restrictions then enforce on their declaration sites, let's discuss the use sites of distributed methods. -A distributed method invocation is inherently a call that may be crossing a network or process boundary. -Therefore, distributed method invocations are implicitly asynchronous _and_ throwing when invoked cross actor. +A distributed method invocation +is inherently a call that may be crossing a network or process boundary. +Therefore, +distributed method invocations are implicitly +asynchronous _and_ throwing when invoked cross actor. -This is similar to how local-only actor method invocations are implicitly asynchronous: +This is similar to how local-only actor method invocations +are implicitly asynchronous: ```swift actor Score { @@ -327,10 +444,13 @@ distributed actor DistributedScore { } ``` -The above two "score" actors handle the same task, of incrementing a managed score counter. -The `Score` actor is local-only, and while the method is not declared as `async`, calling it cross-actor -will result in it being implicitly asynchronous, and forcing us to annotate the call with an `await` to acknowladge -the potential suspension point: +The above two "score" actors handle the same task, +of incrementing a managed score counter. +The `Score` actor is local-only, +and while the method is not declared as `async`, +calling it cross-actor will result in it being implicitly asynchronous, +and forcing us to annotate the call with an `await` +to acknowladge the potential suspension point: ```swift func testLocal(score: Score, @@ -340,21 +460,33 @@ func testLocal(score: Score, } ``` -As we can see, an cross-actor call to an actor function caused an implicit `async` effect, -while the same type of call to a `distributed func` caused an additional `throws` effect that needed to be handled -with a `try`. Since the method `increment(by:)` itself is not declared throwing by itself, we know that the only -way this method can fail, is by the underlying transport mechanism failing in some way. - -> Note: Exact transport semantics can vary between actor system implementations, so you should consult the -> documentation of the `DistributedActorSystem` your actor is using to get a complete picture of its failure -> handling semantics. +As we can see, +an cross-actor call to an actor function +caused an implicit `async` effect, +while the same type of call to a `distributed func` +caused an additional `throws` effect +that needed to be handled with a `try`. +Since the method `increment(by:)` itself +is not declared throwing by itself, +we know that the only way this method can fail, +is by the underlying transport mechanism failing in some way. + +> Note: +> Exact transport semantics +> can vary between actor system implementations, +> so you should consult the documentation +> of the `DistributedActorSystem` your actor is using +> to get a complete picture of its failure handling semantics. ### Conforming to Protocols with Distributed Actors Protocols are one of Swift's more powerful ways to abstract and reuse logic. -Distributed actors can make use of protocols, in the same way as other Swift types, however their "local" and "remote" -sides mean that there are some interesting interactions between them and protocols that we should explain. +Distributed actors can make use of protocols, +in the same way as other Swift types, +however their "local" and "remote" sides +mean that there are some interesting interactions +between them and protocols that we should explain. A distributed actor can conform to protocols stating non distributed requirements: @@ -368,13 +500,19 @@ TODO: complete this ## Distributed Actor Initialization -Distributed actors are created the same way as other reference types in Swift, however they _must_ -initialize the implicit `actorSystem` property that is synthesized for every concrete distributed actor type. +Distributed actors are created +the same way as other reference types in Swift, +however they _must_ initialize the implicit `actorSystem` property +that is synthesized for every concrete distributed actor type. -The default initializer, i.e. the one synthesized if there isn't an initializer declared explicitly, -is declared as accepting an `actorSystem` of the `ActorSystem` type that the actor is associated with. -This initializer effectively assigns the actor system to self and therefore readies the actor with the -distributed actor system: +The default initializer, +i.e. the one synthesized +if there isn't an initializer declared explicitly, +is declared as accepting an `actorSystem` of the `ActorSystem` type +that the actor is associated with. +This initializer effectively assigns the actor system to self +and therefore readies the actor +with the distributed actor system: ```swift distributed actor Player { @@ -390,7 +528,8 @@ distributed actor Player { } ``` -When declaring a custom initializer the actor system property must be initialized explicitly: +When declaring a custom initializer +the actor system property must be initialized explicitly: ```swift distributed actor Player { @@ -408,18 +547,28 @@ distributed actor Player { } ``` -In this initializer, we accept an actor system as a parameter, and initialize the synthesized property -as well as the name property. The `id` property must not, and cannot, be initialized by user code, and will always be -handled by a synthesized call to the actor systems ID assignment method. +In this initializer, +we accept an actor system as a parameter, +and initialize the synthesized property +as well as the name property. +The `id` property must not, and cannot, +be initialized by user code, +and will always be handled by a synthesized call +to the actor systems ID assignment method. ## Resolving Distributed Actors -While creating a local instance of a distributed actor is exactly the same as with other objects in Swift, -obtaining a reference to a remote distributed actor (i.e. a local actor instance, located on a different process or host -than the caller), takes a slightly different form than just creating an instance. +While creating a local instance of a distributed actor +is exactly the same as with other objects in Swift, +obtaining a reference to a remote distributed actor +(i.e. a local actor instance, +located on a different process or host than the caller), +takes a slightly different form than just creating an instance. -In order to obtain a reference to a (potentially) remote distributed actor, we can use the static -`resolve` method, defined on every distributed actor type: +In order to obtain a reference +to a (potentially) remote distributed actor, +we can use the static `resolve` method, +defined on every distributed actor type: ```swift distributed actor Player { @@ -432,39 +581,72 @@ let playerID: Player.ID = /* obtained using some discovery mechanism */ let player: Player = try Player.resolve(id: playerID, using: system) ``` -This allows us to obtain a reference to a `Player` actor by asking the provided actor system to create a reference. - -The actor system may return a local or remote reference, however it should not perform asynchronous work such as -trying to confirm if the actor exists remotely or not. The resolve method should quickly return either a known -local actor identified by the passed `id` or a remote reference if the actor may exist remotely. - -> Note: The reason for the "may exist remotely" semantics of the resolve method is that many systems -> employ creating actors on-demand, on the first time a request appears targeted towards a specific actor. - -The resolve method is allowed to throw if the passed `id` is illegal, or otherwise known to never yield a correct reference. -For example, if the `ID` contains enough information to know that it is actually a local reference, but the system is unable -to map this id to a known local actor, it is allowed to throw and fail the resolution. - -It should be noted that `resolve(id:using:)` is a fairly low level method used to create proxy objects, -and actor system libraries may choose to provide higher level "find an actor" methods using their own built-in -discovery mechanisms - refer to the actor system's documentation that you are using for specific guidance. +This allows us to obtain a reference to a `Player` actor +by asking the provided actor system to create a reference. + +The actor system may return a local or remote reference, +however it should not perform asynchronous work +such as trying to confirm if the actor exists remotely or not. +The resolve method should quickly return +either a known local actor identified by the passed `id` +or a remote reference if the actor may exist remotely. + +> Note: +> The reason for the "may exist remotely" semantics +> of the resolve method +> is that many systems employ creating actors on-demand, +> on the first time a request appears +> targeted towards a specific actor. + +The resolve method is allowed to throw +if the passed `id` is illegal, +or otherwise known to never yield a correct reference. +For example, +if the `ID` contains enough information +to know that it is actually a local reference, +but the system is unable to map this id to a known local actor, +it is allowed to throw and fail the resolution. + +It should be noted that `resolve(id:using:)` +is a fairly low level method used to create proxy objects, +and actor system libraries may choose +to provide higher level "find an actor" methods +using their own built-in discovery mechanisms - +refer to the actor system's documentation +that you are using for specific guidance. ### Resolvable Distributed Actor Protocols (Client/Server applications) -Previous examples mostly used the concrete distributed actor type on both sides of a connection, -this is a common scenario when building peer-to-peer systems where all nodes of a distributed system -are running the same, or mostly equivalent, application code. This is useful when building systems -which load balance workloads across a cluster, or building peer-to-peer games where all participants -share the same code, but have local parts, for example every player in a game has some local state -that is unique to them, but the general logic and types involved in the game may be shared between all peers. - -The situation changes when building client/server applications, or otherwise asymmetric systems where some nodes -have code which others do not. Specifically, a "server" may have implementation logic for a `GameSession` while -clients which host only `Player` distributed actors, do not (and should not!) have the GameSession logic, +Previous examples mostly used +the concrete distributed actor type +on both sides of a connection, +this is a common scenario +when building peer-to-peer systems +where all nodes of a distributed system +are running the same, or mostly equivalent, application code. +This is useful when building systems +which load balance workloads across a cluster, +or building peer-to-peer games +where all participants share the same code, +but have local parts, +for example every player in a game +has some local state that is unique to them, +but the general logic and types involved in the game +may be shared between all peers. + +The situation changes when building client/server applications, +or otherwise asymmetric systems +where some nodes have code which others do not. +Specifically, +a "server" may have implementation logic for a `GameSession` +while clients which host only `Player` distributed actors, +do not (and should not!) have the GameSession logic, as it is intended to be only running on the "server" hosting the game. -In order to facilitate such client/server split, we'll have to introduce a `protocol` for the `GameSession`, -which client systems are aware of, and have it be implemented only in the server component. +In order to facilitate such client/server split, +we'll have to introduce a `protocol` for the `GameSession`, +which client systems are aware of, +and have it be implemented only in the server component. Let us define the game session protocol like this: @@ -477,10 +659,12 @@ public GameSession: DistributedActor where ActorSystem: DistributedActorSystem @@ -534,20 +728,28 @@ distributed actor Greeter } ``` -While the `ActorSystem` remains generic, we did have to specify -what [`SerializationRequirement`](https://developer.apple.com/documentation/distributed/distributedactorsystem/serializationrequirement) +While the `ActorSystem` remains generic, +we did have to specify what +[`SerializationRequirement`](https://developer.apple.com/documentation/distributed/distributedactorsystem/serializationrequirement) the system should be using. -This is necessary for the type-checking of distributed functions to remain in-tact, and prevents any attempts of using -distributed actors with actor systems which cannot serialize all of their method's required parameter and result types. +This is necessary +for the type-checking of distributed functions to remain in-tact, +and prevents any attempts of using +distributed actors with actor systems +which cannot serialize +all of their method's required parameter and result types. ### Emulating callbacks -While in-process actors are able to send and work with closures, this is not possible with distributed actors, as a -closure -cannot be serialized over the network. This is one difference in programming for distribution as opposed to local-only -concurrency with actors. +While in-process actors +are able to send and work with closures, +this is not possible with distributed actors, +as a closure cannot be serialized over the network. +This is one difference in programming +for distribution as opposed to local-only concurrency with actors. -Let us consider the following "call me later" example expressed in pure local actors: +Let us consider the following "call me later" example +expressed in pure local actors: ```swift actor Alice { @@ -578,11 +780,18 @@ actor Bob { // later: Here's the info you asked for! ``` -This example models a situation where we call the `Alice` actor, and receive a reply immediately, but at the same time -we registered a closure to be called at some later point in time when some additional information was looked up by the -actor. In local-only actors, a common way of modeling this is passing a closure to the actor. +This example models a situation +where we call the `Alice` actor, +and receive a reply immediately, +but at the same time we registered a closure +to be called at some later point in time +when some additional information was looked up by the actor. +In local-only actors, +a common way of modeling this is passing a closure to the actor. -In distributed actors, the same can be achieved using distributed method calls rather than closures, like this: +In distributed actors, +the same can be achieved using distributed method calls +rather than closures, like this: ```swift protocol InfoListener: DistributedActor, Codable { @@ -601,9 +810,12 @@ distributed actor Alice { } ``` -So instead of a closure, the `Alice` distributed actor is now accepting an `InfoListener`, -which we'll implement using a distributed actor on the calling side by having Bob be a distributed actor and conform to -this protocol: +So instead of a closure, +the `Alice` distributed actor is now accepting an `InfoListener`, +which we'll implement +using a distributed actor on the calling side +by having Bob be a distributed actor +and conform to this protocol: ```swift distributed actor Bob { @@ -626,9 +838,12 @@ extension Bob: InfoListener { // later: Here's the info you asked for! ``` -You may also create an per-request actor, rather than having Bob conform to this protocol. Using a per-call actor is -equivalent to using the closure callback from the initial example, because we can capture any additional state we'd -like to associate the reply with, like this: +You may also create an per-request actor, +rather than having Bob conform to this protocol. +Using a per-call actor is equivalent to +using the closure callback from the initial example, +because we can capture +any additional state we'd like to associate the reply with, like this: ```swift distributed actor Bob { @@ -658,7 +873,8 @@ distributed actor DistributedCallback: InfoListener { ## Implementing Your Own DistributedActorSystem -TODO: This may be better located in the reference manual actually, because very few developers will be implementing +TODO: This may be better located in the reference manual actually, +because very few developers will be implementing their own systems. + +