Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ECO-5009][CHA-RL1] RoomLifecycle ATTACH tests #37

Open
wants to merge 21 commits into
base: feature/roomlifecycle-attach-with-retry
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d990bd6
Added basic unit tests for room lifecyclemanager attach
sacOO7 Oct 25, 2024
0a6a45e
Merge branch 'feature/roomlifecycle-attach-with-retry' into tests/roo…
sacOO7 Oct 29, 2024
983ec92
Fixed RoomLifecycleManagerTest, added sequentialRoomScope property to…
sacOO7 Oct 29, 2024
18cc65d
Marked atomicCoroutineScope as internal to access from tests, added e…
sacOO7 Nov 5, 2024
30ad922
Refactored RoomLifecycleManagerTest, added more assertions for CHA-RL1d
sacOO7 Nov 5, 2024
0a9d391
Marked atomicCoroutineScope as private, accessed via reflection+exten…
sacOO7 Nov 5, 2024
52005c0
Bumped up io.mocck test dependency to latest version
sacOO7 Nov 6, 2024
b6f4dbd
Added channel attach room lifecycle spec CHA-RL1f, CHA-RL1g, updated
sacOO7 Nov 6, 2024
1d92116
Added mising unit test case blocks for channel attach
sacOO7 Nov 6, 2024
29a9782
Added test for spec CHA-RL1h1 channel attach, improved exception hand…
sacOO7 Nov 7, 2024
0f400b1
Merge branch 'feature/roomlifecycle-attach-with-retry' into tests/roo…
sacOO7 Nov 7, 2024
54a01c3
Refactored roomLifeCycle ATTACH tests with respective spec
sacOO7 Nov 7, 2024
2fee93d
Added channel attach retry test as per spec CHA-RL1h3
sacOO7 Nov 7, 2024
e6b46ff
Added channel attach unit test for contributor failure, get all remai…
sacOO7 Nov 7, 2024
f097e96
Added channel attach failure test, retry channel detach remaining cha…
sacOO7 Nov 7, 2024
e73b9ba
Annotated RoomLifeCycleManager code with right spec tags, refactored …
sacOO7 Nov 8, 2024
a9fffc2
Merge branch 'feature/roomlifecycle-attach-with-retry' into tests/roo…
sacOO7 Nov 8, 2024
774cc3b
Fixed linting issues for roomlifecycle attach tests
sacOO7 Nov 8, 2024
4fc33c7
Merge branch 'feature/roomlifecycle-attach-with-retry' into tests/roo…
sacOO7 Nov 11, 2024
097e82c
Added detach delay to fix test flakiness for channel detach
sacOO7 Nov 11, 2024
f4883bc
Refactored room attach tests with proper naming convention,
sacOO7 Nov 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Messages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ internal class DefaultMessages(
*/
private val messagesChannelName = "$roomId::\$chat::\$chatMessages"

override val featureName: String = "messages"

override val channel = realtimeChannels.get(messagesChannelName, ChatChannelOptions())

override val contributor: ContributesToRoomLifecycle = this
Expand Down
2 changes: 2 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Occupancy.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ internal class DefaultOccupancy(
private val messages: Messages,
) : Occupancy, ContributesToRoomLifecycleImpl(), ResolvedContributor {

override val featureName: String = "occupancy"

override val channel = messages.channel

override val contributor: ContributesToRoomLifecycle = this
Expand Down
2 changes: 2 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Presence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ internal class DefaultPresence(
private val messages: Messages,
) : Presence, ContributesToRoomLifecycleImpl(), ResolvedContributor {

override val featureName = "presence"

override val channel = messages.channel

override val contributor: ContributesToRoomLifecycle = this
Expand Down
81 changes: 56 additions & 25 deletions chat-android/src/main/java/com/ably/chat/RoomLifecycleManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import io.ably.lib.realtime.Channel as AblyRealtimeChannel
* An interface for features that contribute to the room status.
*/
interface ContributesToRoomLifecycle : EmitsDiscontinuities, HandlesDiscontinuity {
/**
* Name of the feature
*/
val featureName: String

/**
* Gets the channel on which the feature operates. This promise is never
* rejected except in the case where room initialization is canceled.
Expand Down Expand Up @@ -106,7 +111,7 @@ class DefaultRoomAttachmentResult : RoomAttachmentResult {
override val exception: AblyException
get() {
val errorInfo = errorField
?: ErrorInfo("unknown error in attach", ErrorCodes.RoomLifecycleError.errorCode, HttpStatusCodes.InternalServerError)
?: ErrorInfo("unknown error in attach", HttpStatusCodes.InternalServerError, ErrorCodes.RoomLifecycleError.errorCode)
throwable?.let {
return AblyException.fromErrorInfo(throwable, errorInfo)
}
Expand Down Expand Up @@ -235,8 +240,8 @@ class RoomLifecycleManager
AblyException.fromErrorInfo(
ErrorInfo(
"no failed feature in doRetry",
ErrorCodes.RoomLifecycleError.errorCode,
HttpStatusCodes.InternalServerError,
ErrorCodes.RoomLifecycleError.errorCode,
),
)
}
Expand Down Expand Up @@ -271,7 +276,7 @@ class RoomLifecycleManager
contributor.channel.once(ChannelState.failed) {
val exception = AblyException.fromErrorInfo(
it.reason
?: ErrorInfo("unknown error in _doRetry", ErrorCodes.RoomLifecycleError.errorCode, HttpStatusCodes.InternalServerError),
?: ErrorInfo("unknown error in _doRetry", HttpStatusCodes.InternalServerError, ErrorCodes.RoomLifecycleError.errorCode),
)
continuation.resumeWithException(exception)
}
Expand All @@ -284,21 +289,22 @@ class RoomLifecycleManager
* If a channel enters the suspended state, then we reject, but we will retry after a short delay as is the case
* in the core SDK.
* If a channel enters the failed state, we reject and then begin to wind down the other channels.
* Spec: CHA-RL1
*/
@SuppressWarnings("ThrowsCount")
internal suspend fun attach() {
val deferredAttach = atomicCoroutineScope.async(LifecycleOperationPrecedence.AttachOrDetach.priority) {
val deferredAttach = atomicCoroutineScope.async(LifecycleOperationPrecedence.AttachOrDetach.priority) { // CHA-RL1d
when (_status.current) {
RoomLifecycle.Attached -> return@async
RoomLifecycle.Releasing ->
RoomLifecycle.Attached -> return@async // CHA-RL1a
RoomLifecycle.Releasing -> // CHA-RL1b
throw AblyException.fromErrorInfo(
ErrorInfo(
"unable to attach room; room is releasing",
HttpStatusCodes.InternalServerError,
ErrorCodes.RoomIsReleasing.errorCode,
),
)
RoomLifecycle.Released ->
RoomLifecycle.Released -> // CHA-RL1c
throw AblyException.fromErrorInfo(
ErrorInfo(
"unable to attach room; room is released",
Expand All @@ -312,35 +318,37 @@ class RoomLifecycleManager
// At this point, we force the room status to be attaching
clearAllTransientDetachTimeouts()
_operationInProgress = true
_status.setStatus(RoomLifecycle.Attaching)
_status.setStatus(RoomLifecycle.Attaching) // CHA-RL1e

val attachResult = doAttach()

// If we're in a failed state, then we should wind down all the channels, eventually
// CHA-RL1h4 - If we're in a failed state, then we should wind down all the channels, eventually
if (attachResult.status === RoomLifecycle.Failed) {
// CHA-RL1h5 - detach all remaining channels
atomicCoroutineScope.async(LifecycleOperationPrecedence.Internal.priority) {
runDownChannelsOnFailedAttach()
}
throw attachResult.exception
throw attachResult.exception // CHA-RL1h1
}

// If we're in suspended, then this attach should fail, but we'll retry after a short delay async
// CHA-RL1h1, CHA-RL1h2 - If we're in suspended, then this attach should fail, but we'll retry after a short delay async
if (attachResult.status === RoomLifecycle.Suspended) {
if (attachResult.failedFeature == null) {
AblyException.fromErrorInfo(
ErrorInfo(
"no failed feature in attach",
ErrorCodes.RoomLifecycleError.errorCode,
HttpStatusCodes.InternalServerError,
ErrorCodes.RoomLifecycleError.errorCode,
),
)
}
attachResult.failedFeature?.let {
// CHA-RL1h3 - Enter recovery for failed room feature/contributor
atomicCoroutineScope.async(LifecycleOperationPrecedence.Internal.priority) {
doRetry(it)
}
}
throw attachResult.exception
throw attachResult.exception // CHA-RL1h1
}

// We attached, finally!
Expand All @@ -353,21 +361,21 @@ class RoomLifecycleManager
*
* Attaches each feature channel with rollback on channel attach failure.
* This method is re-usable and can be called as a part of internal room operations.
*
* Spec: CHA-RL1f, CHA-RL1g, CHA-RL1h
*/
private suspend fun doAttach(): RoomAttachmentResult {
val attachResult = DefaultRoomAttachmentResult()
for (feature in _contributors) {
for (feature in _contributors) { // CHA-RL1f - attach each feature sequentially
try {
feature.channel.attachCoroutine()
_firstAttachesCompleted[feature] = true
} catch (ex: Throwable) {
} catch (ex: Throwable) { // CHA-RL1h - handle channel attach failure
attachResult.throwable = ex
attachResult.failedFeatureField = feature
attachResult.errorField = ErrorInfo(
"failed to attach feature",
feature.contributor.attachmentErrorCode.errorCode,
"failed to attach ${feature.contributor.featureName} feature${feature.channel.errorMessage}",
HttpStatusCodes.InternalServerError,
feature.contributor.attachmentErrorCode.errorCode,
)

// The current feature should be in one of two states, it will be either suspended or failed
Expand All @@ -379,9 +387,9 @@ class RoomLifecycleManager
else -> {
attachResult.statusField = RoomLifecycle.Failed
attachResult.errorField = ErrorInfo(
"unexpected channel state in doAttach ${feature.channel.state}",
ErrorCodes.RoomLifecycleError.errorCode,
"unexpected channel state in doAttach ${feature.channel.state}${feature.channel.errorMessage}",
HttpStatusCodes.InternalServerError,
ErrorCodes.RoomLifecycleError.errorCode,
)
}
}
Expand All @@ -393,7 +401,7 @@ class RoomLifecycleManager
}
}

// We successfully attached all the channels - set our status to attached, start listening changes in channel status
// CHA-RL1g, We successfully attached all the channels - set our status to attached, start listening changes in channel status
this._status.setStatus(attachResult)
this._operationInProgress = false

Expand All @@ -407,14 +415,14 @@ class RoomLifecycleManager

/**
* If we've failed to attach, then we're in the failed state and all that is left to do is to detach all the channels.
*
* Spec: CHA-RL1h5, CHA-RL1h6
* @returns Returns only when all channels are detached. Doesn't throw exception.
*/
private suspend fun runDownChannelsOnFailedAttach() {
// At this point, we have control over the channel lifecycle, so we can hold onto it until things are resolved
// Keep trying to detach the channels until they're all detached.
var channelWindDown = kotlin.runCatching { doChannelWindDown() }
while (channelWindDown.isFailure) {
while (channelWindDown.isFailure) { // CHA-RL1h6 - repeat until all channels are detached
// Something went wrong during the wind down. After a short delay, to give others a turn, we should run down
// again until we reach a suitable conclusion.
delay(_retryDurationInMs)
Expand All @@ -425,9 +433,10 @@ class RoomLifecycleManager
/**
* Detach all features except the one exception provided.
* If the room is in a failed state, then all channels should either reach the failed state or be detached.
*
* Spec: CHA-RL1h5
* @param except The contributor to exclude from the detachment.
* @returns Success/Failure when all channels are detached or at least one of them fails.
*
*/
@SuppressWarnings("CognitiveComplexMethod", "ComplexCondition")
private suspend fun doChannelWindDown(except: ResolvedContributor? = null) = coroutineScope {
Expand Down Expand Up @@ -461,8 +470,8 @@ class RoomLifecycleManager
) {
val contributorError = ErrorInfo(
"failed to detach feature",
contributor.contributor.detachmentErrorCode.errorCode,
HttpStatusCodes.InternalServerError,
contributor.contributor.detachmentErrorCode.errorCode,
)
_status.setStatus(RoomLifecycle.Failed, contributorError)
throw AblyException.fromErrorInfo(throwable, contributorError)
Expand All @@ -474,4 +483,26 @@ class RoomLifecycleManager
}
}.awaitAll()
}

/**
* Detaches the room. If the room is already detached, this is a no-op.
* If one of the channels fails to detach, the room status will be set to failed.
* If the room is in the process of detaching, this will wait for the detachment to complete.
* @return when the room is detached.
*/
internal suspend fun detach() {
// TODO("Need to impl. room detach")
}

/**
* Releases the room. If the room is already released, this is a no-op.
* Any channel that detaches into the failed state is ok. But any channel that fails to detach
* will cause the room status to be set to failed.
*
* @returns Returns when the room is released. If a channel detaches into a non-terminated
* state (e.g. attached), release will throw exception.
*/
internal suspend fun release() {
// TODO("Need to impl. room release")
}
}
1 change: 1 addition & 0 deletions chat-android/src/main/java/com/ably/chat/RoomReactions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ internal class DefaultRoomReactions(
private val roomReactionsChannelName = "$roomId::\$chat::\$reactions"

override val channel: AblyRealtimeChannel = realtimeChannels.get(roomReactionsChannelName, ChatChannelOptions())
override val featureName = "reactions"

override val contributor: ContributesToRoomLifecycle = this

Expand Down
2 changes: 1 addition & 1 deletion chat-android/src/main/java/com/ably/chat/RoomStatus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ class RoomStatusEventEmitter : EventEmitter<RoomLifecycle, RoomStatus.Listener>(
}
}

class DefaultStatus(private val logger: LogHandler?) : InternalRoomStatus {
class DefaultStatus(private val logger: LogHandler? = null) : InternalRoomStatus {

private val _logger = logger

Expand Down
2 changes: 2 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Typing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ internal class DefaultTyping(

private val typingIndicatorsChannelName = "$roomId::\$chat::\$typingIndicators"

override val featureName = "typing"

override val channel = realtimeClient.channels.get(typingIndicatorsChannelName, ChatChannelOptions())

override val contributor: ContributesToRoomLifecycle = this
Expand Down
7 changes: 7 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ suspend fun Channel.publishCoroutine(message: PubSubMessage) = suspendCoroutine
)
}

val Channel.errorMessage: String
get() = if (reason == null) {
""
} else {
", ${reason.message}"
}

@Suppress("FunctionName")
fun ChatChannelOptions(init: (ChannelOptions.() -> Unit)? = null): ChannelOptions {
val options = ChannelOptions()
Expand Down
Loading