From 4a9e4e4846a2e42d03fe705b01e7fcf6cbccc7fa Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Wed, 19 Feb 2020 13:08:39 -0800 Subject: [PATCH 1/6] Rework actor singleton Motivation: The current actor singleton implementation has some shortcomings as documented in https://github.com/apple/swift-distributed-actors/issues/396. Modifications: - Support for proxy-only mode (i.e., without specifying behavior) - Proper implementation for Actorables. Before we can only obtain `Actorable` that is singleton, now we can actually define an `Actorable` to be singleton. Result: Resolves https://github.com/apple/swift-distributed-actors/issues/396. --- .../ActorSingletonPlugin/ActorSingleton.swift | 79 +++++------- .../ActorSingletonManager.swift | 4 - .../ActorSingletonPlugin.swift | 116 ++++++++++++++++-- .../ActorSingletonProxy.swift | 21 ++-- .../Plugins/ActorSystem+Plugins.swift | 20 +-- .../ActorSingletonPluginClusteredTests.swift | 71 +++++------ .../ActorSingletonPluginTests+XCTest.swift | 3 +- .../ActorSingletonPluginTests.swift | 36 ++++-- 8 files changed, 224 insertions(+), 126 deletions(-) diff --git a/Sources/ActorSingletonPlugin/ActorSingleton.swift b/Sources/ActorSingletonPlugin/ActorSingleton.swift index 18a2fcfff..b7242dbde 100644 --- a/Sources/ActorSingletonPlugin/ActorSingleton.swift +++ b/Sources/ActorSingletonPlugin/ActorSingleton.swift @@ -17,46 +17,27 @@ import DistributedActors // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Actor singleton -/// An `ActorSingleton` ensures that there is no more than one instance of an actor running in the cluster. -/// -/// Actors that are singleton must be registered during system setup, as part of `ActorSystemSettings`. -/// The `ActorRef` of the singleton can later be obtained through `ActorSystem.singleton.ref(name:)`. -/// -/// A singleton may run on any node in the cluster. Use `ActorSingletonSettings.allocationStrategy` to control node -/// allocation. The `ActorRef` returned by `ref(name:)` is actually a proxy in order to handle situations where the -/// singleton is shifted to different nodes. -/// -/// - Warning: Refer to the configured `AllocationStrategy` for trade-offs between safety and recovery latency for -/// the singleton allocation. -/// - SeeAlso: The `ActorSingleton` mechanism conceptually similar to Erlang/OTP's `DistributedApplication`, -// and `ClusterSingleton` in Akka. -public final class ActorSingleton { +internal final class ActorSingleton { /// Settings for the `ActorSingleton` - public let settings: ActorSingletonSettings + let settings: ActorSingletonSettings /// Props of singleton behavior - public let props: Props - /// The singleton behavior - public let behavior: Behavior + let props: Props? + /// The singleton behavior. + /// If `nil`, then this instance will be proxy-only and it will never run the actual actor. + let behavior: Behavior? /// The `ActorSingletonProxy` ref internal private(set) var proxy: ActorRef? - /// Defines a `behavior` as singleton with `settings`. - public init(settings: ActorSingletonSettings, props: Props = Props(), _ behavior: Behavior) { + init(settings: ActorSingletonSettings, props: Props?, _ behavior: Behavior?) { self.settings = settings self.props = props self.behavior = behavior } - /// Defines a `behavior` as singleton identified by `name`. - public convenience init(_ name: String, props: Props = Props(), _ behavior: Behavior) { - let settings = ActorSingletonSettings(name: name) - self.init(settings: settings, props: props, behavior) - } - - /// Spawns `ActorSingletonProxy` and associated actors (e.g., `ActorSingleManager`). - internal func spawnAll(_ system: ActorSystem) throws { + /// Spawns `ActorSingletonProxy` and associated actors (e.g., `ActorSingletonManager`). + func spawnAll(_ system: ActorSystem) throws { let allocationStrategy = self.settings.allocationStrategy.make(system.settings.cluster, self.settings) self.proxy = try system._spawnSystemActor( "singletonProxy-\(self.settings.name)", @@ -67,40 +48,46 @@ public final class ActorSingleton { } // ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: Plugin protocol conformance +// MARK: Type-erased actor singleton -extension ActorSingleton: Plugin { - public static func pluginKey(name: String) -> PluginKey> { - PluginKey>(plugin: "$actorSingleton").makeSub(name) - } +internal protocol AnyActorSingleton { + /// Stops the actor singleton. + func stop(_ system: ActorSystem) +} - public var key: PluginKey> { - Self.pluginKey(name: self.settings.name) +internal struct BoxedActorSingleton: AnyActorSingleton { + private let underlying: AnyActorSingleton + + init(_ actorSingleton: ActorSingleton) { + self.underlying = actorSingleton } - public func start(_ system: ActorSystem) -> Result { - do { - try self.spawnAll(system) - return .success(()) - } catch { - return .failure(error) + func unsafeUnwrapAs(_ type: Message.Type) -> ActorSingleton { + guard let unwrapped = self.underlying as? ActorSingleton else { + fatalError("Type mismatch, expected: [\(String(reflecting: ActorSingleton.self))] got [\(self.underlying)]") } + return unwrapped + } + + func stop(_ system: ActorSystem) { + self.underlying.stop(system) } +} - // TODO: Future - public func stop(_ system: ActorSystem) -> Result { +extension ActorSingleton: AnyActorSingleton { + func stop(_ system: ActorSystem) { // Hand over the singleton gracefully let resolveContext = ResolveContext.Directive>(address: ._singletonManager(name: self.settings.name), system: system) let managerRef = system._resolve(context: resolveContext) + // If the manager is not running this will end up in dead-letters but that's fine managerRef.tell(.stop) // We don't control the proxy's directives so we can't tell it to stop - return .success(()) } } // ==== ---------------------------------------------------------------------------------------------------------------- -// MARK: ActorSingleton settings +// MARK: Actor singleton settings /// Settings for a `ActorSingleton`. public struct ActorSingletonSettings { @@ -125,7 +112,7 @@ public struct ActorSingletonSettings { /// Singleton node allocation strategies. public enum AllocationStrategySettings { - /// Singletons will run on the cluster leader + /// Singletons will run on the cluster leader. *All* nodes are potential candidates. case byLeadership func make(_: ClusterSettings, _: ActorSingletonSettings) -> ActorSingletonAllocationStrategy { diff --git a/Sources/ActorSingletonPlugin/ActorSingletonManager.swift b/Sources/ActorSingletonPlugin/ActorSingletonManager.swift index 14bbdbe41..cf6c34752 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonManager.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonManager.swift @@ -99,10 +99,6 @@ extension ActorSingletonManager { // MARK: ActorSingletonManager path / address extension ActorAddress { - internal static func _singletonManager(name: String, on node: UniqueNode) -> ActorAddress { - .init(node: node, path: ._singletonManager(name: name), incarnation: .wellKnown) - } - internal static func _singletonManager(name: String) -> ActorAddress { .init(path: ._singletonManager(name: name), incarnation: .wellKnown) } diff --git a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift index 3fd75acad..658969428 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift @@ -14,13 +14,97 @@ import DistributedActors +// ==== ---------------------------------------------------------------------------------------------------------------- +// MARK: Actor singleton plugin + +/// The actor singleton plugin ensures that there is no more than one instance of an actor that is defined to be +/// singleton running in the cluster. +/// +/// An actor singleton may run on any node in the cluster. Use `ActorSingletonSettings.allocationStrategy` to control +/// its allocation. On candidate nodes where the singleton might run, use `ActorSystem.singleton.ref(type:name:props:behavior)` +/// to define actor behavior. Otherwise, call `ActorSystem.singleton.ref(type:name:)` to obtain a ref. The returned +/// `ActorRef` is in reality a proxy which handle situations where the singleton is shifted to different nodes. +/// +/// - Warning: Refer to the configured `AllocationStrategy` for trade-offs between safety and recovery latency for +/// the singleton allocation. +/// - SeeAlso: The `ActorSingleton` mechanism is conceptually similar to Erlang/OTP's `DistributedApplication`, +/// and `ClusterSingleton` in Akka. +public final class ActorSingletonPlugin { + private var singletons: [String: BoxedActorSingleton] = [:] + + public init() {} + + func ref(of type: Message.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { + if let existing = self.singletons[settings.name] { + guard let proxy = existing.unsafeUnwrapAs(Message.self).proxy else { + fatalError("Singleton[\(settings.name)] not yet initialized") + } + return proxy + } + + let singleton = ActorSingleton(settings: settings, props: props, behavior) + try singleton.spawnAll(system) + self.singletons[settings.name] = BoxedActorSingleton(singleton) + + guard let proxy = singleton.proxy else { + fatalError("Singleton[\(settings.name)] not yet initialized") + } + + return proxy // FIXME: Worried that we never synchronize access to proxy... + } + + func actor(of type: Act.Type, settings: ActorSingletonSettings, system: ActorSystem, _ instance: Act? = nil) throws -> Actor { + let behavior = instance.map { Act.makeBehavior(instance: $0) } + let ref = try self.ref(of: Act.Message.self, settings: settings, system: system, behavior) + return Actor(ref: ref) + } +} + +extension ActorSingletonPlugin { + func ref(of type: Message.Type, name: String, system: ActorSystem, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { + let settings = ActorSingletonSettings(name: name) + return try self.ref(of: type, settings: settings, system: system, props: props, behavior) + } + + func actor(of type: Act.Type, name: String, system: ActorSystem, _ instance: Act? = nil) throws -> Actor { + let settings = ActorSingletonSettings(name: name) + return try self.actor(of: type, settings: settings, system: system, instance) + } +} + +// ==== ---------------------------------------------------------------------------------------------------------------- +// MARK: Plugin protocol conformance + +extension ActorSingletonPlugin: Plugin { + static let pluginKey = PluginKey(plugin: "$actorSingleton") + + public var key: Key { + Self.pluginKey + } + + public func start(_ system: ActorSystem) -> Result { + .success(()) + } + + // TODO: Future + public func stop(_ system: ActorSystem) -> Result { + for (_, singleton) in self.singletons { + singleton.stop(system) + } + return .success(()) + } +} + +// ==== ---------------------------------------------------------------------------------------------------------------- +// MARK: Singleton refs and actors + extension ActorSystem { public var singleton: ActorSingletonLookup { .init(self) } } -/// Allows for simplified lookups of actor references which are known to be managed by `ActorSingleton`. +/// Allows for simplified lookups of actor singleton refs. public struct ActorSingletonLookup { private let system: ActorSystem @@ -28,20 +112,28 @@ public struct ActorSingletonLookup { self.system = system } - /// Obtains a reference to a (proxy) singleton regardless of its current location. - public func ref(name: String, of type: Message.Type) throws -> ActorRef { - let key = ActorSingleton.pluginKey(name: name) - guard let singleton = self.system.settings.plugins[key] else { + private var singletonPlugin: ActorSingletonPlugin { + let key = ActorSingletonPlugin.pluginKey + guard let singletonPlugin = self.system.settings.plugins[key] else { fatalError("No plugin found for key: [\(key)], installed plugins: \(self.system.settings.plugins)") } - guard let proxy = singleton.proxy else { - fatalError("Singleton[\(key)] not yet initialized") - } - return proxy // FIXME: Worried that we never synchronize access to proxy... + return singletonPlugin } - public func actor(name: String, _ type: Act.Type) throws -> Actor { - let ref = try self.ref(name: name, of: Act.Message.self) - return Actor(ref: ref) + /// Obtains a reference to an actor (proxy) singleton regardless of its current location. + public func ref(of type: Message.Type, name: String, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { + try self.singletonPlugin.ref(of: type, name: name, system: self.system, props: props, behavior) + } + + public func ref(of type: Message.Type, settings: ActorSingletonSettings, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { + try self.singletonPlugin.ref(of: type, settings: settings, system: self.system, props: props, behavior) + } + + public func actor(of type: Act.Type, name: String, _ instance: Act? = nil) throws -> Actor { + try self.singletonPlugin.actor(of: type, name: name, system: self.system, instance) + } + + public func actor(of type: Act.Type, settings: ActorSingletonSettings, _ instance: Act? = nil) throws -> Actor { + try self.singletonPlugin.actor(of: type, settings: settings, system: self.system, instance) } } diff --git a/Sources/ActorSingletonPlugin/ActorSingletonProxy.swift b/Sources/ActorSingletonPlugin/ActorSingletonProxy.swift index 604a7a380..8c6b12327 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonProxy.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonProxy.swift @@ -28,8 +28,9 @@ import Logging /// would be disposed to allow insertion of the latest message. /// /// The proxy subscribes to events and feeds them into `AllocationStrategy` to determine the node that the -/// singleton runs on. It spawns a `ActorSingletonManager`, which manages the actual singleton actor, as needed and -/// obtains the ref from it. It instructs the `ActorSingletonManager` to hand over the singleton when the node changes. +/// singleton runs on. If the singleton falls on *this* node, the proxy will spawn a `ActorSingletonManager`, +/// which manages the actual singleton actor, and obtain the ref from it. The proxy instructs the +/// `ActorSingletonManager` to hand over the singleton whenever the node changes. internal class ActorSingletonProxy { /// Settings for the `ActorSingleton` private let settings: ActorSingletonSettings @@ -38,9 +39,11 @@ internal class ActorSingletonProxy { private let allocationStrategy: ActorSingletonAllocationStrategy /// Props of the singleton behavior - private let singletonProps: Props - /// The singleton behavior - private let singletonBehavior: Behavior + private let singletonProps: Props? + /// The singleton behavior. + /// If `nil`, then this node is not a candidate for hosting the singleton. It would result + /// in a failure if `allocationStrategy` selects this node by mistake. + private let singletonBehavior: Behavior? /// The node that the singleton runs on private var targetNode: UniqueNode? @@ -54,7 +57,7 @@ internal class ActorSingletonProxy { /// Message buffer in case singleton `ref` is `nil` private let buffer: StashBuffer - init(settings: ActorSingletonSettings, allocationStrategy: ActorSingletonAllocationStrategy, props: Props, _ behavior: Behavior) { + init(settings: ActorSingletonSettings, allocationStrategy: ActorSingletonAllocationStrategy, props: Props? = nil, _ behavior: Behavior? = nil) { self.settings = settings self.allocationStrategy = allocationStrategy self.singletonProps = props @@ -120,10 +123,14 @@ internal class ActorSingletonProxy { } private func takeOver(_ context: ActorContext, from: UniqueNode?) throws { + guard let singletonBehavior = self.singletonBehavior else { + preconditionFailure("The actor singleton \(self.settings.name) cannot run on this node. Please review AllocationStrategySettings and/or actor singleton usage.") + } + // Spawn the manager then tell it to spawn the singleton actor self.managerRef = try context.system._spawnSystemActor( "singletonManager-\(self.settings.name)", - ActorSingletonManager(settings: self.settings, props: self.singletonProps, self.singletonBehavior).behavior, + ActorSingletonManager(settings: self.settings, props: self.singletonProps ?? Props(), singletonBehavior).behavior, props: ._wellKnown ) // Need the manager to tell us the ref because we can't resolve it due to random incarnation diff --git a/Sources/DistributedActors/Plugins/ActorSystem+Plugins.swift b/Sources/DistributedActors/Plugins/ActorSystem+Plugins.swift index c80db8fc8..6cec7b3e6 100644 --- a/Sources/DistributedActors/Plugins/ActorSystem+Plugins.swift +++ b/Sources/DistributedActors/Plugins/ActorSystem+Plugins.swift @@ -36,25 +36,25 @@ public protocol Plugin: AnyPlugin { internal struct BoxedPlugin: AnyPlugin { private let underlying: AnyPlugin - internal let key: AnyPluginKey + let key: AnyPluginKey - internal init(_ plugin: P) { + init(_ plugin: P) { self.underlying = plugin self.key = AnyPluginKey(plugin.key) } - internal func unsafeUnwrapAs(_: P.Type) -> P { + func unsafeUnwrapAs(_: P.Type) -> P { guard let unwrapped = self.underlying as? P else { fatalError("Type mismatch, expected: [\(String(reflecting: P.self))] got [\(self.underlying)]") } return unwrapped } - internal func start(_ system: ActorSystem) -> Result { + func start(_ system: ActorSystem) -> Result { self.underlying.start(system) } - internal func stop(_ system: ActorSystem) -> Result { + func stop(_ system: ActorSystem) -> Result { self.underlying.stop(system) } } @@ -94,17 +94,17 @@ public struct PluginKey: CustomStringConvertible { } internal struct AnyPluginKey: Hashable, CustomStringConvertible { - internal let pluginTypeId: ObjectIdentifier - internal let plugin: String - internal let sub: String? + let pluginTypeId: ObjectIdentifier + let plugin: String + let sub: String? - internal init(_ key: PluginKey

) { + init(_ key: PluginKey

) { self.pluginTypeId = ObjectIdentifier(P.self) self.plugin = key.plugin self.sub = key.sub } - public var description: String { + var description: String { if let sub = self.sub { return "AnyPluginKey(\(self.plugin), sub: \(sub))" } else { diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift index 2e506b9f0..ee04bc6c2 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift @@ -24,27 +24,24 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { singletonSettings.allocationStrategy = .byLeadership let first = self.setUpNode("first") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 7111 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let second = self.setUpNode("second") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 8222 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let third = self.setUpNode("third") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 9333 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } @@ -55,15 +52,15 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref1 = try first.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref2 = try second.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref3 = try third.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) try replyProbe1.expectMessage("Hello-1 Charlie!") @@ -78,41 +75,38 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { singletonSettings.allocationStrategy = .byLeadership let first = self.setUpNode("first") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 7111 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let second = self.setUpNode("second") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 8222 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let third = self.setUpNode("third") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 9333 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } // No leader so singleton is not available, messages sent should be stashed let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref1 = try first.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie-1", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref2 = try second.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie-2", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref3 = try third.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie-3", _replyTo: replyProbe3.ref)) try replyProbe1.expectNoMessage(for: .milliseconds(200)) @@ -137,35 +131,31 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { singletonSettings.allocationStrategy = .byLeadership let first = self.setUpNode("first") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 7111 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let second = self.setUpNode("second") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 8222 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let third = self.setUpNode("third") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 9333 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } let fourth = self.setUpNode("fourth") { settings in + settings += ActorSingletonPlugin() + settings.cluster.node.port = 7444 settings.cluster.autoLeaderElection = .lowestReachable(minNumberOfMembers: 3) - - settings += ActorSingleton(settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) - settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } @@ -175,17 +165,20 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref1 = try first.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref2 = try second.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + let ref3 = try third.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) + // Spawn the singleton on `fourth` + _ = try fourth.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) + // `first` has the lowest address so it should be the leader and singleton try replyProbe1.expectMessage("Hello-1 Charlie!") try replyProbe2.expectMessage("Hello-1 Charlie!") diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests+XCTest.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests+XCTest.swift index 4bbc79b9b..02e2b7bed 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests+XCTest.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests+XCTest.swift @@ -23,7 +23,8 @@ import XCTest extension ActorSingletonPluginTests { static var allTests: [(String, (ActorSingletonPluginTests) -> () throws -> Void)] { return [ - ("test_ClusterSingleton_shouldWorkWithoutCluster", test_ClusterSingleton_shouldWorkWithoutCluster), + ("test_noCluster_ref", test_noCluster_ref), + ("test_noCluster_actor", test_noCluster_actor), ] } } diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift index 67f8b15ab..96732b93d 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift @@ -18,31 +18,53 @@ import DistributedActorsTestKit import XCTest final class ActorSingletonPluginTests: ActorSystemTestBase { - func test_ClusterSingleton_shouldWorkWithoutCluster() throws { + func test_noCluster_ref() throws { // Singleton should work just fine without clustering let system = ActorSystem("test") { settings in settings.cluster.enabled = false - settings += ActorSingleton(GreeterSingleton.name, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello"))) - settings += ActorSingleton("\(GreeterSingleton.name)-other", GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hi"))) + settings += ActorSingletonPlugin() } defer { system.shutdown().wait() } - // singleton.ref let replyProbe = ActorTestKit(system).spawnTestProbe(expecting: String.self) - let ref = try system.singleton.ref(name: GreeterSingleton.name, of: GreeterSingleton.Message.self) + // singleton.ref + let ref = try system.singleton.ref(of: GreeterSingleton.Message.self, name: GreeterSingleton.name, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello"))) ref.tell(.greet(name: "Charlie", _replyTo: replyProbe.ref)) try replyProbe.expectMessage("Hello Charlie!") + // singleton.ref (proxy-only) + let proxyRef = try system.singleton.ref(of: GreeterSingleton.Message.self, name: GreeterSingleton.name) + proxyRef.tell(.greet(name: "Charlene", _replyTo: replyProbe.ref)) + try replyProbe.expectMessage("Hello Charlene!") + } + + func test_noCluster_actor() throws { + // Singleton should work just fine without clustering + let system = ActorSystem("test") { settings in + settings.cluster.enabled = false + settings += ActorSingletonPlugin() + } + + defer { + system.shutdown().wait() + } + + let replyProbe = ActorTestKit(system).spawnTestProbe(expecting: String.self) + // singleton.actor - let actor = try system.singleton.actor(name: "\(GreeterSingleton.name)-other", GreeterSingleton.self) + let actor = try system.singleton.actor(of: GreeterSingleton.self, name: GreeterSingleton.name, GreeterSingleton("Hi")) // TODO: https://github.com/apple/swift-distributed-actors/issues/344 // let string = try probe.expectReply(actor.greet(name: "Charlie", _replyTo: replyProbe.ref)) actor.ref.tell(.greet(name: "Charlie", _replyTo: replyProbe.ref)) - try replyProbe.expectMessage("Hi Charlie!") + + // singleton.actor (proxy-only) + let actorProxy = try system.singleton.actor(of: GreeterSingleton.self, name: GreeterSingleton.name) + actorProxy.ref.tell(.greet(name: "Charlene", _replyTo: replyProbe.ref)) + try replyProbe.expectMessage("Hi Charlene!") } } From 3bf9aab6d7e75a21940454c21b9fc0fdb3848b50 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Thu, 20 Feb 2020 22:25:08 -0800 Subject: [PATCH 2/6] Fix code comments and rename things --- Sources/ActorSingletonPlugin/ActorSingleton.swift | 4 +++- Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Sources/ActorSingletonPlugin/ActorSingleton.swift b/Sources/ActorSingletonPlugin/ActorSingleton.swift index b7242dbde..8ce0afc6a 100644 --- a/Sources/ActorSingletonPlugin/ActorSingleton.swift +++ b/Sources/ActorSingletonPlugin/ActorSingleton.swift @@ -51,7 +51,9 @@ internal final class ActorSingleton { // MARK: Type-erased actor singleton internal protocol AnyActorSingleton { - /// Stops the actor singleton. + /// Stops the `ActorSingletonProxy` running in the `system`. + /// If `ActorSingletonManager` is also running, which means the actual singleton is hosted + /// on this node, it will attempt to hand-over the singleton gracefully before stopping. func stop(_ system: ActorSystem) } diff --git a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift index 658969428..3594a5786 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift @@ -37,7 +37,7 @@ public final class ActorSingletonPlugin { func ref(of type: Message.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { if let existing = self.singletons[settings.name] { guard let proxy = existing.unsafeUnwrapAs(Message.self).proxy else { - fatalError("Singleton[\(settings.name)] not yet initialized") + fatalError("Singleton [\(settings.name)] not yet initialized") } return proxy } @@ -99,13 +99,13 @@ extension ActorSingletonPlugin: Plugin { // MARK: Singleton refs and actors extension ActorSystem { - public var singleton: ActorSingletonLookup { + public var singleton: ActorSingletonControl { .init(self) } } -/// Allows for simplified lookups of actor singleton refs. -public struct ActorSingletonLookup { +/// Provides actor singleton controls such as obtaining a singleton ref and defining the singleton. +public struct ActorSingletonControl { private let system: ActorSystem internal init(_ system: ActorSystem) { From e15bc87fe349c11a67383f8f8da12fcfabe65603 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Thu, 20 Feb 2020 23:33:50 -0800 Subject: [PATCH 3/6] host --- .../ActorSingletonPlugin.swift | 39 +++++++++++++------ .../ActorSingletonPluginClusteredTests.swift | 20 +++++----- .../ActorSingletonPluginTests.swift | 8 ++-- 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift index 3594a5786..8fc87688a 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift @@ -53,8 +53,12 @@ public final class ActorSingletonPlugin { return proxy // FIXME: Worried that we never synchronize access to proxy... } - func actor(of type: Act.Type, settings: ActorSingletonSettings, system: ActorSystem, _ instance: Act? = nil) throws -> Actor { - let behavior = instance.map { Act.makeBehavior(instance: $0) } + func actor(of type: Act.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ makeInstance: ((Actor.Context) -> Act)? = nil) throws -> Actor { + let behavior = makeInstance.map { maker in + Behavior.setup { context in + Act.makeBehavior(instance: maker(.init(underlying: context))) + } + } let ref = try self.ref(of: Act.Message.self, settings: settings, system: system, behavior) return Actor(ref: ref) } @@ -66,9 +70,9 @@ extension ActorSingletonPlugin { return try self.ref(of: type, settings: settings, system: system, props: props, behavior) } - func actor(of type: Act.Type, name: String, system: ActorSystem, _ instance: Act? = nil) throws -> Actor { + func actor(of type: Act.Type, name: String, system: ActorSystem, props: Props? = nil, _ makeInstance: ((Actor.Context) -> Act)? = nil) throws -> Actor { let settings = ActorSingletonSettings(name: name) - return try self.actor(of: type, settings: settings, system: system, instance) + return try self.actor(of: type, settings: settings, system: system, props: props, makeInstance) } } @@ -120,20 +124,33 @@ public struct ActorSingletonControl { return singletonPlugin } - /// Obtains a reference to an actor (proxy) singleton regardless of its current location. - public func ref(of type: Message.Type, name: String, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { + /// Defines a singleton `behavior` and indicates that it can be hosted on this node. + public func host(of type: Message.Type, name: String, props: Props = Props(), _ behavior: Behavior) throws -> ActorRef { try self.singletonPlugin.ref(of: type, name: name, system: self.system, props: props, behavior) } - public func ref(of type: Message.Type, settings: ActorSingletonSettings, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { + /// Defines a singleton `behavior` and indicates that it can be hosted on this node. + public func host(of type: Message.Type, settings: ActorSingletonSettings, props: Props = Props(), _ behavior: Behavior) throws -> ActorRef { try self.singletonPlugin.ref(of: type, settings: settings, system: self.system, props: props, behavior) } - public func actor(of type: Act.Type, name: String, _ instance: Act? = nil) throws -> Actor { - try self.singletonPlugin.actor(of: type, name: name, system: self.system, instance) + /// Defines a singleton `Actorable` and indicates that it can be hosted on this node. + public func host(of type: Act.Type, name: String, props: Props = Props(), _ makeInstance: @escaping (Actor.Context) -> Act) throws -> Actor { + try self.singletonPlugin.actor(of: type, name: name, system: self.system, props: props, makeInstance) + } + + /// Defines a singleton `Actorable` and indicates that it can be hosted on this node. + public func host(of type: Act.Type, settings: ActorSingletonSettings, props: Props = Props(), _ makeInstance: @escaping (Actor.Context) -> Act) throws -> Actor { + try self.singletonPlugin.actor(of: type, settings: settings, system: self.system, props: props, makeInstance) + } + + /// Obtains a ref to the specified actor singleton. + public func ref(of type: Message.Type, name: String) throws -> ActorRef { + try self.singletonPlugin.ref(of: type, name: name, system: self.system) } - public func actor(of type: Act.Type, settings: ActorSingletonSettings, _ instance: Act? = nil) throws -> Actor { - try self.singletonPlugin.actor(of: type, settings: settings, system: self.system, instance) + /// Obtains the specified singleton actor. + public func actor(of type: Act.Type, name: String) throws -> Actor { + try self.singletonPlugin.actor(of: type, name: name, system: self.system) } } diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift index ee04bc6c2..67ae85850 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift @@ -52,15 +52,15 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref1 = try first.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref2 = try second.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + let ref3 = try third.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) try replyProbe1.expectMessage("Hello-1 Charlie!") @@ -98,15 +98,15 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { // No leader so singleton is not available, messages sent should be stashed let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref1 = try first.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie-1", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref2 = try second.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie-2", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + let ref3 = try third.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie-3", _replyTo: replyProbe3.ref)) try replyProbe1.expectNoMessage(for: .milliseconds(200)) @@ -165,19 +165,19 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref1 = try first.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref2 = try second.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + let ref3 = try third.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) // Spawn the singleton on `fourth` - _ = try fourth.singleton.ref(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) + _ = try fourth.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) // `first` has the lowest address so it should be the leader and singleton try replyProbe1.expectMessage("Hello-1 Charlie!") diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift index 96732b93d..f118f1858 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift @@ -31,8 +31,8 @@ final class ActorSingletonPluginTests: ActorSystemTestBase { let replyProbe = ActorTestKit(system).spawnTestProbe(expecting: String.self) - // singleton.ref - let ref = try system.singleton.ref(of: GreeterSingleton.Message.self, name: GreeterSingleton.name, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello"))) + // singleton.host behavior + let ref = try system.singleton.host(of: GreeterSingleton.Message.self, name: GreeterSingleton.name, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello"))) ref.tell(.greet(name: "Charlie", _replyTo: replyProbe.ref)) try replyProbe.expectMessage("Hello Charlie!") @@ -55,8 +55,8 @@ final class ActorSingletonPluginTests: ActorSystemTestBase { let replyProbe = ActorTestKit(system).spawnTestProbe(expecting: String.self) - // singleton.actor - let actor = try system.singleton.actor(of: GreeterSingleton.self, name: GreeterSingleton.name, GreeterSingleton("Hi")) + // singleton.host Actorable + let actor = try system.singleton.host(of: GreeterSingleton.self, name: GreeterSingleton.name) { _ in GreeterSingleton("Hi") } // TODO: https://github.com/apple/swift-distributed-actors/issues/344 // let string = try probe.expectReply(actor.greet(name: "Charlie", _replyTo: replyProbe.ref)) actor.ref.tell(.greet(name: "Charlie", _replyTo: replyProbe.ref)) From a4db9b7d542c69e710c9b4c9a46358c2b54e8e21 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Fri, 21 Feb 2020 09:06:14 -0800 Subject: [PATCH 4/6] remove of from host --- .../ActorSingletonPlugin.swift | 8 ++++---- .../ActorSingletonPluginClusteredTests.swift | 20 +++++++++---------- .../ActorSingletonPluginTests.swift | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift index 8fc87688a..6c798190f 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift @@ -125,22 +125,22 @@ public struct ActorSingletonControl { } /// Defines a singleton `behavior` and indicates that it can be hosted on this node. - public func host(of type: Message.Type, name: String, props: Props = Props(), _ behavior: Behavior) throws -> ActorRef { + public func host(_ type: Message.Type, name: String, props: Props = Props(), _ behavior: Behavior) throws -> ActorRef { try self.singletonPlugin.ref(of: type, name: name, system: self.system, props: props, behavior) } /// Defines a singleton `behavior` and indicates that it can be hosted on this node. - public func host(of type: Message.Type, settings: ActorSingletonSettings, props: Props = Props(), _ behavior: Behavior) throws -> ActorRef { + public func host(_ type: Message.Type, settings: ActorSingletonSettings, props: Props = Props(), _ behavior: Behavior) throws -> ActorRef { try self.singletonPlugin.ref(of: type, settings: settings, system: self.system, props: props, behavior) } /// Defines a singleton `Actorable` and indicates that it can be hosted on this node. - public func host(of type: Act.Type, name: String, props: Props = Props(), _ makeInstance: @escaping (Actor.Context) -> Act) throws -> Actor { + public func host(_ type: Act.Type, name: String, props: Props = Props(), _ makeInstance: @escaping (Actor.Context) -> Act) throws -> Actor { try self.singletonPlugin.actor(of: type, name: name, system: self.system, props: props, makeInstance) } /// Defines a singleton `Actorable` and indicates that it can be hosted on this node. - public func host(of type: Act.Type, settings: ActorSingletonSettings, props: Props = Props(), _ makeInstance: @escaping (Actor.Context) -> Act) throws -> Actor { + public func host(_ type: Act.Type, settings: ActorSingletonSettings, props: Props = Props(), _ makeInstance: @escaping (Actor.Context) -> Act) throws -> Actor { try self.singletonPlugin.actor(of: type, settings: settings, system: self.system, props: props, makeInstance) } diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift index 67ae85850..30085a50a 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift @@ -52,15 +52,15 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) try replyProbe1.expectMessage("Hello-1 Charlie!") @@ -98,15 +98,15 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { // No leader so singleton is not available, messages sent should be stashed let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie-1", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie-2", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie-3", _replyTo: replyProbe3.ref)) try replyProbe1.expectNoMessage(for: .milliseconds(200)) @@ -165,19 +165,19 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) // Spawn the singleton on `fourth` - _ = try fourth.singleton.host(of: GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) + _ = try fourth.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) // `first` has the lowest address so it should be the leader and singleton try replyProbe1.expectMessage("Hello-1 Charlie!") diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift index f118f1858..d7dc20dc8 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginTests.swift @@ -32,7 +32,7 @@ final class ActorSingletonPluginTests: ActorSystemTestBase { let replyProbe = ActorTestKit(system).spawnTestProbe(expecting: String.self) // singleton.host behavior - let ref = try system.singleton.host(of: GreeterSingleton.Message.self, name: GreeterSingleton.name, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello"))) + let ref = try system.singleton.host(GreeterSingleton.Message.self, name: GreeterSingleton.name, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello"))) ref.tell(.greet(name: "Charlie", _replyTo: replyProbe.ref)) try replyProbe.expectMessage("Hello Charlie!") @@ -56,7 +56,7 @@ final class ActorSingletonPluginTests: ActorSystemTestBase { let replyProbe = ActorTestKit(system).spawnTestProbe(expecting: String.self) // singleton.host Actorable - let actor = try system.singleton.host(of: GreeterSingleton.self, name: GreeterSingleton.name) { _ in GreeterSingleton("Hi") } + let actor = try system.singleton.host(GreeterSingleton.self, name: GreeterSingleton.name) { _ in GreeterSingleton("Hi") } // TODO: https://github.com/apple/swift-distributed-actors/issues/344 // let string = try probe.expectReply(actor.greet(name: "Charlie", _replyTo: replyProbe.ref)) actor.ref.tell(.greet(name: "Charlie", _replyTo: replyProbe.ref)) From 2c2347414805485fa77a0082c90d38e16f72beab Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Fri, 21 Feb 2020 10:30:48 -0800 Subject: [PATCH 5/6] https://github.com/apple/swift-distributed-actors/issues/463 --- .../ActorSingletonPluginClusteredTests.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift index 30085a50a..bde0b9888 100644 --- a/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift +++ b/Tests/ActorSingletonPluginTests/ActorSingletonPluginClusteredTests.swift @@ -45,6 +45,11 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } + // Bring up `ActorSingletonProxy` before setting up cluster (https://github.com/apple/swift-distributed-actors/issues/463) + let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + first.cluster.join(node: second.cluster.node.node) third.cluster.join(node: second.cluster.node.node) @@ -52,15 +57,12 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) try replyProbe1.expectMessage("Hello-1 Charlie!") @@ -159,26 +161,26 @@ final class ActorSingletonPluginClusteredTests: ClusteredNodesTestBase { settings.serialization.registerCodable(for: GreeterSingleton.Message.self, underId: 10001) } + // Bring up `ActorSingletonProxy` before setting up cluster (https://github.com/apple/swift-distributed-actors/issues/463) + let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) + let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) + let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) + _ = try fourth.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) + first.cluster.join(node: second.cluster.node.node) third.cluster.join(node: second.cluster.node.node) try self.ensureNodes(.up, within: .seconds(10), nodes: first.cluster.node, second.cluster.node, third.cluster.node) let replyProbe1 = self.testKit(first).spawnTestProbe(expecting: String.self) - let ref1 = try first.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-1"))) ref1.tell(.greet(name: "Charlie", _replyTo: replyProbe1.ref)) let replyProbe2 = self.testKit(second).spawnTestProbe(expecting: String.self) - let ref2 = try second.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-2"))) ref2.tell(.greet(name: "Charlie", _replyTo: replyProbe2.ref)) let replyProbe3 = self.testKit(third).spawnTestProbe(expecting: String.self) - let ref3 = try third.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-3"))) ref3.tell(.greet(name: "Charlie", _replyTo: replyProbe3.ref)) - // Spawn the singleton on `fourth` - _ = try fourth.singleton.host(GreeterSingleton.Message.self, settings: singletonSettings, GreeterSingleton.makeBehavior(instance: GreeterSingleton("Hello-4"))) - // `first` has the lowest address so it should be the leader and singleton try replyProbe1.expectMessage("Hello-1 Charlie!") try replyProbe2.expectMessage("Hello-1 Charlie!") From 91479d6828f656b0759ecc57ac0db1985fe5c610 Mon Sep 17 00:00:00 2001 From: Yim Lee Date: Fri, 21 Feb 2020 12:23:46 -0800 Subject: [PATCH 6/6] Add lock --- .../ActorSingletonPlugin/ActorSingleton.swift | 22 ++++++++---- .../ActorSingletonPlugin.swift | 34 +++++++++++-------- Sources/DistributedActors/ActorSystem.swift | 2 -- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Sources/ActorSingletonPlugin/ActorSingleton.swift b/Sources/ActorSingletonPlugin/ActorSingleton.swift index 8ce0afc6a..ad718208f 100644 --- a/Sources/ActorSingletonPlugin/ActorSingleton.swift +++ b/Sources/ActorSingletonPlugin/ActorSingleton.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import DistributedActors +import DistributedActorsConcurrencyHelpers // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Actor singleton @@ -28,7 +29,14 @@ internal final class ActorSingleton { let behavior: Behavior? /// The `ActorSingletonProxy` ref - internal private(set) var proxy: ActorRef? + private var _proxy: ActorRef? + private let proxyLock = Lock() + + internal var proxy: ActorRef? { + self.proxyLock.withLock { + self._proxy + } + } init(settings: ActorSingletonSettings, props: Props?, _ behavior: Behavior?) { self.settings = settings @@ -39,11 +47,13 @@ internal final class ActorSingleton { /// Spawns `ActorSingletonProxy` and associated actors (e.g., `ActorSingletonManager`). func spawnAll(_ system: ActorSystem) throws { let allocationStrategy = self.settings.allocationStrategy.make(system.settings.cluster, self.settings) - self.proxy = try system._spawnSystemActor( - "singletonProxy-\(self.settings.name)", - ActorSingletonProxy(settings: self.settings, allocationStrategy: allocationStrategy, props: self.props, self.behavior).behavior, - props: ._wellKnown - ) + try self.proxyLock.withLock { + self._proxy = try system._spawnSystemActor( + "singletonProxy-\(self.settings.name)", + ActorSingletonProxy(settings: self.settings, allocationStrategy: allocationStrategy, props: self.props, self.behavior).behavior, + props: ._wellKnown + ) + } } } diff --git a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift index 6c798190f..f7328e0c9 100644 --- a/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift +++ b/Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import DistributedActors +import DistributedActorsConcurrencyHelpers // ==== ---------------------------------------------------------------------------------------------------------------- // MARK: Actor singleton plugin @@ -31,26 +32,29 @@ import DistributedActors /// and `ClusterSingleton` in Akka. public final class ActorSingletonPlugin { private var singletons: [String: BoxedActorSingleton] = [:] + private let singletonsLock = Lock() public init() {} func ref(of type: Message.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ behavior: Behavior? = nil) throws -> ActorRef { - if let existing = self.singletons[settings.name] { - guard let proxy = existing.unsafeUnwrapAs(Message.self).proxy else { - fatalError("Singleton [\(settings.name)] not yet initialized") + try self.singletonsLock.withLock { + if let existing = self.singletons[settings.name] { + guard let proxy = existing.unsafeUnwrapAs(Message.self).proxy else { + fatalError("Singleton [\(settings.name)] not yet initialized") + } + return proxy } - return proxy - } - let singleton = ActorSingleton(settings: settings, props: props, behavior) - try singleton.spawnAll(system) - self.singletons[settings.name] = BoxedActorSingleton(singleton) + let singleton = ActorSingleton(settings: settings, props: props, behavior) + try singleton.spawnAll(system) + self.singletons[settings.name] = BoxedActorSingleton(singleton) - guard let proxy = singleton.proxy else { - fatalError("Singleton[\(settings.name)] not yet initialized") - } + guard let proxy = singleton.proxy else { + fatalError("Singleton[\(settings.name)] not yet initialized") + } - return proxy // FIXME: Worried that we never synchronize access to proxy... + return proxy + } } func actor(of type: Act.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ makeInstance: ((Actor.Context) -> Act)? = nil) throws -> Actor { @@ -92,8 +96,10 @@ extension ActorSingletonPlugin: Plugin { // TODO: Future public func stop(_ system: ActorSystem) -> Result { - for (_, singleton) in self.singletons { - singleton.stop(system) + self.singletonsLock.withLock { + for (_, singleton) in self.singletons { + singleton.stop(system) + } } return .success(()) } diff --git a/Sources/DistributedActors/ActorSystem.swift b/Sources/DistributedActors/ActorSystem.swift index ddda1d482..c437f8ccb 100644 --- a/Sources/DistributedActors/ActorSystem.swift +++ b/Sources/DistributedActors/ActorSystem.swift @@ -50,8 +50,6 @@ public final class ActorSystem { internal let _root: _ReceivesSystemMessages - private let terminationLock = Lock() - /// Allows inspecting settings that were used to configure this actor system. /// Settings are immutable and may not be changed once the system is running. public let settings: ActorSystemSettings