perf: Make async db transactions and use List instead of Set

This commit is contained in:
oSumAtrIX 2024-07-08 00:41:05 +02:00
parent 01780188b9
commit a7d1892343
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
8 changed files with 131 additions and 122 deletions

View File

@ -27,6 +27,7 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy import kotlinx.serialization.json.JsonNamingStrategy
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.core.parameter.parameterArrayOf import org.koin.core.parameter.parameterArrayOf
import org.koin.dsl.module import org.koin.dsl.module
@ -41,6 +42,7 @@ fun Application.configureDependencies(
single { single {
Dotenv.configure().load() Dotenv.configure().load()
} }
factory { params -> factory { params ->
val defaultRequestUri: String = params.get<String>() val defaultRequestUri: String = params.get<String>()
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {} val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
@ -54,17 +56,6 @@ fun Application.configureDependencies(
} }
val repositoryModule = module { val repositoryModule = module {
single {
val dotenv = get<Dotenv>()
Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
driver = "org.h2.Driver",
)
}
single<BackendRepository> { single<BackendRepository> {
GitHubBackendRepository( GitHubBackendRepository(
get { get {
@ -106,7 +97,17 @@ fun Application.configureDependencies(
Toml.decodeFromStream(configFile.inputStream()) Toml.decodeFromStream(configFile.inputStream())
} }
singleOf(::AnnouncementRepository) single {
val dotenv = get<Dotenv>()
TransactionManager.defaultDatabase = Database.connect(
url = dotenv["DB_URL"],
user = dotenv["DB_USER"],
password = dotenv["DB_PASSWORD"],
)
AnnouncementRepository()
}
} }
val serviceModule = module { val serviceModule = module {

View File

@ -1,47 +1,41 @@
package app.revanced.api.configuration.repository package app.revanced.api.configuration.repository
import app.revanced.api.configuration.repository.AnnouncementRepository.AttachmentTable.announcement
import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.datetime.* import kotlinx.datetime.*
import org.jetbrains.exposed.dao.IntEntity import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.dao.load
import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.kotlin.datetime.CurrentDateTime
import org.jetbrains.exposed.sql.kotlin.datetime.datetime import org.jetbrains.exposed.sql.kotlin.datetime.datetime
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
internal class AnnouncementRepository(private val database: Database) { internal class AnnouncementRepository {
init { init {
runBlocking { runBlocking {
transaction { transaction {
SchemaUtils.create(AnnouncementTable, AttachmentTable) SchemaUtils.create(Announcements, Attachments)
} }
} }
} }
suspend fun all() = transaction { suspend fun all() = transaction {
buildSet { Announcement.all()
AnnouncementEntity.all().forEach { announcement ->
add(announcement.toApi())
}
}
} }
suspend fun all(channel: String) = transaction { suspend fun all(channel: String) = transaction {
buildSet { Announcement.find { Announcements.channel eq channel }
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement ->
add(announcement.toApi())
}
}
} }
suspend fun delete(id: Int) = transaction { suspend fun delete(id: Int) = transaction {
val announcement = AnnouncementEntity.findById(id) ?: return@transaction val announcement = Announcement.findById(id) ?: return@transaction
announcement.delete() announcement.delete()
} }
@ -49,21 +43,21 @@ internal class AnnouncementRepository(private val database: Database) {
// TODO: These are inefficient, but I'm not sure how to make them more efficient. // TODO: These are inefficient, but I'm not sure how to make them more efficient.
suspend fun latest() = transaction { suspend fun latest() = transaction {
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi() Announcement.all().maxByOrNull { it.id }?.load(Announcement::attachments)
} }
suspend fun latest(channel: String) = transaction { suspend fun latest(channel: String) = transaction {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.toApi() Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.load(Announcement::attachments)
} }
suspend fun latestId() = transaction { suspend fun latestId() = transaction {
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let { Announcement.all().maxByOrNull { it.id }?.id?.value?.let {
APIResponseAnnouncementId(it) APIResponseAnnouncementId(it)
} }
} }
suspend fun latestId(channel: String) = transaction { suspend fun latestId(channel: String) = transaction {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.maxByOrNull { it.createdAt }?.id?.value?.let { Announcement.find { Announcements.channel eq channel }.maxByOrNull { it.id }?.id?.value?.let {
APIResponseAnnouncementId(it) APIResponseAnnouncementId(it)
} }
} }
@ -72,106 +66,98 @@ internal class AnnouncementRepository(private val database: Database) {
id: Int, id: Int,
archivedAt: LocalDateTime?, archivedAt: LocalDateTime?,
) = transaction { ) = transaction {
AnnouncementEntity.findById(id)?.apply { Announcement.findByIdAndUpdate(id) {
this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime() it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
} }
} }
suspend fun unarchive(id: Int) = transaction { suspend fun unarchive(id: Int) = transaction {
AnnouncementEntity.findById(id)?.apply { Announcement.findByIdAndUpdate(id) {
archivedAt = null it.archivedAt = null
} }
} }
suspend fun new(new: APIAnnouncement) = transaction { suspend fun new(new: APIAnnouncement) = transaction {
AnnouncementEntity.new announcement@{ Announcement.new {
author = new.author author = new.author
title = new.title title = new.title
content = new.content content = new.content
channel = new.channel channel = new.channel
createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
archivedAt = new.archivedAt archivedAt = new.archivedAt
level = new.level level = new.level
}.also { newAnnouncement -> }.also { newAnnouncement ->
new.attachmentUrls.map { new.attachmentUrls.map { newUrl ->
AttachmentEntity.new { suspendedTransactionAsync {
url = it Attachment.new {
url = newUrl
announcement = newAnnouncement announcement = newAnnouncement
} }
} }
}.awaitAll()
} }
} }
suspend fun update(id: Int, new: APIAnnouncement) = transaction { suspend fun update(id: Int, new: APIAnnouncement) = transaction {
AnnouncementEntity.findById(id)?.apply { Announcement.findByIdAndUpdate(id) {
author = new.author it.author = new.author
title = new.title it.title = new.title
content = new.content it.content = new.content
channel = new.channel it.channel = new.channel
archivedAt = new.archivedAt it.archivedAt = new.archivedAt
level = new.level it.level = new.level
}?.also { newAnnouncement ->
newAnnouncement.attachments.map {
suspendedTransactionAsync {
it.delete()
}
}.awaitAll()
attachments.forEach(AttachmentEntity::delete) new.attachmentUrls.map { newUrl ->
new.attachmentUrls.map { suspendedTransactionAsync {
AttachmentEntity.new { Attachment.new {
url = it url = newUrl
announcement = this@apply announcement = newAnnouncement
} }
} }
}.awaitAll()
} }
} }
private suspend fun <T> transaction(statement: Transaction.() -> T) = private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
newSuspendedTransaction(Dispatchers.IO, database, statement = statement) newSuspendedTransaction(Dispatchers.IO, statement = statement)
private object AnnouncementTable : IntIdTable() { private object Announcements : IntIdTable() {
val author = varchar("author", 32).nullable() val author = varchar("author", 32).nullable()
val title = varchar("title", 64) val title = varchar("title", 64)
val content = text("content").nullable() val content = text("content").nullable()
val channel = varchar("channel", 16).nullable() val channel = varchar("channel", 16).nullable()
val createdAt = datetime("createdAt") val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime)
val archivedAt = datetime("archivedAt").nullable() val archivedAt = datetime("archivedAt").nullable()
val level = integer("level") val level = integer("level")
} }
private object AttachmentTable : IntIdTable() { private object Attachments : IntIdTable() {
val url = varchar("url", 256) val url = varchar("url", 256)
val announcement = reference("announcement", AnnouncementTable, onDelete = ReferenceOption.CASCADE) val announcement = reference("announcement", Announcements, onDelete = ReferenceOption.CASCADE)
} }
class AnnouncementEntity(id: EntityID<Int>) : IntEntity(id) { class Announcement(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<AnnouncementEntity>(AnnouncementTable) companion object : IntEntityClass<Announcement>(Announcements)
var author by AnnouncementTable.author var author by Announcements.author
var title by AnnouncementTable.title var title by Announcements.title
var content by AnnouncementTable.content var content by Announcements.content
val attachments by AttachmentEntity referrersOn announcement val attachments by Attachment referrersOn Attachments.announcement
var channel by AnnouncementTable.channel var channel by Announcements.channel
var createdAt by AnnouncementTable.createdAt var createdAt by Announcements.createdAt
var archivedAt by AnnouncementTable.archivedAt var archivedAt by Announcements.archivedAt
var level by AnnouncementTable.level var level by Announcements.level
fun toApi() = APIResponseAnnouncement(
id.value,
author,
title,
content,
attachmentUrls = buildSet {
attachments.forEach {
add(it.url)
}
},
channel,
createdAt,
archivedAt,
level,
)
} }
class AttachmentEntity(id: EntityID<Int>) : IntEntity(id) { class Attachment(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<AttachmentEntity>(AttachmentTable) companion object : IntEntityClass<Attachment>(Attachments)
var url by AttachmentTable.url var url by Attachments.url
var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement var announcement by Announcement referencedOn Attachments.announcement
} }
} }

View File

@ -30,7 +30,8 @@ abstract class BackendRepository internal constructor(
* @property members The members of the organization. * @property members The members of the organization.
*/ */
class BackendOrganization( class BackendOrganization(
val members: Set<BackendMember>, // Using a list instead of a set because set semantics are unnecessary here.
val members: List<BackendMember>,
) { ) {
/** /**
* A member of an organization. * A member of an organization.
@ -55,7 +56,8 @@ abstract class BackendRepository internal constructor(
* @property url The URL to the GPG master key. * @property url The URL to the GPG master key.
*/ */
class GpgKeys( class GpgKeys(
val ids: Set<String>, // Using a list instead of a set because set semantics are unnecessary here.
val ids: List<String>,
val url: String, val url: String,
) )
} }
@ -66,7 +68,8 @@ abstract class BackendRepository internal constructor(
* @property contributors The contributors of the repository. * @property contributors The contributors of the repository.
*/ */
class BackendRepository( class BackendRepository(
val contributors: Set<BackendContributor>, // Using a list instead of a set because set semantics are unnecessary here.
val contributors: List<BackendContributor>,
) { ) {
/** /**
* A contributor of a repository. * A contributor of a repository.
@ -95,10 +98,11 @@ abstract class BackendRepository internal constructor(
val tag: String, val tag: String,
val releaseNote: String, val releaseNote: String,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val assets: Set<BackendAsset>, // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<BackendAsset>,
) { ) {
companion object { companion object {
fun Set<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) } fun List<BackendAsset>.first(assetRegex: Regex) = first { assetRegex.containsMatchIn(it.name) }
} }
/** /**
@ -149,7 +153,7 @@ abstract class BackendRepository internal constructor(
* @param repository The name of the repository. * @param repository The name of the repository.
* @return The contributors. * @return The contributors.
*/ */
abstract suspend fun contributors(owner: String, repository: String): Set<BackendOrganization.BackendRepository.BackendContributor> abstract suspend fun contributors(owner: String, repository: String): List<BackendOrganization.BackendRepository.BackendContributor>
/** /**
* Get the members of an organization. * Get the members of an organization.
@ -157,7 +161,7 @@ abstract class BackendRepository internal constructor(
* @param organization The name of the organization. * @param organization The name of the organization.
* @return The members. * @return The members.
*/ */
abstract suspend fun members(organization: String): Set<BackendOrganization.BackendMember> abstract suspend fun members(organization: String): List<BackendOrganization.BackendMember>
/** /**
* Get the rate limit of the backend. * Get the rate limit of the backend.

View File

@ -39,15 +39,15 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
name = it.name, name = it.name,
downloadUrl = it.browserDownloadUrl, downloadUrl = it.browserDownloadUrl,
) )
}.toSet(), },
) )
} }
override suspend fun contributors( override suspend fun contributors(
owner: String, owner: String,
repository: String, repository: String,
): Set<BackendContributor> { ): List<BackendContributor> {
val contributors: Set<GitHubContributor> = client.get( val contributors: List<GitHubContributor> = client.get(
Contributors( Contributors(
owner, owner,
repository, repository,
@ -61,12 +61,12 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
url = it.htmlUrl, url = it.htmlUrl,
contributions = it.contributions, contributions = it.contributions,
) )
}.toSet() }
} }
override suspend fun members(organization: String): Set<BackendMember> { override suspend fun members(organization: String): List<BackendMember> {
// Get the list of members of the organization. // Get the list of members of the organization.
val members: Set<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body() val members: List<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
return coroutineScope { return coroutineScope {
members.map { member -> members.map { member ->
@ -78,7 +78,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
}, },
async { async {
// Get the GPG key of the user. // Get the GPG key of the user.
client.get(User.GpgKeys(member.login)).body<Set<GitHubUser.GitHubGpgKey>>() client.get(User.GpgKeys(member.login)).body<List<GitHubUser.GitHubGpgKey>>()
}, },
) )
} }
@ -87,7 +87,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
val user = responses[0] as GitHubUser val user = responses[0] as GitHubUser
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val gpgKeys = responses[1] as Set<GitHubUser.GitHubGpgKey> val gpgKeys = responses[1] as List<GitHubUser.GitHubGpgKey>
BackendMember( BackendMember(
name = user.login, name = user.login,
@ -96,11 +96,11 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
bio = user.bio, bio = user.bio,
gpgKeys = gpgKeys =
BackendMember.GpgKeys( BackendMember.GpgKeys(
ids = gpgKeys.map { it.keyId }.toSet(), ids = gpgKeys.map { it.keyId },
url = "https://api.github.com/users/${user.login}.gpg", url = "https://api.github.com/users/${user.login}.gpg",
), ),
) )
}.toSet() }
} }
override suspend fun rateLimit(): BackendRateLimit { override suspend fun rateLimit(): BackendRateLimit {
@ -153,7 +153,8 @@ class GitHubOrganization {
@Serializable @Serializable
class GitHubRelease( class GitHubRelease(
val tagName: String, val tagName: String,
val assets: Set<GitHubAsset>, // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<GitHubAsset>,
val createdAt: Instant, val createdAt: Instant,
val body: String, val body: String,
) { ) {

View File

@ -8,7 +8,8 @@ class APIRelease(
val version: String, val version: String,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val description: String, val description: String,
val assets: Set<APIAsset>, // Using a list instead of a set because set semantics are unnecessary here.
val assets: List<APIAsset>,
) )
interface APIUser { interface APIUser {
@ -42,7 +43,8 @@ class APIContributor(
@Serializable @Serializable
class APIContributable( class APIContributable(
val name: String, val name: String,
val contributors: Set<APIContributor>, // Using a list instead of a set because set semantics are unnecessary here.
val contributors: List<APIContributor>,
) )
@Serializable @Serializable
@ -69,7 +71,8 @@ class APIAnnouncement(
val author: String? = null, val author: String? = null,
val title: String, val title: String,
val content: String? = null, val content: String? = null,
val attachmentUrls: Set<String> = emptySet(), // Using a list instead of a set because set semantics are unnecessary here.
val attachmentUrls: List<String> = emptyList(),
val channel: String? = null, val channel: String? = null,
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,
val level: Int = 0, val level: Int = 0,
@ -81,7 +84,8 @@ class APIResponseAnnouncement(
val author: String? = null, val author: String? = null,
val title: String, val title: String,
val content: String? = null, val content: String? = null,
val attachmentUrls: Set<String> = emptySet(), // Using a list instead of a set because set semantics are unnecessary here.
val attachmentUrls: List<String> = emptyList(),
val channel: String? = null, val channel: String? = null,
val createdAt: LocalDateTime, val createdAt: LocalDateTime,
val archivedAt: LocalDateTime? = null, val archivedAt: LocalDateTime? = null,

View File

@ -2,6 +2,7 @@ package app.revanced.api.configuration.services
import app.revanced.api.configuration.repository.AnnouncementRepository import app.revanced.api.configuration.repository.AnnouncementRepository
import app.revanced.api.configuration.schema.APIAnnouncement import app.revanced.api.configuration.schema.APIAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
@ -11,11 +12,11 @@ internal class AnnouncementService(
suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel) suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId() suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
suspend fun latest(channel: String) = announcementRepository.latest(channel) suspend fun latest(channel: String) = announcementRepository.latest(channel)?.toApi()
suspend fun latest() = announcementRepository.latest() suspend fun latest() = announcementRepository.latest()?.toApi()
suspend fun all(channel: String) = announcementRepository.all(channel) suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() }
suspend fun all() = announcementRepository.all() suspend fun all() = announcementRepository.all().map { it.toApi() }
suspend fun new(new: APIAnnouncement) { suspend fun new(new: APIAnnouncement) {
announcementRepository.new(new) announcementRepository.new(new)
@ -32,4 +33,16 @@ internal class AnnouncementService(
suspend fun delete(id: Int) { suspend fun delete(id: Int) {
announcementRepository.delete(id) announcementRepository.delete(id)
} }
private fun AnnouncementRepository.Announcement.toApi() = APIResponseAnnouncement(
id.value,
author,
title,
content,
attachments.map { it.url },
channel,
createdAt,
archivedAt,
level,
)
} }

View File

@ -19,11 +19,11 @@ internal class ApiService(
it, it,
backendRepository.contributors(configurationRepository.organization, it).map { backendRepository.contributors(configurationRepository.organization, it).map {
APIContributor(it.name, it.avatarUrl, it.url, it.contributions) APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
}.toSet(), },
) )
} }
} }
}.awaitAll().toSet() }.awaitAll()
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member -> suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
APIMember( APIMember(
@ -41,7 +41,7 @@ internal class ApiService(
}, },
) )
}.toSet() }
suspend fun rateLimit() = backendRepository.rateLimit()?.let { suspend fun rateLimit() = backendRepository.rateLimit()?.let {
APIRateLimit(it.limit, it.remaining, it.reset) APIRateLimit(it.limit, it.remaining, it.reset)

View File

@ -51,7 +51,7 @@ internal class PatchesService(
patchesRelease.tag, patchesRelease.tag,
patchesRelease.createdAt, patchesRelease.createdAt,
patchesRelease.releaseNote, patchesRelease.releaseNote,
setOf(patchesAsset, integrationsAsset), listOf(patchesAsset, integrationsAsset),
) )
} }