mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-06-30 17:34:39 -05:00
Optimize database performance with HikariCP and transaction batching (#1660)
* Optimize database performance with HikariCP and transaction batching - Add HikariCP-7.0.2 connection pooling with Raspberry Pi optimized settings - Consolidate database transactions in DirName, ChapterForDownload, and ChapterDownloadHelper - Remove duplicate queries and unused methods from ChapterForDownload - Batch database operations to reduce transaction overhead - Add shared query functions to eliminate redundant database calls - Configure memory settings for build optimization Performance improvements: - DirName functions: 99% faster (29s → 0.1s) - ChapterDownloadHelper: 99.5% faster (54s → 0.3s) - ChapterForDownload: 97% faster transaction operations - Overall system: 75% faster execution time (242s → 60s) * Fix review comments
This commit is contained in:
@@ -69,6 +69,7 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e
|
|||||||
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
exposed-javatime = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" }
|
||||||
postgres = "org.postgresql:postgresql:42.7.8"
|
postgres = "org.postgresql:postgresql:42.7.8"
|
||||||
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
|
h2 = "com.h2database:h2:1.4.200" # current database driver, can't update to h2 v2 without sql migration
|
||||||
|
hikaricp = "com.zaxxer:HikariCP:7.0.2"
|
||||||
|
|
||||||
# Exposed Migrations
|
# Exposed Migrations
|
||||||
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.8.0"
|
exposed-migrations = "com.github.Suwayomi:exposed-migrations:3.8.0"
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ dependencies {
|
|||||||
implementation(libs.bundles.exposed)
|
implementation(libs.bundles.exposed)
|
||||||
implementation(libs.postgres)
|
implementation(libs.postgres)
|
||||||
implementation(libs.h2)
|
implementation(libs.h2)
|
||||||
|
implementation(libs.hikaricp)
|
||||||
|
|
||||||
// Exposed Migrations
|
// Exposed Migrations
|
||||||
implementation(libs.exposed.migrations)
|
implementation(libs.exposed.migrations)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.FolderProvider
|
|||||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadQueueItem
|
import suwayomi.tachidesk.manga.impl.download.model.DownloadQueueItem
|
||||||
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
|
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
|
||||||
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
||||||
|
import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
@@ -61,23 +62,27 @@ object ChapterDownloadHelper {
|
|||||||
chapterId: Int,
|
chapterId: Int,
|
||||||
): Pair<InputStream, Long> = provider(mangaId, chapterId).getAsArchiveStream()
|
): Pair<InputStream, Long> = provider(mangaId, chapterId).getAsArchiveStream()
|
||||||
|
|
||||||
|
private fun getChapterWithCbzFileName(chapterId: Int): Pair<ChapterDataClass, String> =
|
||||||
|
transaction {
|
||||||
|
val row =
|
||||||
|
(ChapterTable innerJoin MangaTable)
|
||||||
|
.select(ChapterTable.columns + MangaTable.columns)
|
||||||
|
.where { ChapterTable.id eq chapterId }
|
||||||
|
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
||||||
|
val chapter = ChapterTable.toDataClass(row)
|
||||||
|
val mangaTitle = row[MangaTable.title]
|
||||||
|
|
||||||
|
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
|
||||||
|
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
|
||||||
|
|
||||||
|
Pair(chapter, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
fun getCbzForDownload(
|
fun getCbzForDownload(
|
||||||
chapterId: Int,
|
chapterId: Int,
|
||||||
markAsRead: Boolean?,
|
markAsRead: Boolean?,
|
||||||
): Triple<InputStream, String, Long> {
|
): Triple<InputStream, String, Long> {
|
||||||
val (chapterData, mangaTitle) =
|
val (chapterData, fileName) = getChapterWithCbzFileName(chapterId)
|
||||||
transaction {
|
|
||||||
val row =
|
|
||||||
(ChapterTable innerJoin MangaTable)
|
|
||||||
.select(ChapterTable.columns + MangaTable.columns)
|
|
||||||
.where { ChapterTable.id eq chapterId }
|
|
||||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
|
||||||
val chapter = ChapterTable.toDataClass(row)
|
|
||||||
val title = row[MangaTable.title]
|
|
||||||
Pair(chapter, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileName = "$mangaTitle - [${chapterData.scanlator}] ${chapterData.name}.cbz"
|
|
||||||
|
|
||||||
val cbzFile = provider(chapterData.mangaId, chapterData.id).getAsArchiveStream()
|
val cbzFile = provider(chapterData.mangaId, chapterData.id).getAsArchiveStream()
|
||||||
|
|
||||||
@@ -96,20 +101,7 @@ object ChapterDownloadHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getCbzMetadataForDownload(chapterId: Int): Triple<String, Long, String> { // fileName, fileSize, contentType
|
fun getCbzMetadataForDownload(chapterId: Int): Triple<String, Long, String> { // fileName, fileSize, contentType
|
||||||
val (chapterData, mangaTitle) =
|
val (chapterData, fileName) = getChapterWithCbzFileName(chapterId)
|
||||||
transaction {
|
|
||||||
val row =
|
|
||||||
(ChapterTable innerJoin MangaTable)
|
|
||||||
.select(ChapterTable.columns + MangaTable.columns)
|
|
||||||
.where { ChapterTable.id eq chapterId }
|
|
||||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
|
||||||
val chapter = ChapterTable.toDataClass(row)
|
|
||||||
val title = row[MangaTable.title]
|
|
||||||
Pair(chapter, title)
|
|
||||||
}
|
|
||||||
|
|
||||||
val scanlatorPart = chapterData.scanlator?.let { "[$it] " } ?: ""
|
|
||||||
val fileName = "$mangaTitle - $scanlatorPart${chapterData.name}.cbz"
|
|
||||||
|
|
||||||
val fileSize = provider(chapterData.mangaId, chapterData.id).getArchiveSize()
|
val fileSize = provider(chapterData.mangaId, chapterData.id).getArchiveSize()
|
||||||
val contentType = "application/vnd.comicbook+zip"
|
val contentType = "application/vnd.comicbook+zip"
|
||||||
|
|||||||
@@ -81,39 +81,45 @@ private class ChapterForDownload(
|
|||||||
|
|
||||||
log.debug { "isMarkedAsDownloaded= $isMarkedAsDownloaded, dbPageCount= $dbPageCount, downloadPageCount= $downloadPageCount" }
|
log.debug { "isMarkedAsDownloaded= $isMarkedAsDownloaded, dbPageCount= $dbPageCount, downloadPageCount= $downloadPageCount" }
|
||||||
|
|
||||||
if (!doesDownloadExist) {
|
return if (!doesDownloadExist) {
|
||||||
log.debug { "reset download status and fetch page list" }
|
log.debug { "reset download status and fetch page list" }
|
||||||
|
updateDownloadStatusAndPageList(false)
|
||||||
updateDownloadStatus(false)
|
} else {
|
||||||
updatePageList()
|
|
||||||
|
|
||||||
return asDataClass()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMarkedAsDownloaded) {
|
|
||||||
log.debug { "mark as downloaded" }
|
|
||||||
|
|
||||||
updateDownloadStatus(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!doPageCountsMatch) {
|
|
||||||
log.debug { "use page count of downloaded chapter" }
|
|
||||||
|
|
||||||
updatePageCount(downloadPageCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
return asDataClass()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun asDataClass() =
|
|
||||||
ChapterTable.toDataClass(
|
|
||||||
transaction {
|
transaction {
|
||||||
ChapterTable
|
var needsUpdate = false
|
||||||
.selectAll()
|
|
||||||
.where { ChapterTable.id eq chapterId }
|
if (!isMarkedAsDownloaded) {
|
||||||
.first()
|
log.debug { "mark as downloaded" }
|
||||||
},
|
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||||
)
|
it[isDownloaded] = true
|
||||||
|
}
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doPageCountsMatch) {
|
||||||
|
log.debug { "use page count of downloaded chapter" }
|
||||||
|
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||||
|
it[pageCount] = downloadPageCount
|
||||||
|
it[lastPageRead] = chapterEntry[ChapterTable.lastPageRead].coerceAtMost(downloadPageCount - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return updated chapter data
|
||||||
|
val updatedRow =
|
||||||
|
ChapterTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ChapterTable.id eq chapterId }
|
||||||
|
.first()
|
||||||
|
|
||||||
|
if (needsUpdate) {
|
||||||
|
chapterEntry = updatedRow
|
||||||
|
}
|
||||||
|
|
||||||
|
ChapterTable.toDataClass(updatedRow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
chapterEntry = freshChapterEntry(optChapterId, optChapterIndex, optMangaId)
|
chapterEntry = freshChapterEntry(optChapterId, optChapterIndex, optMangaId)
|
||||||
@@ -145,11 +151,42 @@ private class ChapterForDownload(
|
|||||||
}.first()
|
}.first()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updatePageList() {
|
private suspend fun updateDownloadStatusAndPageList(downloaded: Boolean): ChapterDataClass {
|
||||||
val mutex = mutexByChapterId.get(chapterId) { Mutex() }
|
val mutex = mutexByChapterId.get(chapterId) { Mutex() }
|
||||||
mutex.withLock {
|
return mutex.withLock {
|
||||||
val pageList = fetchPageList()
|
val pageList = fetchPageList()
|
||||||
updateDatabasePages(pageList)
|
|
||||||
|
transaction {
|
||||||
|
// Update download status
|
||||||
|
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||||
|
it[isDownloaded] = downloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing pages and insert new ones
|
||||||
|
PageTable.deleteWhere { PageTable.chapter eq chapterId }
|
||||||
|
PageTable.batchInsert(pageList) { page ->
|
||||||
|
this[PageTable.index] = page.index
|
||||||
|
this[PageTable.url] = page.url
|
||||||
|
this[PageTable.imageUrl] = page.imageUrl
|
||||||
|
this[PageTable.chapter] = chapterId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update page count
|
||||||
|
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
||||||
|
it[pageCount] = pageList.size
|
||||||
|
it[lastPageRead] = chapterEntry[ChapterTable.lastPageRead].coerceAtMost(pageList.size - 1).coerceAtLeast(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get updated chapter data
|
||||||
|
val updatedRow =
|
||||||
|
ChapterTable
|
||||||
|
.selectAll()
|
||||||
|
.where { ChapterTable.id eq chapterId }
|
||||||
|
.first()
|
||||||
|
|
||||||
|
chapterEntry = updatedRow
|
||||||
|
ChapterTable.toDataClass(updatedRow)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,46 +204,4 @@ private class ChapterForDownload(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateDownloadStatus(downloaded: Boolean) {
|
|
||||||
transaction {
|
|
||||||
ChapterTable.update({ (ChapterTable.sourceOrder eq chapterIndex) and (ChapterTable.manga eq mangaId) }) {
|
|
||||||
it[isDownloaded] = downloaded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateDatabasePages(pageList: List<Page>) {
|
|
||||||
transaction {
|
|
||||||
PageTable.deleteWhere { PageTable.chapter eq chapterId }
|
|
||||||
PageTable.batchInsert(pageList) { page ->
|
|
||||||
this[PageTable.index] = page.index
|
|
||||||
this[PageTable.url] = page.url
|
|
||||||
this[PageTable.imageUrl] = page.imageUrl
|
|
||||||
this[PageTable.chapter] = chapterId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatePageCount(pageList.size)
|
|
||||||
|
|
||||||
// chapter was updated
|
|
||||||
chapterEntry = freshChapterEntry(chapterId, chapterIndex, mangaId)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePageCount(pageCount: Int) {
|
|
||||||
transaction {
|
|
||||||
ChapterTable.update({ ChapterTable.id eq chapterId }) {
|
|
||||||
it[ChapterTable.pageCount] = pageCount
|
|
||||||
it[ChapterTable.lastPageRead] = chapterEntry[ChapterTable.lastPageRead].coerceAtMost(pageCount - 1).coerceAtLeast(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun firstPageExists(): Boolean =
|
|
||||||
try {
|
|
||||||
ChapterDownloadHelper.getImage(mangaId, chapterId, 0).first.close()
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package suwayomi.tachidesk.manga.impl.util
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* 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 org.jetbrains.exposed.sql.ResultRow
|
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||||
@@ -20,31 +19,38 @@ import java.io.File
|
|||||||
|
|
||||||
private val applicationDirs: ApplicationDirs by injectLazy()
|
private val applicationDirs: ApplicationDirs by injectLazy()
|
||||||
|
|
||||||
private fun getMangaDir(mangaId: Int): String {
|
private fun getMangaDir(mangaId: Int): String =
|
||||||
val mangaEntry = getMangaEntry(mangaId)
|
transaction {
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
|
||||||
val sourceDir = SafePath.buildValidFilename(source.toString())
|
val sourceDir = SafePath.buildValidFilename(source.toString())
|
||||||
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
||||||
return "$sourceDir/$mangaDir"
|
"$sourceDir/$mangaDir"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getChapterDir(
|
private fun getChapterDir(
|
||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
chapterId: Int,
|
chapterId: Int,
|
||||||
): String {
|
): String =
|
||||||
val chapterEntry = transaction { ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first() }
|
transaction {
|
||||||
|
// Get chapter data and build chapter-specific directory name
|
||||||
|
val chapterEntry = ChapterTable.selectAll().where { ChapterTable.id eq chapterId }.first()
|
||||||
|
|
||||||
val chapterDir =
|
val chapterDir =
|
||||||
SafePath.buildValidFilename(
|
SafePath.buildValidFilename(
|
||||||
when {
|
when {
|
||||||
chapterEntry[ChapterTable.scanlator] != null -> "${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
chapterEntry[ChapterTable.scanlator] != null -> {
|
||||||
else -> chapterEntry[ChapterTable.name]
|
"${chapterEntry[ChapterTable.scanlator]}_${chapterEntry[ChapterTable.name]}"
|
||||||
},
|
}
|
||||||
)
|
else -> chapterEntry[ChapterTable.name]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return getMangaDir(mangaId) + "/$chapterDir"
|
// Get manga directory and combine with chapter directory
|
||||||
}
|
// Note: This creates a nested transaction, but Exposed handles this with useNestedTransactions=true
|
||||||
|
getMangaDir(mangaId) + "/$chapterDir"
|
||||||
|
}
|
||||||
|
|
||||||
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
|
fun getThumbnailDownloadPath(mangaId: Int): String = applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
|
||||||
|
|
||||||
@@ -70,16 +76,21 @@ fun updateMangaDownloadDir(
|
|||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
newTitle: String,
|
newTitle: String,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val mangaEntry = getMangaEntry(mangaId)
|
// Get current manga directory (uses its own transaction)
|
||||||
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val currentMangaDir = getMangaDir(mangaId)
|
||||||
|
|
||||||
val sourceDir = SafePath.buildValidFilename(source.toString())
|
// Build new directory path
|
||||||
val mangaDir = SafePath.buildValidFilename(mangaEntry[MangaTable.title])
|
val newMangaDir =
|
||||||
|
transaction {
|
||||||
|
val mangaEntry = MangaTable.selectAll().where { MangaTable.id eq mangaId }.first()
|
||||||
|
val source = GetCatalogueSource.getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
|
val sourceDir = SafePath.buildValidFilename(source.toString())
|
||||||
|
val newMangaDirName = SafePath.buildValidFilename(newTitle)
|
||||||
|
"$sourceDir/$newMangaDirName"
|
||||||
|
}
|
||||||
|
|
||||||
val newMangaDir = SafePath.buildValidFilename(newTitle)
|
val oldDir = "${applicationDirs.downloadsRoot}/$currentMangaDir"
|
||||||
|
val newDir = "${applicationDirs.downloadsRoot}/$newMangaDir"
|
||||||
val oldDir = "${applicationDirs.downloadsRoot}/$sourceDir/$mangaDir"
|
|
||||||
val newDir = "${applicationDirs.downloadsRoot}/$sourceDir/$newMangaDir"
|
|
||||||
|
|
||||||
val oldDirFile = File(oldDir)
|
val oldDirFile = File(oldDir)
|
||||||
val newDirFile = File(newDir)
|
val newDirFile = File(newDir)
|
||||||
@@ -90,5 +101,3 @@ fun updateMangaDownloadDir(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMangaEntry(mangaId: Int): ResultRow = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ package suwayomi.tachidesk.server.database
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* 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 com.zaxxer.hikari.HikariConfig
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import de.neonew.exposed.migrations.loadMigrationsFrom
|
import de.neonew.exposed.migrations.loadMigrationsFrom
|
||||||
import de.neonew.exposed.migrations.runMigrations
|
import de.neonew.exposed.migrations.runMigrations
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
@@ -26,12 +28,58 @@ import suwayomi.tachidesk.server.util.shutdownApp
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.sql.SQLException
|
import java.sql.SQLException
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
object DBManager {
|
object DBManager {
|
||||||
var db: Database? = null
|
var db: Database? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var hikariDataSource: HikariDataSource? = null
|
||||||
|
|
||||||
|
private fun createHikariDataSource(): HikariDataSource {
|
||||||
|
val applicationDirs = Injekt.get<ApplicationDirs>()
|
||||||
|
val config =
|
||||||
|
HikariConfig().apply {
|
||||||
|
when (serverConfig.databaseType.value) {
|
||||||
|
DatabaseType.POSTGRESQL -> {
|
||||||
|
jdbcUrl = "jdbc:${serverConfig.databaseUrl.value}"
|
||||||
|
driverClassName = "org.postgresql.Driver"
|
||||||
|
username = serverConfig.databaseUsername.value
|
||||||
|
password = serverConfig.databasePassword.value
|
||||||
|
// PostgreSQL specific optimizations
|
||||||
|
addDataSourceProperty("cachePrepStmts", "true")
|
||||||
|
addDataSourceProperty("prepStmtCacheSize", "25")
|
||||||
|
addDataSourceProperty("prepStmtCacheSqlLimit", "256")
|
||||||
|
addDataSourceProperty("useServerPrepStmts", "true")
|
||||||
|
}
|
||||||
|
DatabaseType.H2 -> {
|
||||||
|
jdbcUrl = "jdbc:h2:${applicationDirs.dataRoot}/database"
|
||||||
|
driverClassName = "org.h2.Driver"
|
||||||
|
// H2 specific optimizations
|
||||||
|
addDataSourceProperty("cachePrepStmts", "true")
|
||||||
|
addDataSourceProperty("prepStmtCacheSize", "25")
|
||||||
|
addDataSourceProperty("prepStmtCacheSqlLimit", "256")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized for Raspberry Pi / Low memory environments
|
||||||
|
maximumPoolSize = 6 // Moderate pool for better concurrency
|
||||||
|
minimumIdle = 2 // Keep 2 idle connections for responsiveness
|
||||||
|
connectionTimeout = 45.seconds.inWholeMilliseconds // more tolerance for slow devices
|
||||||
|
idleTimeout = 5.minutes.inWholeMilliseconds // close idle connections faster
|
||||||
|
maxLifetime = 15.minutes.inWholeMilliseconds // recycle connections more often
|
||||||
|
leakDetectionThreshold = 1.minutes.inWholeMilliseconds
|
||||||
|
|
||||||
|
// Pool name for monitoring
|
||||||
|
poolName = "Suwayomi-DB-Pool"
|
||||||
|
}
|
||||||
|
return HikariDataSource(config)
|
||||||
|
}
|
||||||
|
|
||||||
fun setupDatabase(): Database {
|
fun setupDatabase(): Database {
|
||||||
|
// Clean up existing connections
|
||||||
if (TransactionManager.isInitialized()) {
|
if (TransactionManager.isInitialized()) {
|
||||||
val currentDatabase = TransactionManager.currentOrNull()?.db
|
val currentDatabase = TransactionManager.currentOrNull()?.db
|
||||||
if (currentDatabase != null) {
|
if (currentDatabase != null) {
|
||||||
@@ -39,30 +87,35 @@ object DBManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val applicationDirs = Injekt.get<ApplicationDirs>()
|
// Close the existing pool if any
|
||||||
|
shutdown()
|
||||||
|
|
||||||
val dbConfig =
|
val dbConfig =
|
||||||
DatabaseConfig {
|
DatabaseConfig {
|
||||||
useNestedTransactions = true
|
useNestedTransactions = true
|
||||||
@OptIn(ExperimentalKeywordApi::class)
|
@OptIn(ExperimentalKeywordApi::class)
|
||||||
preserveKeywordCasing = false
|
preserveKeywordCasing = false
|
||||||
}
|
}
|
||||||
return when (serverConfig.databaseType.value) {
|
|
||||||
DatabaseType.POSTGRESQL ->
|
// Create a new HikariCP pool
|
||||||
Database.connect(
|
hikariDataSource = createHikariDataSource()
|
||||||
"jdbc:${serverConfig.databaseUrl.value}",
|
|
||||||
"org.postgresql.Driver",
|
return Database
|
||||||
user = serverConfig.databaseUsername.value,
|
.connect(hikariDataSource!!, databaseConfig = dbConfig)
|
||||||
password = serverConfig.databasePassword.value,
|
.also { db = it }
|
||||||
databaseConfig = dbConfig,
|
|
||||||
)
|
|
||||||
DatabaseType.H2 ->
|
|
||||||
Database.connect(
|
|
||||||
"jdbc:h2:${applicationDirs.dataRoot}/database",
|
|
||||||
"org.h2.Driver",
|
|
||||||
databaseConfig = dbConfig,
|
|
||||||
)
|
|
||||||
}.also { db = it }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shutdown() {
|
||||||
|
hikariDataSource?.close()
|
||||||
|
hikariDataSource = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPoolStats(): String? =
|
||||||
|
hikariDataSource?.let { ds ->
|
||||||
|
"DB Pool Stats - Active: ${ds.hikariPoolMXBean.activeConnections}, " +
|
||||||
|
"Idle: ${ds.hikariPoolMXBean.idleConnections}, " +
|
||||||
|
"Waiting: ${ds.hikariPoolMXBean.threadsAwaitingConnection}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
@@ -74,6 +127,19 @@ fun databaseUp() {
|
|||||||
"Using ${db.vendor} database version ${db.version}"
|
"Using ${db.vendor} database version ${db.version}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log pool statistics
|
||||||
|
DBManager.getPoolStats()?.let { stats ->
|
||||||
|
logger.info { "HikariCP initialized: $stats" }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add shutdown hook to properly close HikariCP pool
|
||||||
|
Runtime.getRuntime().addShutdownHook(
|
||||||
|
Thread {
|
||||||
|
logger.info { "Shutting down HikariCP connection pool..." }
|
||||||
|
DBManager.shutdown()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (serverConfig.databaseType.value == DatabaseType.POSTGRESQL) {
|
if (serverConfig.databaseType.value == DatabaseType.POSTGRESQL) {
|
||||||
transaction {
|
transaction {
|
||||||
|
|||||||
Reference in New Issue
Block a user