Skip to content

Commit c549ec5

Browse files
authored
!singleton Rework actor singleton API (#458)
* Rework actor singleton Motivation: The current actor singleton implementation has some shortcomings as documented in #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 #396. * Fix code comments and rename things * host * remove of from host * #463 * Add lock
1 parent 25beefc commit c549ec5

File tree

9 files changed

+270
-137
lines changed

9 files changed

+270
-137
lines changed

Sources/ActorSingletonPlugin/ActorSingleton.swift

Lines changed: 51 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,94 +13,93 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import DistributedActors
16+
import DistributedActorsConcurrencyHelpers
1617

1718
// ==== ----------------------------------------------------------------------------------------------------------------
1819
// MARK: Actor singleton
1920

20-
/// An `ActorSingleton` ensures that there is no more than one instance of an actor running in the cluster.
21-
///
22-
/// Actors that are singleton must be registered during system setup, as part of `ActorSystemSettings`.
23-
/// The `ActorRef` of the singleton can later be obtained through `ActorSystem.singleton.ref(name:)`.
24-
///
25-
/// A singleton may run on any node in the cluster. Use `ActorSingletonSettings.allocationStrategy` to control node
26-
/// allocation. The `ActorRef` returned by `ref(name:)` is actually a proxy in order to handle situations where the
27-
/// singleton is shifted to different nodes.
28-
///
29-
/// - Warning: Refer to the configured `AllocationStrategy` for trade-offs between safety and recovery latency for
30-
/// the singleton allocation.
31-
/// - SeeAlso: The `ActorSingleton` mechanism conceptually similar to Erlang/OTP's <a href="http://erlang.org/doc/design_principles/distributed_applications.html">`DistributedApplication`</a>,
32-
// and <a href="https://doc.akka.io/docs/akka/current/cluster-singleton.html">`ClusterSingleton` in Akka</a>.
33-
public final class ActorSingleton<Message> {
21+
internal final class ActorSingleton<Message> {
3422
/// Settings for the `ActorSingleton`
35-
public let settings: ActorSingletonSettings
23+
let settings: ActorSingletonSettings
3624

3725
/// Props of singleton behavior
38-
public let props: Props
39-
/// The singleton behavior
40-
public let behavior: Behavior<Message>
26+
let props: Props?
27+
/// The singleton behavior.
28+
/// If `nil`, then this instance will be proxy-only and it will never run the actual actor.
29+
let behavior: Behavior<Message>?
4130

4231
/// The `ActorSingletonProxy` ref
43-
internal private(set) var proxy: ActorRef<Message>?
32+
private var _proxy: ActorRef<Message>?
33+
private let proxyLock = Lock()
4434

45-
/// Defines a `behavior` as singleton with `settings`.
46-
public init(settings: ActorSingletonSettings, props: Props = Props(), _ behavior: Behavior<Message>) {
35+
internal var proxy: ActorRef<Message>? {
36+
self.proxyLock.withLock {
37+
self._proxy
38+
}
39+
}
40+
41+
init(settings: ActorSingletonSettings, props: Props?, _ behavior: Behavior<Message>?) {
4742
self.settings = settings
4843
self.props = props
4944
self.behavior = behavior
5045
}
5146

52-
/// Defines a `behavior` as singleton identified by `name`.
53-
public convenience init(_ name: String, props: Props = Props(), _ behavior: Behavior<Message>) {
54-
let settings = ActorSingletonSettings(name: name)
55-
self.init(settings: settings, props: props, behavior)
56-
}
57-
58-
/// Spawns `ActorSingletonProxy` and associated actors (e.g., `ActorSingleManager`).
59-
internal func spawnAll(_ system: ActorSystem) throws {
47+
/// Spawns `ActorSingletonProxy` and associated actors (e.g., `ActorSingletonManager`).
48+
func spawnAll(_ system: ActorSystem) throws {
6049
let allocationStrategy = self.settings.allocationStrategy.make(system.settings.cluster, self.settings)
61-
self.proxy = try system._spawnSystemActor(
62-
"singletonProxy-\(self.settings.name)",
63-
ActorSingletonProxy(settings: self.settings, allocationStrategy: allocationStrategy, props: self.props, self.behavior).behavior,
64-
props: ._wellKnown
65-
)
50+
try self.proxyLock.withLock {
51+
self._proxy = try system._spawnSystemActor(
52+
"singletonProxy-\(self.settings.name)",
53+
ActorSingletonProxy(settings: self.settings, allocationStrategy: allocationStrategy, props: self.props, self.behavior).behavior,
54+
props: ._wellKnown
55+
)
56+
}
6657
}
6758
}
6859

6960
// ==== ----------------------------------------------------------------------------------------------------------------
70-
// MARK: Plugin protocol conformance
61+
// MARK: Type-erased actor singleton
7162

72-
extension ActorSingleton: Plugin {
73-
public static func pluginKey(name: String) -> PluginKey<ActorSingleton<Message>> {
74-
PluginKey<ActorSingleton<Message>>(plugin: "$actorSingleton").makeSub(name)
75-
}
63+
internal protocol AnyActorSingleton {
64+
/// Stops the `ActorSingletonProxy` running in the `system`.
65+
/// If `ActorSingletonManager` is also running, which means the actual singleton is hosted
66+
/// on this node, it will attempt to hand-over the singleton gracefully before stopping.
67+
func stop(_ system: ActorSystem)
68+
}
69+
70+
internal struct BoxedActorSingleton: AnyActorSingleton {
71+
private let underlying: AnyActorSingleton
7672

77-
public var key: PluginKey<ActorSingleton<Message>> {
78-
Self.pluginKey(name: self.settings.name)
73+
init<Message>(_ actorSingleton: ActorSingleton<Message>) {
74+
self.underlying = actorSingleton
7975
}
8076

81-
public func start(_ system: ActorSystem) -> Result<Void, Error> {
82-
do {
83-
try self.spawnAll(system)
84-
return .success(())
85-
} catch {
86-
return .failure(error)
77+
func unsafeUnwrapAs<Message>(_ type: Message.Type) -> ActorSingleton<Message> {
78+
guard let unwrapped = self.underlying as? ActorSingleton<Message> else {
79+
fatalError("Type mismatch, expected: [\(String(reflecting: ActorSingleton<Message>.self))] got [\(self.underlying)]")
8780
}
81+
return unwrapped
8882
}
8983

90-
// TODO: Future
91-
public func stop(_ system: ActorSystem) -> Result<Void, Error> {
84+
func stop(_ system: ActorSystem) {
85+
self.underlying.stop(system)
86+
}
87+
}
88+
89+
extension ActorSingleton: AnyActorSingleton {
90+
func stop(_ system: ActorSystem) {
9291
// Hand over the singleton gracefully
9392
let resolveContext = ResolveContext<ActorSingletonManager<Message>.Directive>(address: ._singletonManager(name: self.settings.name), system: system)
9493
let managerRef = system._resolve(context: resolveContext)
94+
// If the manager is not running this will end up in dead-letters but that's fine
9595
managerRef.tell(.stop)
9696

9797
// We don't control the proxy's directives so we can't tell it to stop
98-
return .success(())
9998
}
10099
}
101100

102101
// ==== ----------------------------------------------------------------------------------------------------------------
103-
// MARK: ActorSingleton settings
102+
// MARK: Actor singleton settings
104103

105104
/// Settings for a `ActorSingleton`.
106105
public struct ActorSingletonSettings {
@@ -125,7 +124,7 @@ public struct ActorSingletonSettings {
125124

126125
/// Singleton node allocation strategies.
127126
public enum AllocationStrategySettings {
128-
/// Singletons will run on the cluster leader
127+
/// Singletons will run on the cluster leader. *All* nodes are potential candidates.
129128
case byLeadership
130129

131130
func make(_: ClusterSettings, _: ActorSingletonSettings) -> ActorSingletonAllocationStrategy {

Sources/ActorSingletonPlugin/ActorSingletonManager.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,6 @@ extension ActorSingletonManager {
9999
// MARK: ActorSingletonManager path / address
100100

101101
extension ActorAddress {
102-
internal static func _singletonManager(name: String, on node: UniqueNode) -> ActorAddress {
103-
.init(node: node, path: ._singletonManager(name: name), incarnation: .wellKnown)
104-
}
105-
106102
internal static func _singletonManager(name: String) -> ActorAddress {
107103
.init(path: ._singletonManager(name: name), incarnation: .wellKnown)
108104
}

Sources/ActorSingletonPlugin/ActorSingletonPlugin.swift

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,150 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import DistributedActors
16+
import DistributedActorsConcurrencyHelpers
17+
18+
// ==== ----------------------------------------------------------------------------------------------------------------
19+
// MARK: Actor singleton plugin
20+
21+
/// The actor singleton plugin ensures that there is no more than one instance of an actor that is defined to be
22+
/// singleton running in the cluster.
23+
///
24+
/// An actor singleton may run on any node in the cluster. Use `ActorSingletonSettings.allocationStrategy` to control
25+
/// its allocation. On candidate nodes where the singleton might run, use `ActorSystem.singleton.ref(type:name:props:behavior)`
26+
/// to define actor behavior. Otherwise, call `ActorSystem.singleton.ref(type:name:)` to obtain a ref. The returned
27+
/// `ActorRef` is in reality a proxy which handle situations where the singleton is shifted to different nodes.
28+
///
29+
/// - Warning: Refer to the configured `AllocationStrategy` for trade-offs between safety and recovery latency for
30+
/// the singleton allocation.
31+
/// - SeeAlso: The `ActorSingleton` mechanism is conceptually similar to Erlang/OTP's <a href="http://erlang.org/doc/design_principles/distributed_applications.html">`DistributedApplication`</a>,
32+
/// and <a href="https://doc.akka.io/docs/akka/current/cluster-singleton.html">`ClusterSingleton` in Akka</a>.
33+
public final class ActorSingletonPlugin {
34+
private var singletons: [String: BoxedActorSingleton] = [:]
35+
private let singletonsLock = Lock()
36+
37+
public init() {}
38+
39+
func ref<Message>(of type: Message.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ behavior: Behavior<Message>? = nil) throws -> ActorRef<Message> {
40+
try self.singletonsLock.withLock {
41+
if let existing = self.singletons[settings.name] {
42+
guard let proxy = existing.unsafeUnwrapAs(Message.self).proxy else {
43+
fatalError("Singleton [\(settings.name)] not yet initialized")
44+
}
45+
return proxy
46+
}
47+
48+
let singleton = ActorSingleton<Message>(settings: settings, props: props, behavior)
49+
try singleton.spawnAll(system)
50+
self.singletons[settings.name] = BoxedActorSingleton(singleton)
51+
52+
guard let proxy = singleton.proxy else {
53+
fatalError("Singleton[\(settings.name)] not yet initialized")
54+
}
55+
56+
return proxy
57+
}
58+
}
59+
60+
func actor<Act: Actorable>(of type: Act.Type, settings: ActorSingletonSettings, system: ActorSystem, props: Props? = nil, _ makeInstance: ((Actor<Act>.Context) -> Act)? = nil) throws -> Actor<Act> {
61+
let behavior = makeInstance.map { maker in
62+
Behavior<Act.Message>.setup { context in
63+
Act.makeBehavior(instance: maker(.init(underlying: context)))
64+
}
65+
}
66+
let ref = try self.ref(of: Act.Message.self, settings: settings, system: system, behavior)
67+
return Actor<Act>(ref: ref)
68+
}
69+
}
70+
71+
extension ActorSingletonPlugin {
72+
func ref<Message>(of type: Message.Type, name: String, system: ActorSystem, props: Props? = nil, _ behavior: Behavior<Message>? = nil) throws -> ActorRef<Message> {
73+
let settings = ActorSingletonSettings(name: name)
74+
return try self.ref(of: type, settings: settings, system: system, props: props, behavior)
75+
}
76+
77+
func actor<Act: Actorable>(of type: Act.Type, name: String, system: ActorSystem, props: Props? = nil, _ makeInstance: ((Actor<Act>.Context) -> Act)? = nil) throws -> Actor<Act> {
78+
let settings = ActorSingletonSettings(name: name)
79+
return try self.actor(of: type, settings: settings, system: system, props: props, makeInstance)
80+
}
81+
}
82+
83+
// ==== ----------------------------------------------------------------------------------------------------------------
84+
// MARK: Plugin protocol conformance
85+
86+
extension ActorSingletonPlugin: Plugin {
87+
static let pluginKey = PluginKey<ActorSingletonPlugin>(plugin: "$actorSingleton")
88+
89+
public var key: Key {
90+
Self.pluginKey
91+
}
92+
93+
public func start(_ system: ActorSystem) -> Result<Void, Error> {
94+
.success(())
95+
}
96+
97+
// TODO: Future
98+
public func stop(_ system: ActorSystem) -> Result<Void, Error> {
99+
self.singletonsLock.withLock {
100+
for (_, singleton) in self.singletons {
101+
singleton.stop(system)
102+
}
103+
}
104+
return .success(())
105+
}
106+
}
107+
108+
// ==== ----------------------------------------------------------------------------------------------------------------
109+
// MARK: Singleton refs and actors
16110

17111
extension ActorSystem {
18-
public var singleton: ActorSingletonLookup {
112+
public var singleton: ActorSingletonControl {
19113
.init(self)
20114
}
21115
}
22116

23-
/// Allows for simplified lookups of actor references which are known to be managed by `ActorSingleton`.
24-
public struct ActorSingletonLookup {
117+
/// Provides actor singleton controls such as obtaining a singleton ref and defining the singleton.
118+
public struct ActorSingletonControl {
25119
private let system: ActorSystem
26120

27121
internal init(_ system: ActorSystem) {
28122
self.system = system
29123
}
30124

31-
/// Obtains a reference to a (proxy) singleton regardless of its current location.
32-
public func ref<Message>(name: String, of type: Message.Type) throws -> ActorRef<Message> {
33-
let key = ActorSingleton<Message>.pluginKey(name: name)
34-
guard let singleton = self.system.settings.plugins[key] else {
125+
private var singletonPlugin: ActorSingletonPlugin {
126+
let key = ActorSingletonPlugin.pluginKey
127+
guard let singletonPlugin = self.system.settings.plugins[key] else {
35128
fatalError("No plugin found for key: [\(key)], installed plugins: \(self.system.settings.plugins)")
36129
}
37-
guard let proxy = singleton.proxy else {
38-
fatalError("Singleton[\(key)] not yet initialized")
39-
}
40-
return proxy // FIXME: Worried that we never synchronize access to proxy...
130+
return singletonPlugin
41131
}
42132

43-
public func actor<Act: Actorable>(name: String, _ type: Act.Type) throws -> Actor<Act> {
44-
let ref = try self.ref(name: name, of: Act.Message.self)
45-
return Actor<Act>(ref: ref)
133+
/// Defines a singleton `behavior` and indicates that it can be hosted on this node.
134+
public func host<Message>(_ type: Message.Type, name: String, props: Props = Props(), _ behavior: Behavior<Message>) throws -> ActorRef<Message> {
135+
try self.singletonPlugin.ref(of: type, name: name, system: self.system, props: props, behavior)
136+
}
137+
138+
/// Defines a singleton `behavior` and indicates that it can be hosted on this node.
139+
public func host<Message>(_ type: Message.Type, settings: ActorSingletonSettings, props: Props = Props(), _ behavior: Behavior<Message>) throws -> ActorRef<Message> {
140+
try self.singletonPlugin.ref(of: type, settings: settings, system: self.system, props: props, behavior)
141+
}
142+
143+
/// Defines a singleton `Actorable` and indicates that it can be hosted on this node.
144+
public func host<Act: Actorable>(_ type: Act.Type, name: String, props: Props = Props(), _ makeInstance: @escaping (Actor<Act>.Context) -> Act) throws -> Actor<Act> {
145+
try self.singletonPlugin.actor(of: type, name: name, system: self.system, props: props, makeInstance)
146+
}
147+
148+
/// Defines a singleton `Actorable` and indicates that it can be hosted on this node.
149+
public func host<Act: Actorable>(_ type: Act.Type, settings: ActorSingletonSettings, props: Props = Props(), _ makeInstance: @escaping (Actor<Act>.Context) -> Act) throws -> Actor<Act> {
150+
try self.singletonPlugin.actor(of: type, settings: settings, system: self.system, props: props, makeInstance)
151+
}
152+
153+
/// Obtains a ref to the specified actor singleton.
154+
public func ref<Message>(of type: Message.Type, name: String) throws -> ActorRef<Message> {
155+
try self.singletonPlugin.ref(of: type, name: name, system: self.system)
156+
}
157+
158+
/// Obtains the specified singleton actor.
159+
public func actor<Act: Actorable>(of type: Act.Type, name: String) throws -> Actor<Act> {
160+
try self.singletonPlugin.actor(of: type, name: name, system: self.system)
46161
}
47162
}

Sources/ActorSingletonPlugin/ActorSingletonProxy.swift

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ import Logging
2828
/// would be disposed to allow insertion of the latest message.
2929
///
3030
/// The proxy subscribes to events and feeds them into `AllocationStrategy` to determine the node that the
31-
/// singleton runs on. It spawns a `ActorSingletonManager`, which manages the actual singleton actor, as needed and
32-
/// obtains the ref from it. It instructs the `ActorSingletonManager` to hand over the singleton when the node changes.
31+
/// singleton runs on. If the singleton falls on *this* node, the proxy will spawn a `ActorSingletonManager`,
32+
/// which manages the actual singleton actor, and obtain the ref from it. The proxy instructs the
33+
/// `ActorSingletonManager` to hand over the singleton whenever the node changes.
3334
internal class ActorSingletonProxy<Message> {
3435
/// Settings for the `ActorSingleton`
3536
private let settings: ActorSingletonSettings
@@ -38,9 +39,11 @@ internal class ActorSingletonProxy<Message> {
3839
private let allocationStrategy: ActorSingletonAllocationStrategy
3940

4041
/// Props of the singleton behavior
41-
private let singletonProps: Props
42-
/// The singleton behavior
43-
private let singletonBehavior: Behavior<Message>
42+
private let singletonProps: Props?
43+
/// The singleton behavior.
44+
/// If `nil`, then this node is not a candidate for hosting the singleton. It would result
45+
/// in a failure if `allocationStrategy` selects this node by mistake.
46+
private let singletonBehavior: Behavior<Message>?
4447

4548
/// The node that the singleton runs on
4649
private var targetNode: UniqueNode?
@@ -54,7 +57,7 @@ internal class ActorSingletonProxy<Message> {
5457
/// Message buffer in case singleton `ref` is `nil`
5558
private let buffer: StashBuffer<Message>
5659

57-
init(settings: ActorSingletonSettings, allocationStrategy: ActorSingletonAllocationStrategy, props: Props, _ behavior: Behavior<Message>) {
60+
init(settings: ActorSingletonSettings, allocationStrategy: ActorSingletonAllocationStrategy, props: Props? = nil, _ behavior: Behavior<Message>? = nil) {
5861
self.settings = settings
5962
self.allocationStrategy = allocationStrategy
6063
self.singletonProps = props
@@ -120,10 +123,14 @@ internal class ActorSingletonProxy<Message> {
120123
}
121124

122125
private func takeOver(_ context: ActorContext<Message>, from: UniqueNode?) throws {
126+
guard let singletonBehavior = self.singletonBehavior else {
127+
preconditionFailure("The actor singleton \(self.settings.name) cannot run on this node. Please review AllocationStrategySettings and/or actor singleton usage.")
128+
}
129+
123130
// Spawn the manager then tell it to spawn the singleton actor
124131
self.managerRef = try context.system._spawnSystemActor(
125132
"singletonManager-\(self.settings.name)",
126-
ActorSingletonManager(settings: self.settings, props: self.singletonProps, self.singletonBehavior).behavior,
133+
ActorSingletonManager(settings: self.settings, props: self.singletonProps ?? Props(), singletonBehavior).behavior,
127134
props: ._wellKnown
128135
)
129136
// Need the manager to tell us the ref because we can't resolve it due to random incarnation

Sources/DistributedActors/ActorSystem.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@ public final class ActorSystem {
5050

5151
internal let _root: _ReceivesSystemMessages
5252

53-
private let terminationLock = Lock()
54-
5553
/// Allows inspecting settings that were used to configure this actor system.
5654
/// Settings are immutable and may not be changed once the system is running.
5755
public let settings: ActorSystemSettings

0 commit comments

Comments
 (0)