Chapters

Hide chapters

Modern Concurrency in Swift

Second Edition · iOS 16 · Swift 5.8 · Xcode 14

Section I: Modern Concurrency in Swift

Section 1: 11 chapters
Show chapters Hide chapters

10. Actors in a Distributed System
Written by Marin Todorov

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

In the previous chapters, you learned how to run concurrent tasks in parallel on multiple CPU cores. Furthermore, you learned how to use actor types to make concurrency safe. In this last chapter of the book, you’ll cover the advanced topic of distributed actors: actors that run locally as well as in other processes — or even on different machines altogether.

actor actor actor Laptop Desktop

There are multiple reasons you’d want to make use of distributed actors, for example:

  • To run code in a child process on the same machine. This way, if a fatal error crashes the child process, your main process will continue working and can even start a new copy of the child.
  • To run a process on a remote machine, like a database server. This way, you don’t need to use REST or GraphQL to send and receive data. You simply call methods directly on objects running on the server.
  • Finally, you can use a cluster of devices to perform many tasks as an ensemble.

The distributed actors model has been around for some time, and libraries offer actors and distributed actors for many languages. Therefore, this chapter includes only a minimal amount of theory that covers the model in general.

Understanding the State of Distributed Actors in Swift

Distributed actors were part of Swift’s larger set of proposals for modern concurrency. When Swift 5.5 introduced async/await initially, it did have partial experimental support for the distributed language feature, but not all implementations were complete. As the new concurrency features of async/await, tasks, groups and actors improved over a number of minor releases, the distributed actors did not, holistically speaking, land.

At the time of this writing, the latest Swift version is 5.8, and distributed actors have been around for more than a year, but the support for real-world usage still feels like a work-in-progress in some ways:

  • The feature is still described as experimental.
  • The documentation is somewhat unclear; it contains typos and still looks like a draft version.
  • Generally, the guidelines are that actor systems are challenging to build, and developers shouldn’t build them. However, there are yet to be any officially released systems by Apple for developers to use.
  • The examples provided officially use a combination of async/await, classes, locks and notifications instead of leveraging modern concurrency in Swift.

Given all of the above, since it covers an experimental Swift feature, this chapter is also experimental. But it’s fun!

In this chapter, you’ll work on a project that includes an almost completed distributed actor system. You’ll make a few changes to each key part: the network service, the actor system itself, the distributed actor, and the app that puts it all together.

In doing this, you’ll understand how the network layer and the system work, which should give you a better understanding of how to use distributed actors if one day you have access to an official actor system or if you decide to build one yourself.

Work through the chapter with the understanding that the project follows the basics of Apple’s examples and is, therefore, just a sample system for learning purposes.

Great work on making your way through this rather lengthy disclaimer! Now, it’s time to get to it…

Evolving Local to Distributed

You’re already familiar with actors; they isolate state by putting an automatic barrier between the type’s synchronized internal state and when accessing the synchronized scope from “outside”. That means calls from other actors are considered outside access and automatically made asynchronous:

ivberKokdup() OycorFcyo dpeki ehidofeam aedxave agzrsdhohaix udmidb LxEwcaf xgQoz czNecped() iqjaya ggmylvudiak otwoxd

AgkocMvci feqouyuqoj vofpt za swu ivwox no smapiww lpi gbulag ghivi KbUqmud

exavErnegy() Semujuxo Afleq ngejo ukazeteij xoswihm kxoydgugx Poyozive Evmej okufItbujp() Lasqaq Gosumu FwZiek Edlojl Tovahizo

Getting Started With SkyNet

In this chapter, you’ll work more on the project from Chapter 7, “Concurrent Code With TaskGroup”.

Connecting to Devices via Bonjour

At the end of Chapter 7, “Concurrent Code With TaskGroup”, you completed the Sky project. The user can start a scan by tapping Engage systems, and the app will concurrently iterate over sectors in satellite imagery and scan them.

MkepZboyjrekj QVJeisst KebxuciQxenrin LSMaemzg YaqretoIjfoplefek QWFanjaes kixpn uplahq an civnihr urmutlarac mejz di fuywepf rusvb/mupaazuh tiye

Creating a Distributed Actor

Firstly, you’ll explore the distributed keyword. You prefix an actor, property or method with distributed to indicate that these might be invoked remotely.

import Foundation
import Distributed

distributed actor ScanActor {
  typealias ActorSystem = BonjourActorSystem

}
private let nameValue: String

init(name: String, actorSystem: ActorSystem) {
  self.nameValue = name
  self.actorSystem = actorSystem
}
distributed var name: String {
  nameValue
}

private var countValue = 0
distributed var count: Int {
  countValue
}
distributed func commit() {
  countValue += 1
}

distributed func run(_ task: ScanTask) async throws -> Data {
  defer {
    countValue -= 1
  }
  return try await task.run()
}

Tracking Devices on the Local Network

The Bonjour service plays two key roles in network discovery. On one side, it “advertises” the current device on the network; on another, it listens for announcements from other devices. This way, effectively, each device tracks all other devices on the network:

if [.connected, .notConnected].contains(state) {
  actorSystem?.connectivityChangedFor(
    deviceName: peerID.displayName, 
    to: state == .connected
  )
}
actorSystem?.connectivityChangedFor(
  deviceName: peerID.displayName,
  to: false
)

Managing Actors in a Distributed System

An actor system may take on many tasks; using or managing a data transport such as Bluetooth, encode and decode invocations across the wire, manage a list of available remote actors, receive remote requests and many others.

