Commit c3037e3e authored by Lucio Maciel's avatar Lucio Maciel

retryDB

parent 644d6518
...@@ -3,9 +3,12 @@ package chat.rocket.android.chatrooms.infrastructure ...@@ -3,9 +3,12 @@ package chat.rocket.android.chatrooms.infrastructure
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import chat.rocket.android.db.ChatRoomDao import chat.rocket.android.db.ChatRoomDao
import chat.rocket.android.db.model.ChatRoom import chat.rocket.android.db.model.ChatRoom
import chat.rocket.android.util.retryDB
import javax.inject.Inject import javax.inject.Inject
class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao) {
// TODO - check how to use retryDB here - suspend
fun getChatRooms(order: Order): LiveData<List<ChatRoom>> { fun getChatRooms(order: Order): LiveData<List<ChatRoom>> {
return when(order) { return when(order) {
Order.ACTIVITY -> dao.getAll() Order.ACTIVITY -> dao.getAll()
...@@ -15,9 +18,10 @@ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){ ...@@ -15,9 +18,10 @@ class ChatRoomsRepository @Inject constructor(private val dao: ChatRoomDao){
} }
} }
fun search(query: String) = dao.searchSync(query) suspend fun search(query: String) =
retryDB("roomSearch($query)") { dao.searchSync(query) }
fun count() = dao.count() suspend fun count() = retryDB("roomsCount") { dao.count() }
enum class Order { enum class Order {
ACTIVITY, ACTIVITY,
......
...@@ -19,6 +19,7 @@ import chat.rocket.android.util.extensions.exhaustive ...@@ -19,6 +19,7 @@ import chat.rocket.android.util.extensions.exhaustive
import chat.rocket.android.util.extensions.removeTrailingSlash import chat.rocket.android.util.extensions.removeTrailingSlash
import chat.rocket.android.util.extensions.toEntity import chat.rocket.android.util.extensions.toEntity
import chat.rocket.android.util.extensions.userId import chat.rocket.android.util.extensions.userId
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.BaseRoom import chat.rocket.common.model.BaseRoom
import chat.rocket.common.model.RoomType import chat.rocket.common.model.RoomType
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
...@@ -96,7 +97,9 @@ class DatabaseManager(val context: Application, val serverUrl: String) { ...@@ -96,7 +97,9 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
} }
suspend fun getRoom(id: String) = withContext(dbManagerContext) { suspend fun getRoom(id: String) = withContext(dbManagerContext) {
chatRoomDao().get(id) retryDB("getRoom($id)") {
chatRoomDao().get(id)
}
} }
fun processUsersBatch(users: List<User>) { fun processUsersBatch(users: List<User>) {
...@@ -151,7 +154,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) { ...@@ -151,7 +154,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
fun updateSelfUser(myself: Myself) { fun updateSelfUser(myself: Myself) {
launch(dbManagerContext) { launch(dbManagerContext) {
val user = userDao().getUser(myself.id) val user = retryDB("getUser(${myself.id})") { userDao().getUser(myself.id) }
val entity = user?.copy( val entity = user?.copy(
name = myself.name ?: user.name, name = myself.name ?: user.name,
username = myself.username ?: user.username, username = myself.username ?: user.username,
...@@ -335,7 +338,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) { ...@@ -335,7 +338,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
} }
private suspend fun updateRoom(data: Room): ChatRoomEntity? { private suspend fun updateRoom(data: Room): ChatRoomEntity? {
return chatRoomDao().get(data.id)?.let { current -> return retryDB("getChatRoom(${data.id})") { chatRoomDao().get(data.id) }?.let { current ->
with(data) { with(data) {
val chatRoom = current.chatRoom val chatRoom = current.chatRoom
...@@ -373,7 +376,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) { ...@@ -373,7 +376,7 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
context.getString(R.string.msg_sent_attachment) context.getString(R.string.msg_sent_attachment)
private suspend fun updateSubscription(data: Subscription): ChatRoomEntity? { private suspend fun updateSubscription(data: Subscription): ChatRoomEntity? {
return chatRoomDao().get(data.roomId)?.let { current -> return retryDB("getRoom(${data.roomId}") { chatRoomDao().get(data.roomId) }?.let { current ->
with(data) { with(data) {
val userId = if (type is RoomType.DirectMessage) { val userId = if (type is RoomType.DirectMessage) {
...@@ -539,39 +542,42 @@ class DatabaseManager(val context: Application, val serverUrl: String) { ...@@ -539,39 +542,42 @@ class DatabaseManager(val context: Application, val serverUrl: String) {
} }
} }
private fun findUser(userId: String): String? = userDao().findUser(userId) private suspend fun findUser(userId: String): String? =
retryDB("findUser($userId)") { userDao().findUser(userId) }
private fun doOperation(operation: Operation) { private suspend fun doOperation(operation: Operation) {
when (operation) { retryDB(description = "doOperation($operation)") {
is Operation.ClearStatus -> userDao().clearStatus() when (operation) {
is Operation.UpdateRooms -> { is Operation.ClearStatus -> userDao().clearStatus()
Timber.d("Running ChatRooms transaction: remove: ${operation.toRemove} - insert: ${operation.toInsert} - update: ${operation.toUpdate}") is Operation.UpdateRooms -> {
Timber.d("Running ChatRooms transaction: remove: ${operation.toRemove} - insert: ${operation.toInsert} - update: ${operation.toUpdate}")
chatRoomDao().update(operation.toInsert, operation.toUpdate, operation.toRemove) chatRoomDao().update(operation.toInsert, operation.toUpdate, operation.toRemove)
} }
is Operation.InsertRooms -> { is Operation.InsertRooms -> {
chatRoomDao().insertOrReplace(operation.chatRooms) chatRoomDao().insertOrReplace(operation.chatRooms)
} }
is Operation.CleanInsertRooms -> { is Operation.CleanInsertRooms -> {
chatRoomDao().cleanInsert(operation.chatRooms) chatRoomDao().cleanInsert(operation.chatRooms)
} }
is Operation.InsertUsers -> { is Operation.InsertUsers -> {
val time = measureTimeMillis { userDao().upsert(operation.users) } val time = measureTimeMillis { userDao().upsert(operation.users) }
Timber.d("Upserted users batch(${operation.users.size}) in $time MS") Timber.d("Upserted users batch(${operation.users.size}) in $time MS")
} }
is Operation.InsertUser -> { is Operation.InsertUser -> {
userDao().insert(operation.user) userDao().insert(operation.user)
} }
is Operation.UpsertUser -> { is Operation.UpsertUser -> {
userDao().upsert(operation.user) userDao().upsert(operation.user)
} }
is Operation.InsertMessages -> { is Operation.InsertMessages -> {
messageDao().insert(operation.list) messageDao().insert(operation.list)
} }
is Operation.SaveLastSync -> { is Operation.SaveLastSync -> {
messageDao().saveLastSync(operation.sync) messageDao().saveLastSync(operation.sync)
} }
}.exhaustive }.exhaustive
}
} }
} }
......
...@@ -7,6 +7,7 @@ import chat.rocket.android.db.model.FullMessage ...@@ -7,6 +7,7 @@ import chat.rocket.android.db.model.FullMessage
import chat.rocket.android.db.model.ReactionEntity import chat.rocket.android.db.model.ReactionEntity
import chat.rocket.android.db.model.UrlEntity import chat.rocket.android.db.model.UrlEntity
import chat.rocket.android.db.model.UserEntity import chat.rocket.android.db.model.UserEntity
import chat.rocket.android.util.retryDB
import chat.rocket.common.model.SimpleRoom import chat.rocket.common.model.SimpleRoom
import chat.rocket.common.model.SimpleUser import chat.rocket.common.model.SimpleUser
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
...@@ -135,14 +136,18 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) { ...@@ -135,14 +136,18 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
with(attachment) { with(attachment) {
val fields = if (hasFields) { val fields = if (hasFields) {
withContext(CommonPool) { withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id) retryDB("getAttachmentFields(${attachment._id})") {
dbManager.messageDao().getAttachmentFields(attachment._id)
}
}.map { Field(it.title, it.value) } }.map { Field(it.title, it.value) }
} else { } else {
null null
} }
val actions = if (hasActions) { val actions = if (hasActions) {
withContext(CommonPool) { withContext(CommonPool) {
dbManager.messageDao().getAttachmentActions(attachment._id) retryDB("getAttachmentActions(${attachment._id})") {
dbManager.messageDao().getAttachmentActions(attachment._id)
}
}.mapNotNull { mapAction(it) } }.mapNotNull { mapAction(it) }
} else { } else {
null null
...@@ -183,29 +188,6 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) { ...@@ -183,29 +188,6 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
return list return list
} }
/*private suspend fun mapColorAttachmentWithFields(entity: AttachmentEntity): ColorAttachment {
val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(entity._id)
}.map { Field(it.title, it.value) }
return with(entity) {
ColorAttachment(
color = Color.Custom(color ?: DEFAULT_COLOR_STR),
text = text ?: "",
fallback = fallback,
fields = fields)
}
}
private suspend fun mapActionAttachment(attachment: AttachmentEntity): ActionsAttachment {
val actions = withContext(CommonPool) {
dbManager.messageDao().getAttachmentActions(attachment._id)
}.mapNotNull { mapAction(it) }
return with(attachment) {
// TODO - remove the default "vertical" value from here...
ActionsAttachment(title, actions, buttonAlignment ?: "vertical")
}
}*/
private fun mapAction(action: AttachmentActionEntity): Action? { private fun mapAction(action: AttachmentActionEntity): Action? {
return when (action.type) { return when (action.type) {
"button" -> ButtonAction(action.type, action.text, action.url, action.isWebView, "button" -> ButtonAction(action.type, action.text, action.url, action.isWebView,
...@@ -214,13 +196,4 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) { ...@@ -214,13 +196,4 @@ class DatabaseMessageMapper(private val dbManager: DatabaseManager) {
else -> null else -> null
} }
} }
/*private suspend fun mapAuthorAttachment(attachment: AttachmentEntity): AuthorAttachment {
val fields = withContext(CommonPool) {
dbManager.messageDao().getAttachmentFields(attachment._id)
}.map { Field(it.title, it.value) }
return with(attachment) {
AuthorAttachment(authorLink!!, authorIcon, authorName, fields)
}
}*/
} }
\ No newline at end of file
...@@ -4,6 +4,7 @@ import chat.rocket.android.db.DatabaseManager ...@@ -4,6 +4,7 @@ import chat.rocket.android.db.DatabaseManager
import chat.rocket.android.db.Operation import chat.rocket.android.db.Operation
import chat.rocket.android.db.model.MessagesSync import chat.rocket.android.db.model.MessagesSync
import chat.rocket.android.server.domain.MessagesRepository import chat.rocket.android.server.domain.MessagesRepository
import chat.rocket.android.util.retryDB
import chat.rocket.core.model.Message import chat.rocket.core.model.Message
import kotlinx.coroutines.experimental.CommonPool import kotlinx.coroutines.experimental.CommonPool
import kotlinx.coroutines.experimental.withContext import kotlinx.coroutines.experimental.withContext
...@@ -14,25 +15,31 @@ class DatabaseMessagesRepository( ...@@ -14,25 +15,31 @@ class DatabaseMessagesRepository(
) : MessagesRepository { ) : MessagesRepository {
override suspend fun getById(id: String): Message? = withContext(CommonPool) { override suspend fun getById(id: String): Message? = withContext(CommonPool) {
dbManager.messageDao().getMessageById(id)?.let { message -> mapper.map(message) } retryDB("getMessageById($id)") {
dbManager.messageDao().getMessageById(id)?.let { message -> mapper.map(message) }
}
} }
override suspend fun getByRoomId(roomId: String): List<Message> = withContext(CommonPool) { override suspend fun getByRoomId(roomId: String): List<Message> = withContext(CommonPool) {
// FIXME - investigate how to avoid this distinctBy here, since DAO is returning a lot of // FIXME - investigate how to avoid this distinctBy here, since DAO is returning a lot of
// duplicate rows (something related to our JOINS and relations on Room) // duplicate rows (something related to our JOINS and relations on Room)
dbManager.messageDao().getMessagesByRoomId(roomId) retryDB("getMessagesByRoomId($roomId)") {
.distinctBy { it.message.message.id } dbManager.messageDao().getMessagesByRoomId(roomId)
.let { messages -> .distinctBy { it.message.message.id }
mapper.map(messages) .let { messages ->
} mapper.map(messages)
}
}
} }
override suspend fun getRecentMessages(roomId: String, count: Long): List<Message> = withContext(CommonPool) { override suspend fun getRecentMessages(roomId: String, count: Long): List<Message> = withContext(CommonPool) {
dbManager.messageDao().getRecentMessagesByRoomId(roomId, count) retryDB("getRecentMessagesByRoomId($roomId, $count)") {
.distinctBy { it.message.message.id } dbManager.messageDao().getRecentMessagesByRoomId(roomId, count)
.let { messages -> .distinctBy { it.message.message.id }
mapper.map(messages) .let { messages ->
} mapper.map(messages)
}
}
} }
override suspend fun save(message: Message) { override suspend fun save(message: Message) {
...@@ -45,20 +52,24 @@ class DatabaseMessagesRepository( ...@@ -45,20 +52,24 @@ class DatabaseMessagesRepository(
override suspend fun removeById(id: String) { override suspend fun removeById(id: String) {
withContext(CommonPool) { withContext(CommonPool) {
dbManager.messageDao().delete(id) retryDB("delete($id)") { dbManager.messageDao().delete(id) }
} }
} }
override suspend fun removeByRoomId(roomId: String) { override suspend fun removeByRoomId(roomId: String) {
withContext(CommonPool) { withContext(CommonPool) {
dbManager.messageDao().deleteByRoomId(roomId) retryDB("deleteByRoomId($roomId)") {
dbManager.messageDao().deleteByRoomId(roomId)
}
} }
} }
override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) { override suspend fun getAllUnsent(): List<Message> = withContext(CommonPool) {
dbManager.messageDao().getUnsentMessages() retryDB("getUnsentMessages") {
.distinctBy { it.message.message.id } dbManager.messageDao().getUnsentMessages()
.let { mapper.map(it) } .distinctBy { it.message.message.id }
.let { mapper.map(it) }
}
} }
override suspend fun saveLastSyncDate(roomId: String, timeMillis: Long) { override suspend fun saveLastSyncDate(roomId: String, timeMillis: Long) {
...@@ -66,6 +77,8 @@ class DatabaseMessagesRepository( ...@@ -66,6 +77,8 @@ class DatabaseMessagesRepository(
} }
override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) { override suspend fun getLastSyncDate(roomId: String): Long? = withContext(CommonPool) {
dbManager.messageDao().getLastSync(roomId)?.let { it.timestamp } retryDB("getLastSync($roomId)") {
dbManager.messageDao().getLastSync(roomId)?.let { it.timestamp }
}
} }
} }
\ No newline at end of file
package chat.rocket.android.util package chat.rocket.android.util
import android.database.sqlite.SQLiteDatabaseLockedException
import chat.rocket.common.RocketChatNetworkErrorException import chat.rocket.common.RocketChatNetworkErrorException
import kotlinx.coroutines.experimental.TimeoutCancellationException import kotlinx.coroutines.experimental.TimeoutCancellationException
import kotlinx.coroutines.experimental.delay import kotlinx.coroutines.experimental.delay
...@@ -8,6 +9,7 @@ import timber.log.Timber ...@@ -8,6 +9,7 @@ import timber.log.Timber
import kotlin.coroutines.experimental.coroutineContext import kotlin.coroutines.experimental.coroutineContext
const val DEFAULT_RETRY = 3 const val DEFAULT_RETRY = 3
private const val DEFAULT_DB_RETRY = 5
suspend fun <T> retryIO( suspend fun <T> retryIO(
description: String = "<missing description>", description: String = "<missing description>",
...@@ -32,6 +34,33 @@ suspend fun <T> retryIO( ...@@ -32,6 +34,33 @@ suspend fun <T> retryIO(
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
} }
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
return block() // last attempt
}
suspend fun <T> retryDB(
description: String = "<missing description>",
times: Int = DEFAULT_DB_RETRY,
initialDelay: Long = 100, // 0.1 second
maxDelay: Long = 500, // 0.5 second
factor: Double = 1.2,
block: suspend () -> T): T
{
var currentDelay = initialDelay
repeat(times - 1) { currentTry ->
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
try {
return block()
} catch (e: SQLiteDatabaseLockedException) {
Timber.d(e, "failed call($currentTry): $description")
e.printStackTrace()
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
delay(currentDelay)
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
}
if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled") if (!coroutineContext.isActive) throw TimeoutCancellationException("job canceled")
return block() // last attempt return block() // last attempt
} }
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment