Skip to content

Commit 435af70

Browse files
Merge pull request #128 from ably-labs/50-trigger-RETRY
[ECO-4985] Trigger the RETRY operation where spec says to
2 parents eacfcb7 + 82fed05 commit 435af70

File tree

4 files changed

+235
-54
lines changed

4 files changed

+235
-54
lines changed

Sources/AblyChat/Room.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ internal actor DefaultRoom<LifecycleManagerFactory: RoomLifecycleManagerFactory>
218218
// MARK: - Room status
219219

220220
internal func onStatusChange(bufferingPolicy: BufferingPolicy) async -> Subscription<RoomStatusChange> {
221-
await lifecycleManager.onChange(bufferingPolicy: bufferingPolicy)
221+
await lifecycleManager.onRoomStatusChange(bufferingPolicy: bufferingPolicy)
222222
}
223223

224224
internal var status: RoomStatus {

Sources/AblyChat/RoomLifecycleManager.swift

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ internal protocol RoomLifecycleManager: Sendable {
4545
func performDetachOperation() async throws
4646
func performReleaseOperation() async
4747
var roomStatus: RoomStatus { get async }
48-
func onChange(bufferingPolicy: BufferingPolicy) async -> Subscription<RoomStatusChange>
48+
func onRoomStatusChange(bufferingPolicy: BufferingPolicy) async -> Subscription<RoomStatusChange>
4949
func waitToBeAbleToPerformPresenceOperations(requestedByFeature requester: RoomFeature) async throws(ARTErrorInfo)
5050
}
5151

@@ -88,7 +88,7 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
8888
private var contributorAnnotations: ContributorAnnotations
8989
private var listenForStateChangesTask: Task<Void, Never>!
9090
// TODO: clean up old subscriptions (https://github.com/ably-labs/ably-chat-swift/issues/36)
91-
private var subscriptions: [Subscription<RoomStatusChange>] = []
91+
private var roomStatusChangeSubscriptions: [Subscription<RoomStatusChange>] = []
9292
private var operationResultContinuations = OperationResultContinuations()
9393

9494
// MARK: - Initializers and `deinit`
@@ -188,7 +188,8 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
188188
case detaching(detachOperationID: UUID)
189189
case detached
190190
case detachedDueToRetryOperation(retryOperationID: UUID)
191-
case suspendedAwaitingStartOfRetryOperation(error: ARTErrorInfo)
191+
// `retryOperationTask` is exposed so that tests can wait for the triggered RETRY operation to complete.
192+
case suspendedAwaitingStartOfRetryOperation(retryOperationTask: Task<Void, Never>, error: ARTErrorInfo)
192193
case suspended(retryOperationID: UUID, error: ARTErrorInfo)
193194
case failed(error: ARTErrorInfo)
194195
case releasing(releaseOperationID: UUID)
@@ -210,7 +211,7 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
210211
.detaching
211212
case .detached, .detachedDueToRetryOperation:
212213
.detached
213-
case let .suspendedAwaitingStartOfRetryOperation(error):
214+
case let .suspendedAwaitingStartOfRetryOperation(_, error):
214215
.suspended(error: error)
215216
case let .suspended(_, error):
216217
.suspended(error: error)
@@ -318,12 +319,36 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
318319
status.toRoomStatus
319320
}
320321

