android, desktop: better handling of parallel updates of chats (#3204)

* android, desktop: better handling of parallel updates of chats

* one more case
This commit is contained in:
Stanislav Dmitrenko 2023-10-12 20:19:57 +08:00 committed by GitHub
parent b956988a83
commit 8ffe1c23c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 53 additions and 35 deletions

View File

@ -9,12 +9,14 @@ import chat.simplex.common.helpers.APPLICATION_ID
import chat.simplex.common.helpers.requiresIgnoringBattery import chat.simplex.common.helpers.requiresIgnoringBattery
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.appPrefs
import chat.simplex.common.model.ChatModel.updatingChatsMutex
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.onboarding.OnboardingStage import chat.simplex.common.views.onboarding.OnboardingStage
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.views.call.RcvCallInvitation import chat.simplex.common.views.call.RcvCallInvitation
import com.jakewharton.processphoenix.ProcessPhoenix import com.jakewharton.processphoenix.ProcessPhoenix
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
import java.io.* import java.io.*
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -52,21 +54,23 @@ class SimplexApp: Application(), LifecycleEventObserver {
Lifecycle.Event.ON_START -> { Lifecycle.Event.ON_START -> {
isAppOnForeground = true isAppOnForeground = true
if (chatModel.chatRunning.value == true) { if (chatModel.chatRunning.value == true) {
kotlin.runCatching { updatingChatsMutex.withLock {
val currentUserId = chatModel.currentUser.value?.userId kotlin.runCatching {
val chats = ArrayList(chatController.apiGetChats()) val currentUserId = chatModel.currentUser.value?.userId
/** Active user can be changed in background while [ChatController.apiGetChats] is executing */ val chats = ArrayList(chatController.apiGetChats())
if (chatModel.currentUser.value?.userId == currentUserId) { /** Active user can be changed in background while [ChatController.apiGetChats] is executing */
val currentChatId = chatModel.chatId.value if (chatModel.currentUser.value?.userId == currentUserId) {
val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null val currentChatId = chatModel.chatId.value
if (oldStats != null) { val oldStats = if (currentChatId != null) chatModel.getChat(currentChatId)?.chatStats else null
val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId } if (oldStats != null) {
/** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ val indexOfCurrentChat = chats.indexOfFirst { it.id == currentChatId }
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */
if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats)
}
chatModel.updateChats(chats)
} }
chatModel.updateChats(chats) }.onFailure { Log.e(TAG, it.stackTraceToString()) }
} }
}.onFailure { Log.e(TAG, it.stackTraceToString()) }
} }
} }
Lifecycle.Event.ON_RESUME -> { Lifecycle.Event.ON_RESUME -> {

View File

@ -6,7 +6,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.* import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.* import chat.simplex.common.views.call.*
@ -16,6 +15,8 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.* import kotlinx.datetime.*
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.serialization.* import kotlinx.serialization.*
@ -102,6 +103,8 @@ object ChatModel {
val filesToDelete = mutableSetOf<File>() val filesToDelete = mutableSetOf<File>()
val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) } val simplexLinkMode by lazy { mutableStateOf(ChatController.appPrefs.simplexLinkMode.get()) }
var updatingChatsMutex: Mutex = Mutex()
fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) {
currentUser.value currentUser.value
} else { } else {
@ -198,7 +201,7 @@ object ChatModel {
} }
} }
suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) { suspend fun addChatItem(cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock {
// update previews // update previews
val i = getChatIndex(cInfo.id) val i = getChatIndex(cInfo.id)
val chat: Chat val chat: Chat
@ -221,10 +224,11 @@ object ChatModel {
} else { } else {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem))) addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
} }
// add to current chat Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
if (chatId.value == cInfo.id) { withContext(Dispatchers.Main) {
Log.d(TAG, "TODOCHAT: addChatItem: adding to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") // add to current chat
withContext(Dispatchers.Main) { if (chatId.value == cInfo.id) {
Log.d(TAG, "TODOCHAT: addChatItem: chatIds are equal, size ${chatItems.size}")
// Prevent situation when chat item already in the list received from backend // Prevent situation when chat item already in the list received from backend
if (chatItems.none { it.id == cItem.id }) { if (chatItems.none { it.id == cItem.id }) {
if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { if (chatItems.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
@ -238,7 +242,7 @@ object ChatModel {
} }
} }
suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean { suspend fun upsertChatItem(cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock {
// update previews // update previews
val i = getChatIndex(cInfo.id) val i = getChatIndex(cInfo.id)
val chat: Chat val chat: Chat
@ -258,10 +262,10 @@ object ChatModel {
addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem))) addChat(Chat(chatInfo = cInfo, chatItems = arrayListOf(cItem)))
res = true res = true
} }
// update current chat Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
return if (chatId.value == cInfo.id) { return withContext(Dispatchers.Main) {
Log.d(TAG, "TODOCHAT: upsertChatItem: upserting to chat ${chatId.value} from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") // update current chat
withContext(Dispatchers.Main) { if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) { if (itemIndex >= 0) {
chatItems[itemIndex] = cItem chatItems[itemIndex] = cItem
@ -272,15 +276,15 @@ object ChatModel {
Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}")
true true
} }
} else {
res
} }
} else {
res
} }
} }
suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) { suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) {
if (chatId.value == cInfo.id) { withContext(Dispatchers.Main) {
withContext(Dispatchers.Main) { if (chatId.value == cInfo.id) {
val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } val itemIndex = chatItems.indexOfFirst { it.id == cItem.id }
if (itemIndex >= 0) { if (itemIndex >= 0) {
chatItems[itemIndex] = cItem chatItems[itemIndex] = cItem

View File

@ -4,6 +4,7 @@ import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import chat.simplex.common.model.ChatModel.updatingChatsMutex
import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.painterResource
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
@ -16,6 +17,7 @@ import com.charleskorn.kaml.YamlConfiguration
import chat.simplex.res.MR import chat.simplex.res.MR
import com.russhwolf.settings.Settings import com.russhwolf.settings.Settings
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.* import kotlinx.serialization.*
@ -349,8 +351,10 @@ object ChatController {
startReceiver() startReceiver()
Log.d(TAG, "startChat: started") Log.d(TAG, "startChat: started")
} else { } else {
val chats = apiGetChats() updatingChatsMutex.withLock {
chatModel.updateChats(chats) val chats = apiGetChats()
chatModel.updateChats(chats)
}
Log.d(TAG, "startChat: running") Log.d(TAG, "startChat: running")
} }
} catch (e: Error) { } catch (e: Error) {
@ -384,8 +388,10 @@ object ChatController {
suspend fun getUserChatData() { suspend fun getUserChatData() {
chatModel.userAddress.value = apiGetUserAddress() chatModel.userAddress.value = apiGetUserAddress()
chatModel.chatItemTTL.value = getChatItemTTL() chatModel.chatItemTTL.value = getChatItemTTL()
val chats = apiGetChats() updatingChatsMutex.withLock {
chatModel.updateChats(chats) val chats = apiGetChats()
chatModel.updateChats(chats)
}
} }
private fun startReceiver() { private fun startReceiver() {

View File

@ -20,11 +20,13 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.updatingChatsMutex
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.* import chat.simplex.common.views.usersettings.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.sync.withLock
import kotlinx.datetime.* import kotlinx.datetime.*
import java.io.* import java.io.*
import java.net.URI import java.net.URI
@ -620,8 +622,10 @@ private fun afterSetCiTTL(
appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath)
withApi { withApi {
try { try {
val chats = m.controller.apiGetChats() updatingChatsMutex.withLock {
m.updateChats(chats) val chats = m.controller.apiGetChats()
m.updateChats(chats)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "apiGetChats error: ${e.message}") Log.e(TAG, "apiGetChats error: ${e.message}")
} }