Skip to content

Commit fcde683

Browse files
authored
Merge pull request #41756 from yim-lee/distributed/loccal-actor-system
[Distributed] Offer LocalTestingDistributedActorSystem
2 parents 7be83da + 4c09b91 commit fcde683

File tree

4 files changed

+329
-4
lines changed

4 files changed

+329
-4
lines changed

stdlib/public/Distributed/CMakeLists.txt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# This source file is part of the Swift.org open source project
44
#
5-
# Copyright (c) 2019 - 2020 Apple Inc. and the Swift project authors
5+
# Copyright (c) 2019 - 2022 Apple Inc. and the Swift project authors
66
# Licensed under Apache License v2.0 with Runtime Library Exception
77
#
88
# See https://swift.org/LICENSE.txt for license information
@@ -19,13 +19,18 @@ add_swift_target_library(swiftDistributed ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS
1919
DistributedActor.swift
2020
DistributedActorSystem.swift
2121
DistributedMetadata.swift
22+
LocalTestingDistributedActorSystem.swift
2223

24+
SWIFT_MODULE_DEPENDS_IOS Darwin
25+
SWIFT_MODULE_DEPENDS_OSX Darwin
26+
SWIFT_MODULE_DEPENDS_TVOS Darwin
27+
SWIFT_MODULE_DEPENDS_WATCHOS Darwin
2328
SWIFT_MODULE_DEPENDS_LINUX Glibc
2429
SWIFT_MODULE_DEPENDS_FREEBSD Glibc
2530
SWIFT_MODULE_DEPENDS_OPENBSD Glibc
2631
SWIFT_MODULE_DEPENDS_CYGWIN Glibc
2732
SWIFT_MODULE_DEPENDS_HAIKU Glibc
28-
SWIFT_MODULE_DEPENDS_WINDOWS CRT
33+
SWIFT_MODULE_DEPENDS_WINDOWS CRT WinSDK
2934

3035
LINK_LIBRARIES ${swift_distributed_link_libraries}
3136

stdlib/public/Distributed/DistributedActorSystem.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public protocol DistributedActorSystem: Sendable {
7878
///
7979
/// The `actor.id` of the passed actor must be an `ActorID` that this system previously has assigned.
8080
///
81-
/// If the `actorReady` gets called with some unknown ID, it should crash immediately as it signifies some
81+
/// If `actorReady` gets called with some unknown ID, it should crash immediately as it signifies some
8282
/// very unexpected use of the system.
8383
///
8484
/// - Parameter actor: reference to the (local) actor that was just fully initialized.
@@ -93,7 +93,7 @@ public protocol DistributedActorSystem: Sendable {
9393
/// and not re-cycled by the system), i.e. if it is called during a failure to initialize completely,
9494
/// the call from the actor's deinitalizer will not happen (as under these circumstances, `deinit` will be run).
9595
///
96-
/// If the `actorReady` gets called with some unknown ID, it should crash immediately as it signifies some
96+
/// If `resignID` gets called with some unknown ID, it should crash immediately as it signifies some
9797
/// very unexpected use of the system.
9898
///
9999
/// - Parameter id: the id of an actor managed by this system that has begun its `deinit`.
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import Swift
14+
15+
#if canImport(Darwin)
16+
import Darwin
17+
#elseif canImport(Glibc)
18+
import Glibc
19+
#elseif os(Windows)
20+
import WinSDK
21+
#endif
22+
23+
/// A `DistributedActorSystem` designed for local only testing.
24+
///
25+
/// It will crash on any attempt of remote communication, but can be useful
26+
/// for learning about `distributed actor` isolation, as well as early
27+
/// prototyping stages of development where a real system is not necessary yet.
28+
@available(SwiftStdlib 5.7, *)
29+
public final class LocalTestingDistributedActorSystem: DistributedActorSystem, @unchecked Sendable {
30+
public typealias ActorID = LocalTestingActorAddress
31+
public typealias InvocationEncoder = LocalTestingInvocationEncoder
32+
public typealias InvocationDecoder = LocalTestingInvocationDecoder
33+
public typealias SerializationRequirement = Codable
34+
35+
private var activeActors: [ActorID: any DistributedActor] = [:]
36+
private let activeActorsLock = _Lock()
37+
38+
private var idProvider: ActorIDProvider = ActorIDProvider()
39+
private var assignedIDs: Set<ActorID> = []
40+
private let assignedIDsLock = _Lock()
41+
42+
public init() {}
43+
44+
public func resolve<Act>(id: ActorID, as actorType: Act.Type)
45+
throws -> Act? where Act: DistributedActor {
46+
guard let anyActor = self.activeActorsLock.withLock({ self.activeActors[id] }) else {
47+
throw LocalTestingDistributedActorSystemError(message: "Unable to locate id '\(id)' locally")
48+
}
49+
guard let actor = anyActor as? Act else {
50+
throw LocalTestingDistributedActorSystemError(message: "Failed to resolve id '\(id)' as \(Act.Type.self)")
51+
}
52+
return actor
53+
}
54+
55+
public func assignID<Act>(_ actorType: Act.Type) -> ActorID
56+
where Act: DistributedActor {
57+
let id = self.idProvider.next()
58+
self.assignedIDsLock.withLock {
59+
self.assignedIDs.insert(id)
60+
}
61+
return id
62+
}
63+
64+
public func actorReady<Act>(_ actor: Act)
65+
where Act: DistributedActor,
66+
Act.ID == ActorID {
67+
guard self.assignedIDsLock.withLock({ self.assignedIDs.contains(actor.id) }) else {
68+
fatalError("Attempted to mark an unknown actor '\(actor.id)' ready")
69+
}
70+
self.activeActorsLock.withLock {
71+
self.activeActors[actor.id] = actor
72+
}
73+
}
74+
75+
public func resignID(_ id: ActorID) {
76+
self.activeActorsLock.withLock {
77+
self.activeActors.removeValue(forKey: id)
78+
}
79+
}
80+
81+
public func makeInvocationEncoder() -> InvocationEncoder {
82+
.init()
83+
}
84+
85+
public func remoteCall<Act, Err, Res>(
86+
on actor: Act,
87+
target: RemoteCallTarget,
88+
invocation: inout InvocationEncoder,
89+
throwing errorType: Err.Type,
90+
returning returnType: Res.Type
91+
) async throws -> Res
92+
where Act: DistributedActor,
93+
Act.ID == ActorID,
94+
Err: Error,
95+
Res: SerializationRequirement {
96+
fatalError("Attempted to make remote call to \(target) on actor \(actor) using a local-only actor system")
97+
}
98+
99+
public func remoteCallVoid<Act, Err>(
100+
on actor: Act,
101+
target: RemoteCallTarget,
102+
invocation: inout InvocationEncoder,
103+
throwing errorType: Err.Type
104+
) async throws
105+
where Act: DistributedActor,
106+
Act.ID == ActorID,
107+
Err: Error {
108+
fatalError("Attempted to make remote call to \(target) on actor \(actor) using a local-only actor system")
109+
}
110+
111+
private struct ActorIDProvider {
112+
private var counter: Int = 0
113+
private let counterLock = _Lock()
114+
115+
init() {}
116+
117+
mutating func next() -> LocalTestingActorAddress {
118+
let id: Int = self.counterLock.withLock {
119+
self.counter += 1
120+
return self.counter
121+
}
122+
return LocalTestingActorAddress(parse: "\(id)")
123+
}
124+
}
125+
}
126+
127+
@available(SwiftStdlib 5.7, *)
128+
public struct LocalTestingActorAddress: Hashable, Sendable, Codable {
129+
public let address: String
130+
131+
public init(parse address: String) {
132+
self.address = address
133+
}
134+
135+
public init(from decoder: Decoder) throws {
136+
let container = try decoder.singleValueContainer()
137+
self.address = try container.decode(String.self)
138+
}
139+
140+
public func encode(to encoder: Encoder) throws {
141+
var container = encoder.singleValueContainer()
142+
try container.encode(self.address)
143+
}
144+
}
145+
146+
@available(SwiftStdlib 5.7, *)
147+
public struct LocalTestingInvocationEncoder: DistributedTargetInvocationEncoder {
148+
public typealias SerializationRequirement = Codable
149+
150+
public mutating func recordGenericSubstitution<T>(_ type: T.Type) throws {
151+
fatalError("Attempted to call encoder method in a local-only actor system")
152+
}
153+
154+
public mutating func recordArgument<Argument: SerializationRequirement>(_ argument: Argument) throws {
155+
fatalError("Attempted to call encoder method in a local-only actor system")
156+
}
157+
158+
public mutating func recordErrorType<E: Error>(_ type: E.Type) throws {
159+
fatalError("Attempted to call encoder method in a local-only actor system")
160+
}
161+
162+
public mutating func recordReturnType<R: SerializationRequirement>(_ type: R.Type) throws {
163+
fatalError("Attempted to call encoder method in a local-only actor system")
164+
}
165+
166+
public mutating func doneRecording() throws {
167+
fatalError("Attempted to call encoder method in a local-only actor system")
168+
}
169+
}
170+
171+
@available(SwiftStdlib 5.7, *)
172+
public final class LocalTestingInvocationDecoder : DistributedTargetInvocationDecoder {
173+
public typealias SerializationRequirement = Codable
174+
175+
public func decodeGenericSubstitutions() throws -> [Any.Type] {
176+
fatalError("Attempted to call decoder method in a local-only actor system")
177+
}
178+
179+
public func decodeNextArgument<Argument: SerializationRequirement>() throws -> Argument {
180+
fatalError("Attempted to call decoder method in a local-only actor system")
181+
}
182+
183+
public func decodeErrorType() throws -> Any.Type? {
184+
fatalError("Attempted to call decoder method in a local-only actor system")
185+
}
186+
187+
public func decodeReturnType() throws -> Any.Type? {
188+
fatalError("Attempted to call decoder method in a local-only actor system")
189+
}
190+
}
191+
192+
// === errors ----------------------------------------------------------------
193+
194+
@available(SwiftStdlib 5.7, *)
195+
public struct LocalTestingDistributedActorSystemError: DistributedActorSystemError {
196+
public let message: String
197+
198+
public init(message: String) {
199+
self.message = message
200+
}
201+
}
202+
203+
// === lock ----------------------------------------------------------------
204+
205+
@available(SwiftStdlib 5.7, *)
206+
fileprivate class _Lock {
207+
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
208+
private let underlying: UnsafeMutablePointer<os_unfair_lock>
209+
#elseif os(Windows)
210+
private let underlying: UnsafeMutablePointer<SRWLOCK>
211+
#elseif os(WASI)
212+
// pthread is currently not available on WASI
213+
#elseif os(Cygwin) || os(FreeBSD) || os(OpenBSD)
214+
private let underlying: UnsafeMutablePointer<pthread_mutex_t?>
215+
#else
216+
private let underlying: UnsafeMutablePointer<pthread_mutex_t>
217+
#endif
218+
219+
deinit {
220+
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
221+
// `os_unfair_lock`s do not need to be explicitly destroyed
222+
#elseif os(Windows)
223+
// `SRWLOCK`s do not need to be explicitly destroyed
224+
#elseif os(WASI)
225+
// WASI environment has only a single thread
226+
#else
227+
guard pthread_mutex_destroy(self.underlying) == 0 else {
228+
fatalError("pthread_mutex_destroy failed")
229+
}
230+
#endif
231+
232+
#if !os(WASI)
233+
self.underlying.deinitialize(count: 1)
234+
self.underlying.deallocate()
235+
#endif
236+
}
237+
238+
init() {
239+
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
240+
self.underlying = UnsafeMutablePointer.allocate(capacity: 1)
241+
#elseif os(Windows)
242+
self.underlying = UnsafeMutablePointer.allocate(capacity: 1)
243+
InitializeSRWLock(self.underlying)
244+
#elseif os(WASI)
245+
// WASI environment has only a single thread
246+
#else
247+
self.underlying = UnsafeMutablePointer.allocate(capacity: 1)
248+
guard pthread_mutex_init(self.underlying, nil) == 0 else {
249+
fatalError("pthread_mutex_init failed")
250+
}
251+
#endif
252+
}
253+
254+
@discardableResult
255+
func withLock<T>(_ body: () -> T) -> T {
256+
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
257+
os_unfair_lock_lock(self.underlying)
258+
#elseif os(Windows)
259+
AcquireSRWLockExclusive(self.underlying)
260+
#elseif os(WASI)
261+
// WASI environment has only a single thread
262+
#else
263+
guard pthread_mutex_lock(self.underlying) == 0 else {
264+
fatalError("pthread_mutex_lock failed")
265+
}
266+
#endif
267+
268+
defer {
269+
#if os(iOS) || os(macOS) || os(tvOS) || os(watchOS)
270+
os_unfair_lock_unlock(self.underlying)
271+
#elseif os(Windows)
272+
ReleaseSRWLockExclusive(self.underlying)
273+
#elseif os(WASI)
274+
// WASI environment has only a single thread
275+
#else
276+
guard pthread_mutex_unlock(self.underlying) == 0 else {
277+
fatalError("pthread_mutex_unlock failed")
278+
}
279+
#endif
280+
}
281+
282+
return body()
283+
}
284+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-build-swift -module-name main -Xfrontend -enable-experimental-distributed -Xfrontend -disable-availability-checking -j2 -parse-as-library -I %t %s -o %t/a.out
3+
// RUN: %target-run %t/a.out | %FileCheck %s --color
4+
5+
// REQUIRES: executable_test
6+
// REQUIRES: concurrency
7+
// REQUIRES: distributed
8+
9+
// rdar://76038845
10+
// UNSUPPORTED: use_os_stdlib
11+
// UNSUPPORTED: back_deployment_runtime
12+
13+
import Distributed
14+
15+
distributed actor Worker {
16+
typealias ActorSystem = LocalTestingDistributedActorSystem
17+
18+
distributed func hi() {
19+
print("hi!")
20+
}
21+
22+
nonisolated var description: Swift.String {
23+
"Worker(\(id))"
24+
}
25+
}
26+
27+
// ==== Execute ----------------------------------------------------------------
28+
@main struct Main {
29+
static func main() async throws {
30+
let system = LocalTestingDistributedActorSystem()
31+
32+
let actor = Worker(system: system)
33+
try await actor.hi() // local calls should still just work
34+
// CHECK: hi!
35+
}
36+
}

0 commit comments

Comments
 (0)