321-
internal func onChange(bufferingPolicy: BufferingPolicy) -> Subscription<RoomStatusChange> {
322+
internal func onRoomStatusChange(bufferingPolicy: BufferingPolicy) -> Subscription<RoomStatusChange> {
322323
let subscription: Subscription<RoomStatusChange> = .init(bufferingPolicy: bufferingPolicy)
323-
subscriptions.append(subscription)
324+
roomStatusChangeSubscriptions.append(subscription)
324325
return subscription
325326
}
326327

328+
#if DEBUG
329+
// TODO: clean up old subscriptions (https://github.com/ably-labs/ably-chat-swift/issues/36)
330+
/// Supports the ``testsOnly_onRoomStatusChange()`` method.
331+
private var statusChangeSubscriptions: [Subscription<StatusChange>] = []
332+
333+
internal struct StatusChange {
334+
internal var current: Status
335+
internal var previous: Status
336+
}
337+
338+
/// Allows tests to subscribe to changes to the manager’s internal status (which exposes more cases and additional metadata, compared to the ``RoomStatus`` exposed by ``onRoomStatusChange(bufferingPolicy:)``).
339+
internal func testsOnly_onStatusChange() -> Subscription<StatusChange> {
340+
let subscription: Subscription<StatusChange> = .init(bufferingPolicy: .unbounded)
341+
statusChangeSubscriptions.append(subscription)
342+
return subscription
343+
}
344+
345+
internal func emitStatusChange(_ change: StatusChange) {
346+
for subscription in statusChangeSubscriptions {
347+
subscription.emit(change)
348+
}
349+
}
350+
#endif
351+
327352
/// Updates ``status`` and emits a status change event.
328353
private func changeStatus(to new: Status) {
329354
logger.log(message: "Transitioning from \(status) to \(new)", level: .info)
@@ -333,12 +358,17 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
333358
// Avoid a double-emit of room status when changing from `.suspendedAwaitingStartOfRetryOperation` to `.suspended`.
334359
if new.toRoomStatus != previous.toRoomStatus {
335360
let statusChange = RoomStatusChange(current: status.toRoomStatus, previous: previous.toRoomStatus)
336-
emitStatusChange(statusChange)
361+
emitRoomStatusChange(statusChange)
337362
}
363+
364+
#if DEBUG
365+
let statusChange = StatusChange(current: status, previous: previous)
366+
emitStatusChange(statusChange)
367+
#endif
338368
}
339369

340-
private func emitStatusChange(_ change: RoomStatusChange) {
341-
for subscription in subscriptions {
370+
private func emitRoomStatusChange(_ change: RoomStatusChange) {
371+
for subscription in roomStatusChangeSubscriptions {
342372
subscription.emit(change)
343373
}
344374
}
@@ -480,7 +510,14 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
480510

481511
clearTransientDisconnectTimeouts()
482512

483-
changeStatus(to: .suspendedAwaitingStartOfRetryOperation(error: reason))
513+
// My understanding is that, since this task is being created inside an actor’s synchronous code, the two .suspended* statuses will always come in the right order; i.e. first .suspendedAwaitingStartOfRetryOperation and then .suspended.
514+
let retryOperationTask = scheduleAnOperation(
515+
kind: .retry(
516+
triggeringContributor: contributor,
517+
errorForSuspendedStatus: reason
518+
)
519+
)
520+
changeStatus(to: .suspendedAwaitingStartOfRetryOperation(retryOperationTask: retryOperationTask, error: reason))
484521
}
485522
case .attaching:
486523
if !hasOperationInProgress, !contributorAnnotations[contributor].hasTransientDisconnectTimeout {
@@ -690,6 +727,27 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
690727
try result.get()
691728
}
692729

730+
/// The kinds of operation that you can schedule using ``scheduleAnOperation(kind:)``.
731+
private enum OperationKind {
732+
/// The RETRY operation.
733+
case retry(triggeringContributor: Contributor, errorForSuspendedStatus: ARTErrorInfo)
734+
}
735+
736+
/// Requests that a room lifecycle operation be performed asynchronously.
737+
private func scheduleAnOperation(kind: OperationKind) -> Task<Void, Never> {
738+
logger.log(message: "Scheduling operation \(kind)", level: .debug)
739+
return Task {
740+
logger.log(message: "Performing scheduled operation \(kind)", level: .debug)
741+
switch kind {
742+
case let .retry(triggeringContributor, errorForSuspendedStatus):
743+
await performRetryOperation(
744+
triggeredByContributor: triggeringContributor,
745+
errorForSuspendedStatus: errorForSuspendedStatus
746+
)
747+
}
748+
}
749+
}
750+
693751
// MARK: - ATTACH operation
694752

695753
internal func performAttachOperation() async throws {
@@ -752,9 +810,16 @@ internal actor DefaultRoomLifecycleManager<Contributor: RoomLifecycleContributor
752810
case .suspended:
753811
// CHA-RL1h2
754812
let error = ARTErrorInfo(chatError: .attachmentFailed(feature: contributor.feature, underlyingError: contributorAttachError))
755-
changeStatus(to: .suspendedAwaitingStartOfRetryOperation(error: error))
756813

757814
// CHA-RL1h3
815+
// My understanding is that, since this task is being created inside an actor’s synchronous code, the two .suspended* statuses will always come in the right order; i.e. first .suspendedAwaitingStartOfRetryOperation and then .suspended.
816+
let retryOperationTask = scheduleAnOperation(
817+
kind: .retry(
818+
triggeringContributor: contributor,
819+
errorForSuspendedStatus: error
820+
)
821+
)
822+
changeStatus(to: .suspendedAwaitingStartOfRetryOperation(retryOperationTask: retryOperationTask, error: error))
758823
throw error
759824
case .failed:
760825
// CHA-RL1h4

0 commit comments

Comments
 (0)