Ruxicsaf/niqexoqfok eqzilc Yilv is oylej o xozzivo Zajeezo ay axwib a neghewi Ojniku tetzev nime Aplape xahexidapk Ofsixe malasy dzji Iygaki yixqan vofohobp Cozg azgitot higd va celaegip Toqi jgovq aq osuji hug zoxipovh

var localActor: ScanActor!
self.localActor = ScanActor(
  name: localName, 
  actorSystem: self
)

withActors { $0[localActor.id] = localActor }
if connected {
  if let remoteActor = try? ScanActor
    .resolve(id: name, using: self) {
    withActors { $0[remoteActor.id] = remoteActor }
  }
}
else {
  withActors { $0.removeValue(forKey: name) }
  NotificationCenter.default.post(
    name: .disconnected, object: name
  )
}

[MCNearbyDiscoveryPeerConnection] Read failed.
[MCNearbyDiscoveryPeerConnection] Stream error occurred: Code=54 "Connection reset by peer"
Connectivity: iPhone SE (3rd generation) true
[GCKSession] Failed to send a DTLS packet with 117 bytes; sendmsg error: No route to host (65).
[GCKSession] Something is terribly wrong; no clist for remoteID [1104778395] channelID [-1].
...
func firstAvailableActor() async throws
  -> ScanActor {
  while true {
    
  }
  fatalError("Will never execute")
}
for nextID in withActors(\.keys) {

}
try await Task.sleep(for: .milliseconds(100))
guard let nextActor = try? ScanActor
  .resolve(id: nextID, using: self),
  await nextActor.count < 4 else {
    continue
  }

  do {
    try await nextActor.commit()
    return nextActor
  } catch { }

Using a System Instead of a Single Actor

In this section, you’ll leave behind the service and the actor system and move on to updating the app model.

func worker(number: Int, actor: ScanActor) async
-> Result<Data, Error> {
result = try .success(await actor.run(task))
started = Date()
try await withThrowingTaskGroup(
  of: Result<Data, Error>.self
) { [unowned self] group in
    
}
for number in 0 ..< total {
  let actor = try await 
    actorSystem.firstAvailableActor()

  group.addTask {
    return await self.worker(
      number: number, 
      actor: actor
    )
  }
}
for try await result in group {
  switch result {
  case .success(let result):
    print("Completed: \(result)")
  case .failure(let error):
    print("Failed: \(error.localizedDescription)")
  }
}
await MainActor.run {
  completed = 0
  countPerSecond = 0
  scheduled = 0
  counted = 0
}
print("Done.")

Updating the UI to Showcase Collaborative Work

While it’s pretty impressive to make simulators join SkyNet and work together, presentation is important, too. Right now, collaborating on the search for alien life seems a little…unspectacular.

@MainActor @Published var scheduled = 0 {
  didSet {
    Task {
      isCollaborating = scheduled > 0 
        && actorSystem.actorCount > 1
    }
  }
}

NotificationCenter.default.post(
  name: .localTaskUpdate,
  object: nil,
  userInfo: [Notification.taskStatusKey: "Committed"]
)
var info: [String: Any] = [:]
NotificationCenter.default.post(
  name: .localTaskUpdate, 
  object: nil,
  userInfo: info
)
do {
  let data = try await task.run()
  info[Notification.taskStatusKey] = "Task \(task.input) Completed"
  return data
} catch {
  info[Notification.taskStatusKey] = "Task \(task.input) Failed"
  throw error
}
Task {
  for await notification in NotificationCenter.default
    .notifications(named: .localTaskUpdate) {
    let status = notification.taskStatus
    let runningTasksCount = try await actorSystem.localActor.count
    Task { @MainActor in
      if scheduled == 0 {
        isCollaborating = runningTasksCount > 0
      }
      localTasksCompleted.append(status)
    }
  }
}

Retrying Failed Tasks

While it might seem like you’re finished with this chapter, there’s one final task to take care of.

struct ScanTaskError: Error {
  let underlyingError: Error
  let task: ScanTask
}
Result<Data, ScanTaskError>
let result: Result<Data, ScanTaskError>
result = .failure(.init(
  underlyingError: error,
  task: task
))
case .failure(let error):
  print("Failed: \(error.localizedDescription)")
case .failure(let error):
  group.addTask(priority: .high) {
    print("Re-run task: \(error.task.input).")
    print("Failed with: \(error.underlyingError)")
    return await self.worker(
      number: error.task.input,
      actor: self.actorSystem.localActor
    )
  }
Completed: 11
Completed: 9 by Marin's iPod
Re-run task: 16. Failed with: UnreliableAPI.action(failingEvery:) failed. <---
Completed: 12
Completed: 13 by Ted's iPhone
Completed: 14 by Ted's iPhone
Completed: 17
Completed: 15
Completed: 18
Completed: 16
Re-run task: 19. Failed with: UnreliableAPI.action(failingEvery:) failed. <---
Completed: 19
Done.

Key Points

  • Systems of distributed actors communicate over a transport layer that can use many different underlying services: local network, Bonjour, REST service, web socket and more.
  • Thanks to location transparency, regardless of whether the actor method calls are relayed to another process or a different machine, you use a simple await call at the point of use.
  • In a system of distributed actors, each needs a unique address to relay requests reliably to the target peer and the responses delivered back to the original actor.
  • Using distributed actors can fail for a myriad of reasons, so asynchronous error handling plays an even more significant role in such apps.
  • Last but not least, a distributed app uses the same APIs as a local app: async/await, task groups and actors. The actor model allows for encapsulating the transport layer and keeping its implementation hidden from the API consumers.

Where to Go From Here?

Completing this book is no small feat!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now