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

feat: handle adding participants while some of their backends are down (WPB-462) #1900

Merged
merged 21 commits into from
Jul 21, 2023
Merged
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
958e5c9
feat: add mapping of hasUnreachableDomainsError and prepare handling …
yamilmedina Jul 17, 2023
fab5cc3
feat: add mapping of hasUnreachableDomainsError and prepare handling …
yamilmedina Jul 17, 2023
08fef21
feat: add retry and mapper of failed domains
yamilmedina Jul 18, 2023
f72af3b
feat: add retry and mapper of failed domains
yamilmedina Jul 18, 2023
5e09d09
Merge branch 'develop' into feat/fed-addingparticipants
yamilmedina Jul 18, 2023
3f9fe89
feat: add retry mechanism to attempt with valid users
yamilmedina Jul 18, 2023
0110644
feat: add retry mechanism to attempt with valid users
yamilmedina Jul 18, 2023
05962da
feat: add retry mechanism to attempt with valid users
yamilmedina Jul 18, 2023
dccbf45
feat: add retry mechanism to attempt with valid users
yamilmedina Jul 18, 2023
bc5ec5a
feat: add retry mechanism to attempt with valid users
yamilmedina Jul 18, 2023
e525cde
feat: cleanup
yamilmedina Jul 19, 2023
33784c4
feat: add system message creation of failed to add
yamilmedina Jul 19, 2023
7fe0fbf
Merge branch 'develop' into feat/fed-addingparticipants
yamilmedina Jul 19, 2023
f29ae83
feat: test cov
yamilmedina Jul 19, 2023
18b81f7
feat: test cov
yamilmedina Jul 19, 2023
f8df876
Merge branch 'develop' into feat/fed-addingparticipants
yamilmedina Jul 20, 2023
85d9a4b
feat: test coverage
yamilmedina Jul 20, 2023
825c444
Merge branch 'develop' into feat/fed-addingparticipants
yamilmedina Jul 20, 2023
a0e9f7e
feat: test coverage, code cleanup
yamilmedina Jul 20, 2023
aa0fd7d
feat: test coverage, code cleanup
yamilmedina Jul 21, 2023
c01d690
Merge branch 'develop' into feat/fed-addingparticipants
yamilmedina Jul 21, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ sealed interface CoreFailure {
&& this.kaliumException is KaliumException.InvalidRequestError
&& this.kaliumException.errorResponse.code == HttpStatusCode.NotFound.value

val hasUnreachableDomainsError: Boolean
get() = this is NetworkFailure.FederatedBackendFailure
&& this.label == "federation-unreachable-domains-error" && this.domains.isNotEmpty()

/**
* The attempted operation requires that this client is registered.
*/
Expand Down Expand Up @@ -106,7 +110,7 @@ sealed class NetworkFailure : CoreFailure {
/**
* Failure due to a federated backend context
*/
object FederatedBackendFailure : NetworkFailure()
class FederatedBackendFailure(val label: String, val domains: List<String> = emptyList()) : NetworkFailure()
}

interface MLSFailure : CoreFailure {
Expand Down Expand Up @@ -151,7 +155,8 @@ internal inline fun <T : Any> wrapApiRequest(networkCall: () -> NetworkResponse<
val exception = result.kException
when {
exception is KaliumException.FederationError -> {
Either.Left(NetworkFailure.FederatedBackendFailure)
val cause = exception.errorResponse.cause
Either.Left(NetworkFailure.FederatedBackendFailure(exception.errorResponse.label, cause?.domains.orEmpty()))
}

// todo SocketException is platform specific so need to wrap it in our own exceptions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.data.conversation

import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.data.user.UserId

internal interface AddingMembersFailureMapper {
/**
* Will map the [initialUsersIds] and split accordingly excluding users with domain failures.
* @param initialUsersIds the list of users that were initially requested to be added to the conversation
* @param federatedBackendFailure the [NetworkFailure.FederatedBackendFailure] that contains the domains that failed
* @param previousUserIdsExcluded the previous attempt of list of users that cannot be added to the conversation
*/
fun mapToUsersRequestState(
initialUsersIds: List<UserId>,
federatedBackendFailure: NetworkFailure.FederatedBackendFailure,
previousUserIdsExcluded: Set<UserId> = emptySet(),
): AddingMembersRequestState
}

internal class AddingMembersFailureMapperImpl : AddingMembersFailureMapper {
override fun mapToUsersRequestState(
initialUsersIds: List<UserId>,
federatedBackendFailure: NetworkFailure.FederatedBackendFailure,
previousUserIdsExcluded: Set<UserId>
): AddingMembersRequestState {
val domainsToExclude = federatedBackendFailure.domains
// splitting the initialUsersIds into users with failures[true] and users without failures[false]
val groupedUsersWithFailure = initialUsersIds.groupBy {
domainsToExclude.contains(it.domain)
}
return AddingMembersRequestState(
usersThatCanBeAdded = groupedUsersWithFailure[false]?.toSet().orEmpty(),
usersThatCannotBeAdded = groupedUsersWithFailure[true]?.toSet().orEmpty() + previousUserIdsExcluded
)
}
}

data class AddingMembersRequestState(
val usersThatCanBeAdded: Set<UserId>,
val usersThatCannotBeAdded: Set<UserId>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ internal class ConversationGroupRepositoryImpl(
private val conversationMapper: ConversationMapper = MapperProvider.conversationMapper(),
private val eventMapper: EventMapper = MapperProvider.eventMapper(),
private val protocolInfoMapper: ProtocolInfoMapper = MapperProvider.protocolInfoMapper(),
private val addingMembersFailureMapper: AddingMembersFailureMapper = MapperProvider.addingMembersFailureMapper(),
) : ConversationGroupRepository {

override suspend fun createGroupConversation(
Expand Down Expand Up @@ -140,7 +141,7 @@ internal class ConversationGroupRepositoryImpl(
.flatMap { protocol ->
when (protocol) {
is ConversationEntity.ProtocolInfo.Proteus ->
addMembersToCloudAndStorage(userIdList, conversationId)
tryAddMembersToCloudAndStorage(userIdList, conversationId)

is ConversationEntity.ProtocolInfo.MLS -> {
mlsConversationRepository.addMemberToMLSGroup(GroupID(protocol.groupId), userIdList)
Expand Down Expand Up @@ -174,20 +175,51 @@ internal class ConversationGroupRepositoryImpl(
}
}

private suspend fun addMembersToCloudAndStorage(userIdList: List<UserId>, conversationId: ConversationId): Either<CoreFailure, Unit> =
wrapApiRequest {
private suspend fun tryAddMembersToCloudAndStorage(
userIdList: List<UserId>,
conversationId: ConversationId,
previousUserIdsExcluded: Set<UserId> = emptySet(),
): Either<CoreFailure, Unit> {
val apiResult = wrapApiRequest {
val users = userIdList.map { it.toApi() }
val addParticipantRequest = AddConversationMembersRequest(users, ConversationDataSource.DEFAULT_MEMBER_ROLE)
conversationApi.addMember(
addParticipantRequest, conversationId.toApi()
)
}.onSuccess { response ->
if (response is ConversationMemberAddedResponse.Changed) {
memberJoinEventHandler.handle(eventMapper.conversationMemberJoin(LocalId.generate(), response.event, true))
}

return when (apiResult) {
is Either.Left -> {
if (apiResult.value.hasUnreachableDomainsError) {
val usersReqState = addingMembersFailureMapper.mapToUsersRequestState(
userIdList,
apiResult.value as NetworkFailure.FederatedBackendFailure,
previousUserIdsExcluded
)
// retry adding, only with filtered available members to the conversation
tryAddMembersToCloudAndStorage(
usersReqState.usersThatCanBeAdded.toList(), conversationId, usersReqState.usersThatCannotBeAdded
)
} else {
Either.Left(apiResult.value)
}
}

is Either.Right -> {
if (apiResult.value is ConversationMemberAddedResponse.Changed) {
memberJoinEventHandler.handle(
eventMapper.conversationMemberJoin(LocalId.generate(), apiResult.value.event, true)
)
}
if (previousUserIdsExcluded.isNotEmpty()) {
newGroupConversationSystemMessagesCreator.value.conversationFailedToAddMembers(
conversationId, previousUserIdsExcluded
)
}
Either.Right(Unit)
}
}.map {
Either.Right(Unit)
}
}

override suspend fun deleteMember(
userId: UserId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ internal interface NewGroupConversationSystemMessagesCreator {
conversationId: ConversationIDEntity,
conversationResponse: ConversationResponse
): Either<CoreFailure, Unit>

suspend fun conversationFailedToAddMembers(
conversationId: ConversationId,
userIdList: Set<UserId>
): Either<CoreFailure, Unit>
}

internal class NewGroupConversationSystemMessagesCreatorImpl(
Expand Down Expand Up @@ -156,6 +161,23 @@ internal class NewGroupConversationSystemMessagesCreatorImpl(
}
}

override suspend fun conversationFailedToAddMembers(
conversationId: ConversationId,
userIdList: Set<UserId>
): Either<CoreFailure, Unit> {
val messageFailedToAddMembers = Message.System(
uuid4().toString(),
MessageContent.MemberChange.FailedToAdd(userIdList.toList()),
conversationId,
DateTimeUtil.currentIsoDateTimeString(),
selfUserId,
Message.Status.SENT,
Message.Visibility.VISIBLE,
expirationData = null
)
return persistMessage(messageFailedToAddMembers)
}

private suspend fun createFailedToAddSystemMessage(conversationResponse: ConversationResponse) {
if (conversationResponse.failedToAdd.isNotEmpty()) {
val messageStartedWithFailedMembers = Message.System(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import com.wire.kalium.logic.data.connection.ConnectionMapper
import com.wire.kalium.logic.data.connection.ConnectionMapperImpl
import com.wire.kalium.logic.data.connection.ConnectionStatusMapper
import com.wire.kalium.logic.data.connection.ConnectionStatusMapperImpl
import com.wire.kalium.logic.data.conversation.AddingMembersFailureMapper
import com.wire.kalium.logic.data.conversation.AddingMembersFailureMapperImpl
import com.wire.kalium.logic.data.conversation.ConversationMapper
import com.wire.kalium.logic.data.conversation.ConversationMapperImpl
import com.wire.kalium.logic.data.conversation.ConversationRoleMapper
Expand Down Expand Up @@ -80,10 +82,10 @@ import com.wire.kalium.logic.data.prekey.PreKeyMapper
import com.wire.kalium.logic.data.prekey.PreKeyMapperImpl
import com.wire.kalium.logic.data.publicuser.PublicUserMapper
import com.wire.kalium.logic.data.publicuser.PublicUserMapperImpl
import com.wire.kalium.logic.data.service.ServiceMapper
import com.wire.kalium.logic.data.session.SessionMapper
import com.wire.kalium.logic.data.session.SessionMapperImpl
import com.wire.kalium.logic.data.session.SessionRepository
import com.wire.kalium.logic.data.service.ServiceMapper
import com.wire.kalium.logic.data.team.TeamMapper
import com.wire.kalium.logic.data.team.TeamMapperImpl
import com.wire.kalium.logic.data.user.AvailabilityStatusMapper
Expand Down Expand Up @@ -170,7 +172,7 @@ internal object MapperProvider {
fun protocolInfoMapper(): ProtocolInfoMapper = ProtocolInfoMapperImpl()
fun receiptModeMapper(): ReceiptModeMapper = ReceiptModeMapperImpl()
fun sendMessagePartialFailureMapper(): SendMessagePartialFailureMapper = SendMessagePartialFailureMapperImpl()

fun serviceMapper(): ServiceMapper = ServiceMapper()
fun addingMembersFailureMapper(): AddingMembersFailureMapper = AddingMembersFailureMapperImpl()

}
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ internal class LoginUseCaseImpl internal constructor(
when (it) {
is NetworkFailure.ProxyError -> AuthenticationResult.Failure.SocketError
is NetworkFailure.ServerMiscommunication -> handleServerMiscommunication(it, isEmail, cleanUserIdentifier)
is NetworkFailure.NoNetworkConnection, NetworkFailure.FederatedBackendFailure ->
is NetworkFailure.NoNetworkConnection, is NetworkFailure.FederatedBackendFailure ->
AuthenticationResult.Failure.Generic(it)
}
}, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.data.conversation

import com.wire.kalium.logic.NetworkFailure
import com.wire.kalium.logic.framework.TestUser
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class AddingMembersFailureMapperTest {

private val addingMembersFailureMapper = AddingMembersFailureMapperImpl()

@Test
fun givenUsersIdsAndDomainsError_whenMapping_thenShouldSplitIntoValidAndNotValidUserIds() {
// When
val mappedRequestState = addingMembersFailureMapper.mapToUsersRequestState(
initialUsersIds = initialUserIdList,
federatedBackendFailure = NetworkFailure.FederatedBackendFailure("federated-label", unreachableDomains)
)

// Then
assertEquals(TestUser.OTHER_USER_ID, mappedRequestState.usersThatCanBeAdded.first())
assertEquals(TestUser.OTHER_FEDERATED_USER_ID, mappedRequestState.usersThatCannotBeAdded.first())
}

@Test
fun givenUsersIdsADomainsErrorAndPreviousFailedIds_whenMapping_thenShouldSplitConsideringPreviousFailedIds() {
// When
val mappedRequestState = addingMembersFailureMapper.mapToUsersRequestState(
initialUsersIds = initialUserIdList,
federatedBackendFailure = NetworkFailure.FederatedBackendFailure("federated-label", unreachableDomains),
previousUserIdsExcluded = previousFailedIds
)

// Then
assertEquals(TestUser.OTHER_USER_ID, mappedRequestState.usersThatCanBeAdded.first())
assertEquals(2, mappedRequestState.usersThatCannotBeAdded.size)
assertTrue {
mappedRequestState.usersThatCannotBeAdded.containsAll(
listOf(
TestUser.OTHER_FEDERATED_USER_ID,
TestUser.OTHER_FEDERATED_USER_ID_2
)
)
}
}

private companion object {
val initialUserIdList = listOf(TestUser.OTHER_USER_ID, TestUser.OTHER_FEDERATED_USER_ID)
val previousFailedIds = setOf(TestUser.OTHER_FEDERATED_USER_ID_2)
val unreachableDomains = listOf("otherDomain")
}
}
Loading
Loading