Skip to content

Commit a9fffc2

Browse files
committed
Merge branch 'feature/roomlifecycle-attach-with-retry' into tests/roomlifecycle-attach
# Conflicts: # chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt # chat-android/src/main/java/com/ably/chat/RoomReactions.kt # chat-android/src/main/java/com/ably/chat/Utils.kt
2 parents e73b9ba + 290fb84 commit a9fffc2

20 files changed

+335
-163
lines changed

chat-android/src/main/java/com/ably/chat/AtomicCoroutineScope.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,15 @@ class AtomicCoroutineScope(private val scope: CoroutineScope = CoroutineScope(Di
3737
private var isRunning = false // Only accessed from sequentialScope
3838
private var queueCounter = 0 // Only accessed from synchronized method
3939

40+
val finishedProcessing: Boolean
41+
get() = jobs.isEmpty() && !isRunning
42+
43+
val pendingJobCount: Int
44+
get() = jobs.count()
45+
4046
/**
41-
* @param priority Defines priority for the operation execution.
42-
* @param coroutineBlock Suspended function that needs to be executed mutually exclusive under given scope.
47+
* Defines priority for the operation execution and
48+
* executes given coroutineBlock mutually exclusive under given scope.
4349
*/
4450
@Synchronized
4551
fun <T : Any>async(priority: Int = 0, coroutineBlock: suspend CoroutineScope.() -> T): CompletableDeferred<T> {
@@ -77,12 +83,6 @@ class AtomicCoroutineScope(private val scope: CoroutineScope = CoroutineScope(Di
7783
}
7884
}
7985

80-
val finishedProcessing: Boolean
81-
get() = jobs.isEmpty() && !isRunning
82-
83-
val pendingJobCount: Int
84-
get() = jobs.count()
85-
8686
/**
8787
* Cancels ongoing and pending operations with given error.
8888
*/

chat-android/src/main/java/com/ably/chat/Discontinuities.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@ interface EmitsDiscontinuities {
4444
}
4545
}
4646

47-
open class DiscontinuityEmitter : EventEmitter<String, EmitsDiscontinuities.Listener>() {
47+
class DiscontinuityEmitter : EventEmitter<String, EmitsDiscontinuities.Listener>() {
4848
override fun apply(listener: EmitsDiscontinuities.Listener?, event: String?, vararg args: Any?) {
4949
try {
50-
listener?.discontinuityEmitted(args[0] as ErrorInfo?)
50+
listener?.discontinuityEmitted(args[0] as? ErrorInfo?)
5151
} catch (t: Throwable) {
5252
Log.e("DiscontinuityEmitter", "Unexpected exception calling Discontinuity Listener", t)
5353
}

chat-android/src/main/java/com/ably/chat/Messages.kt

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ internal class DefaultMessages(
225225
private val roomId: String,
226226
realtimeChannels: AblyRealtime.Channels,
227227
private val chatApi: ChatApi,
228-
) : Messages, ContributesToRoomLifecycle, ResolvedContributor {
228+
) : Messages, ContributesToRoomLifecycleImpl(), ResolvedContributor {
229229

230230
private var listeners: Map<Messages.Listener, DeferredValue<String>> = emptyMap()
231231

@@ -303,19 +303,6 @@ internal class DefaultMessages(
303303

304304
override suspend fun send(params: SendMessageParams): Message = chatApi.sendMessage(roomId, params)
305305

306-
private val discontinuityEmitter = DiscontinuityEmitter()
307-
308-
override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Subscription {
309-
discontinuityEmitter.on(listener)
310-
return Subscription {
311-
discontinuityEmitter.off(listener)
312-
}
313-
}
314-
315-
override fun discontinuityDetected(reason: ErrorInfo?) {
316-
discontinuityEmitter.emit("discontinuity", reason)
317-
}
318-
319306
fun release() {
320307
channel.off(channelStateListener)
321308
}

chat-android/src/main/java/com/ably/chat/Occupancy.kt

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
package com.ably.chat
44

5-
import io.ably.lib.types.ErrorInfo
65
import io.ably.lib.realtime.Channel as AblyRealtimeChannel
76

87
/**
@@ -62,7 +61,7 @@ data class OccupancyEvent(
6261

6362
internal class DefaultOccupancy(
6463
private val messages: Messages,
65-
) : Occupancy, ContributesToRoomLifecycle, ResolvedContributor {
64+
) : Occupancy, ContributesToRoomLifecycleImpl(), ResolvedContributor {
6665

6766
override val featureName: String = "occupancy"
6867

@@ -81,17 +80,4 @@ internal class DefaultOccupancy(
8180
override suspend fun get(): OccupancyEvent {
8281
TODO("Not yet implemented")
8382
}
84-
85-
private val discontinuityEmitter = DiscontinuityEmitter()
86-
87-
override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Subscription {
88-
discontinuityEmitter.on(listener)
89-
return Subscription {
90-
discontinuityEmitter.off(listener)
91-
}
92-
}
93-
94-
override fun discontinuityDetected(reason: ErrorInfo?) {
95-
discontinuityEmitter.emit("discontinuity", reason)
96-
}
9783
}

chat-android/src/main/java/com/ably/chat/Presence.kt

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ data class PresenceEvent(
132132

133133
internal class DefaultPresence(
134134
private val messages: Messages,
135-
) : Presence, ContributesToRoomLifecycle, ResolvedContributor {
135+
) : Presence, ContributesToRoomLifecycleImpl(), ResolvedContributor {
136136

137137
override val featureName = "presence"
138138

@@ -167,17 +167,4 @@ internal class DefaultPresence(
167167
override fun subscribe(listener: Presence.Listener): Subscription {
168168
TODO("Not yet implemented")
169169
}
170-
171-
private val discontinuityEmitter = DiscontinuityEmitter()
172-
173-
override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Subscription {
174-
discontinuityEmitter.on(listener)
175-
return Subscription {
176-
discontinuityEmitter.off(listener)
177-
}
178-
}
179-
180-
override fun discontinuityDetected(reason: ErrorInfo?) {
181-
discontinuityEmitter.emit("discontinuity", reason)
182-
}
183170
}

chat-android/src/main/java/com/ably/chat/Room.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@file:Suppress("StringLiteralDuplication", "NotImplementedDeclaration")
22

33
package com.ably.chat
4+
45
import io.ably.lib.util.Log.LogHandler
56
import kotlinx.coroutines.CoroutineName
67
import kotlinx.coroutines.CoroutineScope
@@ -124,7 +125,8 @@ internal class DefaultRoom(
124125

125126
override val reactions = DefaultRoomReactions(
126127
roomId = roomId,
127-
realtimeClient = realtimeClient,
128+
clientId = realtimeClient.auth.clientId,
129+
realtimeChannels = realtimeClient.channels,
128130
)
129131

130132
override val occupancy = DefaultOccupancy(

chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt

Lines changed: 71 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@ interface ContributesToRoomLifecycle : EmitsDiscontinuities, HandlesDiscontinuit
4242
val detachmentErrorCode: ErrorCodes
4343
}
4444

45+
abstract class ContributesToRoomLifecycleImpl : ContributesToRoomLifecycle {
46+
47+
private val discontinuityEmitter = DiscontinuityEmitter()
48+
49+
override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Subscription {
50+
discontinuityEmitter.on(listener)
51+
return Subscription {
52+
discontinuityEmitter.off(listener)
53+
}
54+
}
55+
56+
override fun discontinuityDetected(reason: ErrorInfo?) {
57+
discontinuityEmitter.emit("discontinuity", reason)
58+
}
59+
}
60+
4561
/**
4662
* This interface represents a feature that contributes to the room lifecycle and
4763
* exposes its channel directly. Objects of this type are created by awaiting the
@@ -78,27 +94,29 @@ interface RoomAttachmentResult : NewRoomStatus {
7894
}
7995

8096
class DefaultRoomAttachmentResult : RoomAttachmentResult {
81-
internal var _failedFeature: ResolvedContributor? = null
82-
internal var _status: RoomLifecycle = RoomLifecycle.Attached
83-
internal var _error: ErrorInfo? = null
97+
internal var statusField: RoomLifecycle = RoomLifecycle.Attached
98+
override val status: RoomLifecycle
99+
get() = statusField
84100

101+
internal var failedFeatureField: ResolvedContributor? = null
85102
override val failedFeature: ResolvedContributor?
86-
get() = _failedFeature
103+
get() = failedFeatureField
87104

88-
override val exception: AblyException
89-
get() = AblyException.fromErrorInfo(
90-
_error
91-
?: ErrorInfo(
92-
"unknown error in attach for ${failedFeature?.contributor?.featureName} feature",
93-
500, ErrorCodes.RoomLifecycleError.errorCode,
94-
),
95-
)
105+
internal var errorField: ErrorInfo? = null
106+
override val error: ErrorInfo?
107+
get() = errorField
96108

97-
override val status: RoomLifecycle
98-
get() = _status
109+
internal var throwable: Throwable? = null
99110

100-
override val error: ErrorInfo?
101-
get() = _error
111+
override val exception: AblyException
112+
get() {
113+
val errorInfo = errorField
114+
?: ErrorInfo("unknown error in attach", HttpStatusCodes.InternalServerError, ErrorCodes.RoomLifecycleError.errorCode)
115+
throwable?.let {
116+
return AblyException.fromErrorInfo(throwable, errorInfo)
117+
}
118+
return AblyException.fromErrorInfo(errorInfo)
119+
}
102120
}
103121

104122
/**
@@ -153,6 +171,11 @@ class RoomLifecycleManager
153171
*/
154172
private val _firstAttachesCompleted = mutableMapOf<ResolvedContributor, Boolean>()
155173

174+
/**
175+
* Retry duration in milliseconds, used by internal doRetry and runDownChannelsOnFailedAttach methods
176+
*/
177+
private val _retryDurationInMs: Long = 250
178+
156179
init {
157180
if (_status.current != RoomLifecycle.Attached) {
158181
_operationInProgress = true
@@ -183,6 +206,7 @@ class RoomLifecycleManager
183206
* @param contributor The contributor that has entered a suspended state.
184207
* @returns Returns when the room is attached, or the room enters a failed state.
185208
*/
209+
@SuppressWarnings("CognitiveComplexMethod")
186210
private suspend fun doRetry(contributor: ResolvedContributor) {
187211
// Handle the channel wind-down for other channels
188212
var result = kotlin.runCatching { doChannelWindDown(contributor) }
@@ -191,7 +215,7 @@ class RoomLifecycleManager
191215
if (this._status.current === RoomLifecycle.Failed) {
192216
error("room is in a failed state")
193217
}
194-
delay(250)
218+
delay(_retryDurationInMs)
195219
result = kotlin.runCatching { doChannelWindDown(contributor) }
196220
}
197221

@@ -214,7 +238,11 @@ class RoomLifecycleManager
214238
val failedFeature = attachmentResult.failedFeature
215239
if (failedFeature == null) {
216240
AblyException.fromErrorInfo(
217-
ErrorInfo("no failed feature in doRetry", 500, ErrorCodes.RoomLifecycleError.errorCode),
241+
ErrorInfo(
242+
"no failed feature in doRetry",
243+
HttpStatusCodes.InternalServerError,
244+
ErrorCodes.RoomLifecycleError.errorCode,
245+
),
218246
)
219247
}
220248
// No need to catch errors, rather they should propagate to caller method
@@ -248,7 +276,7 @@ class RoomLifecycleManager
248276
contributor.channel.once(ChannelState.failed) {
249277
val exception = AblyException.fromErrorInfo(
250278
it.reason
251-
?: ErrorInfo("unknown error in _doRetry", 500, ErrorCodes.RoomLifecycleError.errorCode),
279+
?: ErrorInfo("unknown error in _doRetry", HttpStatusCodes.InternalServerError, ErrorCodes.RoomLifecycleError.errorCode),
252280
)
253281
continuation.resumeWithException(exception)
254282
}
@@ -263,6 +291,7 @@ class RoomLifecycleManager
263291
* If a channel enters the failed state, we reject and then begin to wind down the other channels.
264292
* Spec: CHA-RL1
265293
*/
294+
@SuppressWarnings("ThrowsCount")
266295
internal suspend fun attach() {
267296
val deferredAttach = atomicCoroutineScope.async(LifecycleOperationPrecedence.AttachOrDetach.priority) { // CHA-RL1d
268297
when (_status.current) {
@@ -271,15 +300,15 @@ class RoomLifecycleManager
271300
throw AblyException.fromErrorInfo(
272301
ErrorInfo(
273302
"unable to attach room; room is releasing",
274-
500,
303+
HttpStatusCodes.InternalServerError,
275304
ErrorCodes.RoomIsReleasing.errorCode,
276305
),
277306
)
278307
RoomLifecycle.Released -> // CHA-RL1c
279308
throw AblyException.fromErrorInfo(
280309
ErrorInfo(
281310
"unable to attach room; room is released",
282-
500,
311+
HttpStatusCodes.InternalServerError,
283312
ErrorCodes.RoomIsReleased.errorCode,
284313
),
285314
)
@@ -306,7 +335,11 @@ class RoomLifecycleManager
306335
if (attachResult.status === RoomLifecycle.Suspended) {
307336
if (attachResult.failedFeature == null) {
308337
AblyException.fromErrorInfo(
309-
ErrorInfo("no failed feature in attach", 500, ErrorCodes.RoomLifecycleError.errorCode),
338+
ErrorInfo(
339+
"no failed feature in attach",
340+
HttpStatusCodes.InternalServerError,
341+
ErrorCodes.RoomLifecycleError.errorCode,
342+
),
310343
)
311344
}
312345
attachResult.failedFeature?.let {
@@ -337,29 +370,26 @@ class RoomLifecycleManager
337370
feature.channel.attachCoroutine()
338371
_firstAttachesCompleted[feature] = true
339372
} catch (ex: Throwable) { // CHA-RL1h - handle channel attach failure
340-
attachResult._failedFeature = feature
341-
attachResult._error = ErrorInfo(
373+
attachResult.throwable = ex
374+
attachResult.failedFeatureField = feature
375+
attachResult.errorField = ErrorInfo(
342376
"failed to attach ${feature.contributor.featureName} feature${feature.channel.errorMessage}",
343-
500,
377+
HttpStatusCodes.InternalServerError,
344378
feature.contributor.attachmentErrorCode.errorCode,
345379
)
346380

347381
// The current feature should be in one of two states, it will be either suspended or failed
348382
// If it's in suspended, we wind down the other channels and wait for the reattach
349383
// If it's failed, we can fail the entire room
350384
when (feature.channel.state) {
351-
ChannelState.suspended -> {
352-
attachResult._status = RoomLifecycle.Suspended
353-
}
354-
ChannelState.failed -> {
355-
attachResult._status = RoomLifecycle.Failed
356-
}
385+
ChannelState.suspended -> attachResult.statusField = RoomLifecycle.Suspended
386+
ChannelState.failed -> attachResult.statusField = RoomLifecycle.Failed
357387
else -> {
358-
attachResult._status = RoomLifecycle.Failed
359-
attachResult._error = ErrorInfo(
388+
attachResult.statusField = RoomLifecycle.Failed
389+
attachResult.errorField = ErrorInfo(
360390
"unexpected channel state in doAttach ${feature.channel.state}${feature.channel.errorMessage}",
361-
500,
362-
feature.contributor.attachmentErrorCode.errorCode,
391+
HttpStatusCodes.InternalServerError,
392+
ErrorCodes.RoomLifecycleError.errorCode,
363393
)
364394
}
365395
}
@@ -395,7 +425,7 @@ class RoomLifecycleManager
395425
while (channelWindDown.isFailure) { // CHA-RL1h6 - repeat until all channels are detached
396426
// Something went wrong during the wind down. After a short delay, to give others a turn, we should run down
397427
// again until we reach a suitable conclusion.
398-
delay(250)
428+
delay(_retryDurationInMs)
399429
channelWindDown = kotlin.runCatching { doChannelWindDown() }
400430
}
401431
}
@@ -408,6 +438,7 @@ class RoomLifecycleManager
408438
* @returns Success/Failure when all channels are detached or at least one of them fails.
409439
*
410440
*/
441+
@SuppressWarnings("CognitiveComplexMethod", "ComplexCondition")
411442
private suspend fun doChannelWindDown(except: ResolvedContributor? = null) = coroutineScope {
412443
_contributors.map { contributor: ResolvedContributor ->
413444
async {
@@ -417,12 +448,12 @@ class RoomLifecycleManager
417448
return@async
418449
}
419450
// If the room's already in the failed state, or it's releasing, we should not detach a failed channel
420-
if (
421-
(
451+
if ((
422452
_status.current === RoomLifecycle.Failed ||
423453
_status.current === RoomLifecycle.Releasing ||
424454
_status.current === RoomLifecycle.Released
425-
) && contributor.channel.state === ChannelState.failed
455+
) &&
456+
contributor.channel.state === ChannelState.failed
426457
) {
427458
return@async
428459
}
@@ -439,7 +470,7 @@ class RoomLifecycleManager
439470
) {
440471
val contributorError = ErrorInfo(
441472
"failed to detach feature",
442-
500,
473+
HttpStatusCodes.InternalServerError,
443474
contributor.contributor.detachmentErrorCode.errorCode,
444475
)
445476
_status.setStatus(RoomLifecycle.Failed, contributorError)

0 commit comments

Comments
 (0)