@@ -71,10 +71,12 @@ public struct ClusterControl {
7171 }
7272 }
7373
74+ private let cluster : ClusterShell ?
7475 internal let ref : ClusterShell . Ref
7576
76- init ( _ settings: ClusterSystemSettings , clusterRef: ClusterShell . Ref , eventStream: EventStream < Cluster . Event > ) {
77+ init ( _ settings: ClusterSystemSettings , cluster : ClusterShell ? , clusterRef: ClusterShell . Ref , eventStream: EventStream < Cluster . Event > ) {
7778 self . settings = settings
79+ self . cluster = cluster
7880 self . ref = clusterRef
7981 self . events = eventStream
8082
@@ -155,4 +157,135 @@ public struct ClusterControl {
155157 public func down( member: Cluster . Member ) {
156158 self . ref. tell ( . command( . downCommandMember( member) ) )
157159 }
160+
161+ /// Wait, within the given duration, until this actor system has joined the node's cluster.
162+ ///
163+ /// - Parameters
164+ /// - node: The node to be joined by this system.
165+ /// - within: Duration to wait for.
166+ ///
167+ /// - Returns `Cluster.Member` for the joined node.
168+ public func joined( node: UniqueNode , within: Duration ) async throws -> Cluster . Member {
169+ try await self . waitFor ( node, . up, within: within)
170+ }
171+
172+ /// Wait, within the given duration, for this actor system to be a member of all the nodes' respective cluster and have the specified status.
173+ ///
174+ /// - Parameters
175+ /// - nodes: The nodes to be joined by this system.
176+ /// - status: The expected member status.
177+ /// - within: Duration to wait for.
178+ public func waitFor( _ nodes: Set < UniqueNode > , _ status: Cluster . MemberStatus , within: Duration ) async throws {
179+ try await withThrowingTaskGroup ( of: Void . self) { group in
180+ for node in nodes {
181+ group. addTask {
182+ _ = try await self . waitFor ( node, status, within: within)
183+ }
184+ }
185+ // loop explicitly to propagagte any error that might have been thrown
186+ for try await _ in group { }
187+ }
188+ }
189+
190+ /// Wait, within the given duration, for this actor system to be a member of all the nodes' respective cluster and have **at least** the specified status.
191+ ///
192+ /// - Parameters
193+ /// - nodes: The nodes to be joined by this system.
194+ /// - status: The minimum expected member status.
195+ /// - within: Duration to wait for.
196+ public func waitFor( _ nodes: Set < UniqueNode > , atLeast atLeastStatus: Cluster . MemberStatus , within: Duration ) async throws {
197+ try await withThrowingTaskGroup ( of: Void . self) { group in
198+ for node in nodes {
199+ group. addTask {
200+ _ = try await self . waitFor ( node, atLeast: atLeastStatus, within: within)
201+ }
202+ }
203+ // loop explicitly to propagagte any error that might have been thrown
204+ for try await _ in group { }
205+ }
206+ }
207+
208+ /// Wait, within the given duration, for this actor system to be a member of the node's cluster and have the specified status.
209+ ///
210+ /// - Parameters
211+ /// - node: The node to be joined by this system.
212+ /// - status: The expected member status.
213+ /// - within: Duration to wait for.
214+ ///
215+ /// - Returns `Cluster.Member` for the joined node with the expected status.
216+ /// If the expected status is `.down` or `.removed`, and the node is already known to have been removed from the cluster
217+ /// a synthesized `Cluster/MemberStatus/removed` (and `.unreachable`) member is returned.
218+ public func waitFor( _ node: UniqueNode , _ status: Cluster . MemberStatus , within: Duration ) async throws -> Cluster . Member {
219+ try await self . waitForMembershipEventually ( within: within) { membership in
220+ if status == . down || status == . removed {
221+ if let cluster = self . cluster, let tombstone = cluster. getExistingAssociationTombstone ( with: node) {
222+ return Cluster . Member ( node: node, status: . removed) . asUnreachable
223+ }
224+ }
225+
226+ guard let foundMember = membership. uniqueMember ( node) else {
227+ if status == . down || status == . removed {
228+ // so we're seeing an already removed member, this can indeed happen and is okey
229+ return Cluster . Member ( node: node, status: . removed) . asUnreachable
230+ }
231+ throw Cluster . MembershipError. notFound ( node, in: membership)
232+ }
233+
234+ if status != foundMember. status {
235+ throw Cluster . MembershipError. statusRequirementNotMet ( expected: status, found: foundMember)
236+ }
237+ return foundMember
238+ }
239+ }
240+
241+ /// Wait, within the given duration, for this actor system to be a member of the node's cluster and have **at least** the specified status.
242+ ///
243+ /// - Parameters
244+ /// - node: The node to be joined by this system.
245+ /// - atLeastStatus: The minimum expected member status.
246+ /// - within: Duration to wait for.
247+ ///
248+ /// - Returns `Cluster.Member` for the joined node with the minimum expected status.
249+ /// If the expected status is at least `.down` or `.removed`, and either a tombstone exists for the node or the associated
250+ /// membership is not found, the `Cluster.Member` returned would have `.removed` status and *unreachable*.
251+ public func waitFor( _ node: UniqueNode , atLeast atLeastStatus: Cluster . MemberStatus , within: Duration ) async throws -> Cluster . Member {
252+ try await self . waitForMembershipEventually ( within: within) { membership in
253+ if atLeastStatus == . down || atLeastStatus == . removed {
254+ if let cluster = self . cluster, let tombstone = cluster. getExistingAssociationTombstone ( with: node) {
255+ return Cluster . Member ( node: node, status: . removed) . asUnreachable
256+ }
257+ }
258+
259+ guard let foundMember = membership. uniqueMember ( node) else {
260+ if atLeastStatus == . down || atLeastStatus == . removed {
261+ // so we're seeing an already removed member, this can indeed happen and is okey
262+ return Cluster . Member ( node: node, status: . removed) . asUnreachable
263+ }
264+ throw Cluster . MembershipError. notFound ( node, in: membership)
265+ }
266+
267+ if atLeastStatus <= foundMember. status {
268+ throw Cluster . MembershipError. atLeastStatusRequirementNotMet ( expectedAtLeast: atLeastStatus, found: foundMember)
269+ }
270+ return foundMember
271+ }
272+ }
273+
274+ private func waitForMembershipEventually< T> ( within: Duration , interval: Duration = . milliseconds( 100 ) , _ block: ( Cluster . Membership ) async throws -> T ) async throws -> T {
275+ let deadline = ContinuousClock . Instant. fromNow ( within)
276+
277+ var lastError : Error ?
278+ while deadline. hasTimeLeft ( ) {
279+ let membership = await self . membershipSnapshot
280+ do {
281+ let result = try await block ( membership)
282+ return result
283+ } catch {
284+ lastError = error
285+ try await Task . sleep ( nanoseconds: UInt64 ( interval. nanoseconds) )
286+ }
287+ }
288+
289+ throw Cluster . MembershipError. awaitStatusTimedOut ( within, lastError)
290+ }
158291}
0 commit comments