Add mutex to "updateExtensionDatabase" (#829)

If called in quick succession it is possible that duplicated extensions get inserted to the database, because it has not yet been updated by the first call
This commit is contained in:
schroda
2024-01-21 01:42:01 +01:00
committed by GitHub
parent 57d5bc6480
commit d8876cf96a

View File

@@ -8,6 +8,8 @@ package suwayomi.tachidesk.manga.impl.extension
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging import mu.KotlinLogging
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
@@ -86,117 +88,121 @@ object ExtensionsList {
} }
} }
private fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) { private val updateExtensionDatabaseMutex = Mutex()
transaction {
val uniqueExtensions =
foundExtensions.groupBy { it.pkgName }.mapValues {
(_, extension) ->
extension.maxBy { it.versionCode }
}.values
val installedExtensions =
ExtensionTable.selectAll().toList()
.associateBy { it[ExtensionTable.pkgName] }
val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
val extensionsToInsert = mutableListOf<OnlineExtension>()
val extensionsToDelete =
installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
}
uniqueExtensions.forEach {
val extension = installedExtensions[it.pkgName]
if (extension != null) {
extensionsToUpdate.add(it to extension)
} else {
extensionsToInsert.add(it)
}
}
if (extensionsToUpdate.isNotEmpty()) {
val extensionsInstalled =
extensionsToUpdate
.groupBy { it.second[ExtensionTable.isInstalled] }
val installedExtensionsToUpdate = extensionsInstalled[true].orEmpty()
if (installedExtensionsToUpdate.isNotEmpty()) {
BatchUpdateStatement(ExtensionTable).apply {
installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
// Always update icon url and repo
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
this[ExtensionTable.repo] = foundExtension.repo
// add these because batch updates need matching columns private suspend fun updateExtensionDatabase(foundExtensions: List<OnlineExtension>) {
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate] updateExtensionDatabaseMutex.withLock {
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete] transaction {
val uniqueExtensions =
// a previously removed extension is now available again foundExtensions.groupBy { it.pkgName }.mapValues {
if (extensionRecord[ExtensionTable.isObsolete] && (_, extension) ->
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode] extension.maxBy { it.versionCode }
) { }.values
this[ExtensionTable.isObsolete] = false val installedExtensions =
} ExtensionTable.selectAll().toList()
.associateBy { it[ExtensionTable.pkgName] }
when { val extensionsToUpdate = mutableListOf<Pair<OnlineExtension, ResultRow>>()
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> { val extensionsToInsert = mutableListOf<OnlineExtension>()
// there is an update val extensionsToDelete =
this[ExtensionTable.hasUpdate] = true installedExtensions.filter { it.value[ExtensionTable.repo] != null }.mapNotNull { (pkgName, extension) ->
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension) extension.takeUnless { uniqueExtensions.any { it.pkgName == pkgName } }
} }
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> { uniqueExtensions.forEach {
// somehow the user installed an invalid version val extension = installedExtensions[it.pkgName]
this[ExtensionTable.isObsolete] = true if (extension != null) {
} extensionsToUpdate.add(it to extension)
} } else {
} extensionsToInsert.add(it)
execute(this@transaction)
} }
} }
val extensionsToFullyUpdate = extensionsInstalled[false].orEmpty() if (extensionsToUpdate.isNotEmpty()) {
if (extensionsToFullyUpdate.isNotEmpty()) { val extensionsInstalled =
BatchUpdateStatement(ExtensionTable).apply { extensionsToUpdate
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) -> .groupBy { it.second[ExtensionTable.isInstalled] }
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable)) val installedExtensionsToUpdate = extensionsInstalled[true].orEmpty()
// extension is not installed, so we can overwrite the data without a care if (installedExtensionsToUpdate.isNotEmpty()) {
this[ExtensionTable.repo] = foundExtension.repo BatchUpdateStatement(ExtensionTable).apply {
this[ExtensionTable.name] = foundExtension.name installedExtensionsToUpdate.forEach { (foundExtension, extensionRecord) ->
this[ExtensionTable.versionName] = foundExtension.versionName addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
this[ExtensionTable.versionCode] = foundExtension.versionCode // Always update icon url and repo
this[ExtensionTable.lang] = foundExtension.lang this[ExtensionTable.iconUrl] = foundExtension.iconUrl
this[ExtensionTable.isNsfw] = foundExtension.isNsfw this[ExtensionTable.repo] = foundExtension.repo
this[ExtensionTable.apkName] = foundExtension.apkName
this[ExtensionTable.iconUrl] = foundExtension.iconUrl // add these because batch updates need matching columns
this[ExtensionTable.hasUpdate] = extensionRecord[ExtensionTable.hasUpdate]
this[ExtensionTable.isObsolete] = extensionRecord[ExtensionTable.isObsolete]
// a previously removed extension is now available again
if (extensionRecord[ExtensionTable.isObsolete] &&
foundExtension.versionCode >= extensionRecord[ExtensionTable.versionCode]
) {
this[ExtensionTable.isObsolete] = false
}
when {
foundExtension.versionCode > extensionRecord[ExtensionTable.versionCode] -> {
// there is an update
this[ExtensionTable.hasUpdate] = true
updateMap.putIfAbsent(foundExtension.pkgName, foundExtension)
}
foundExtension.versionCode < extensionRecord[ExtensionTable.versionCode] -> {
// somehow the user installed an invalid version
this[ExtensionTable.isObsolete] = true
}
}
}
execute(this@transaction)
}
}
val extensionsToFullyUpdate = extensionsInstalled[false].orEmpty()
if (extensionsToFullyUpdate.isNotEmpty()) {
BatchUpdateStatement(ExtensionTable).apply {
extensionsToFullyUpdate.forEach { (foundExtension, extensionRecord) ->
addBatch(EntityID(extensionRecord[ExtensionTable.id].value, ExtensionTable))
// extension is not installed, so we can overwrite the data without a care
this[ExtensionTable.repo] = foundExtension.repo
this[ExtensionTable.name] = foundExtension.name
this[ExtensionTable.versionName] = foundExtension.versionName
this[ExtensionTable.versionCode] = foundExtension.versionCode
this[ExtensionTable.lang] = foundExtension.lang
this[ExtensionTable.isNsfw] = foundExtension.isNsfw
this[ExtensionTable.apkName] = foundExtension.apkName
this[ExtensionTable.iconUrl] = foundExtension.iconUrl
}
execute(this@transaction)
} }
execute(this@transaction)
} }
} }
} if (extensionsToInsert.isNotEmpty()) {
if (extensionsToInsert.isNotEmpty()) { ExtensionTable.batchInsert(extensionsToInsert) { foundExtension ->
ExtensionTable.batchInsert(extensionsToInsert) { foundExtension -> this[ExtensionTable.repo] = foundExtension.repo
this[ExtensionTable.repo] = foundExtension.repo this[ExtensionTable.name] = foundExtension.name
this[ExtensionTable.name] = foundExtension.name this[ExtensionTable.pkgName] = foundExtension.pkgName
this[ExtensionTable.pkgName] = foundExtension.pkgName this[ExtensionTable.versionName] = foundExtension.versionName
this[ExtensionTable.versionName] = foundExtension.versionName this[ExtensionTable.versionCode] = foundExtension.versionCode
this[ExtensionTable.versionCode] = foundExtension.versionCode this[ExtensionTable.lang] = foundExtension.lang
this[ExtensionTable.lang] = foundExtension.lang this[ExtensionTable.isNsfw] = foundExtension.isNsfw
this[ExtensionTable.isNsfw] = foundExtension.isNsfw this[ExtensionTable.apkName] = foundExtension.apkName
this[ExtensionTable.apkName] = foundExtension.apkName this[ExtensionTable.iconUrl] = foundExtension.iconUrl
this[ExtensionTable.iconUrl] = foundExtension.iconUrl }
} }
}
// deal with obsolete extensions // deal with obsolete extensions
val extensionsToRemove = val extensionsToRemove =
extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] } extensionsToDelete.groupBy { it[ExtensionTable.isInstalled] }
.mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } } .mapValues { (_, extensions) -> extensions.map { it[ExtensionTable.pkgName] } }
// not in the repo, so these extensions are obsolete // not in the repo, so these extensions are obsolete
val obsoleteExtensions = extensionsToRemove[true].orEmpty() val obsoleteExtensions = extensionsToRemove[true].orEmpty()
if (obsoleteExtensions.isNotEmpty()) { if (obsoleteExtensions.isNotEmpty()) {
ExtensionTable.update({ ExtensionTable.pkgName inList obsoleteExtensions }) { ExtensionTable.update({ ExtensionTable.pkgName inList obsoleteExtensions }) {
it[isObsolete] = true it[isObsolete] = true
}
}
// is not installed, so we can remove the record without a care
val removeExtensions = extensionsToRemove[false].orEmpty()
if (removeExtensions.isNotEmpty()) {
ExtensionTable.deleteWhere { ExtensionTable.pkgName inList removeExtensions }
} }
}
// is not installed, so we can remove the record without a care
val removeExtensions = extensionsToRemove[false].orEmpty()
if (removeExtensions.isNotEmpty()) {
ExtensionTable.deleteWhere { ExtensionTable.pkgName inList removeExtensions }
} }
} }
} }