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.JsonNamingStrategy
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.koin.core.module.dsl.singleOf
import org.koin.core.parameter.parameterArrayOf
import org.koin.dsl.module
@ -41,6 +42,7 @@ fun Application.configureDependencies(
single {
Dotenv.configure().load()
}
factory { params ->
val defaultRequestUri: String = params.get<String>()
val configBlock = params.getOrNull<(HttpClientConfig<OkHttpConfig>.() -> Unit)>() ?: {}
@ -54,17 +56,6 @@ fun Application.configureDependencies(
}
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> {
GitHubBackendRepository(
get {
@ -106,7 +97,17 @@ fun Application.configureDependencies(
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 {

View File

@ -1,47 +1,41 @@
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.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking
import kotlinx.datetime.*
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.dao.load
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.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.experimental.suspendedTransactionAsync
internal class AnnouncementRepository(private val database: Database) {
internal class AnnouncementRepository {
init {
runBlocking {
transaction {
SchemaUtils.create(AnnouncementTable, AttachmentTable)
SchemaUtils.create(Announcements, Attachments)
}
}
}
suspend fun all() = transaction {
buildSet {
AnnouncementEntity.all().forEach { announcement ->
add(announcement.toApi())
}
}
Announcement.all()
}
suspend fun all(channel: String) = transaction {
buildSet {
AnnouncementEntity.find { AnnouncementTable.channel eq channel }.forEach { announcement ->
add(announcement.toApi())
}
}
Announcement.find { Announcements.channel eq channel }
}
suspend fun delete(id: Int) = transaction {
val announcement = AnnouncementEntity.findById(id) ?: return@transaction
val announcement = Announcement.findById(id) ?: return@transaction
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.
suspend fun latest() = transaction {
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.toApi()
Announcement.all().maxByOrNull { it.id }?.load(Announcement::attachments)
}
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 {
AnnouncementEntity.all().maxByOrNull { it.createdAt }?.id?.value?.let {
Announcement.all().maxByOrNull { it.id }?.id?.value?.let {
APIResponseAnnouncementId(it)
}
}
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)
}
}
@ -72,106 +66,98 @@ internal class AnnouncementRepository(private val database: Database) {
id: Int,
archivedAt: LocalDateTime?,
) = transaction {
AnnouncementEntity.findById(id)?.apply {
this.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
Announcement.findByIdAndUpdate(id) {
it.archivedAt = archivedAt ?: java.time.LocalDateTime.now().toKotlinLocalDateTime()
}
}
suspend fun unarchive(id: Int) = transaction {
AnnouncementEntity.findById(id)?.apply {
archivedAt = null
Announcement.findByIdAndUpdate(id) {
it.archivedAt = null
}
}
suspend fun new(new: APIAnnouncement) = transaction {
AnnouncementEntity.new announcement@{
Announcement.new {
author = new.author
title = new.title
content = new.content
channel = new.channel
createdAt = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
archivedAt = new.archivedAt
level = new.level
}.also { newAnnouncement ->
new.attachmentUrls.map {
AttachmentEntity.new {
url = it
announcement = newAnnouncement
new.attachmentUrls.map { newUrl ->
suspendedTransactionAsync {
Attachment.new {
url = newUrl
announcement = newAnnouncement
}
}
}
}.awaitAll()
}
}
suspend fun update(id: Int, new: APIAnnouncement) = transaction {
AnnouncementEntity.findById(id)?.apply {
author = new.author
title = new.title
content = new.content
channel = new.channel
archivedAt = new.archivedAt
level = new.level
attachments.forEach(AttachmentEntity::delete)
new.attachmentUrls.map {
AttachmentEntity.new {
url = it
announcement = this@apply
Announcement.findByIdAndUpdate(id) {
it.author = new.author
it.title = new.title
it.content = new.content
it.channel = new.channel
it.archivedAt = new.archivedAt
it.level = new.level
}?.also { newAnnouncement ->
newAnnouncement.attachments.map {
suspendedTransactionAsync {
it.delete()
}
}
}.awaitAll()
new.attachmentUrls.map { newUrl ->
suspendedTransactionAsync {
Attachment.new {
url = newUrl
announcement = newAnnouncement
}
}
}.awaitAll()
}
}
private suspend fun <T> transaction(statement: Transaction.() -> T) =
newSuspendedTransaction(Dispatchers.IO, database, statement = statement)
private suspend fun <T> transaction(statement: suspend Transaction.() -> T) =
newSuspendedTransaction(Dispatchers.IO, statement = statement)
private object AnnouncementTable : IntIdTable() {
private object Announcements : IntIdTable() {
val author = varchar("author", 32).nullable()
val title = varchar("title", 64)
val content = text("content").nullable()
val channel = varchar("channel", 16).nullable()
val createdAt = datetime("createdAt")
val createdAt = datetime("createdAt").defaultExpression(CurrentDateTime)
val archivedAt = datetime("archivedAt").nullable()
val level = integer("level")
}
private object AttachmentTable : IntIdTable() {
private object Attachments : IntIdTable() {
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) {
companion object : IntEntityClass<AnnouncementEntity>(AnnouncementTable)
class Announcement(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Announcement>(Announcements)
var author by AnnouncementTable.author
var title by AnnouncementTable.title
var content by AnnouncementTable.content
val attachments by AttachmentEntity referrersOn announcement
var channel by AnnouncementTable.channel
var createdAt by AnnouncementTable.createdAt
var archivedAt by AnnouncementTable.archivedAt
var level by AnnouncementTable.level
fun toApi() = APIResponseAnnouncement(
id.value,
author,
title,
content,
attachmentUrls = buildSet {
attachments.forEach {
add(it.url)
}
},
channel,
createdAt,
archivedAt,
level,
)
var author by Announcements.author
var title by Announcements.title
var content by Announcements.content
val attachments by Attachment referrersOn Attachments.announcement
var channel by Announcements.channel
var createdAt by Announcements.createdAt
var archivedAt by Announcements.archivedAt
var level by Announcements.level
}
class AttachmentEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<AttachmentEntity>(AttachmentTable)
class Attachment(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<Attachment>(Attachments)
var url by AttachmentTable.url
var announcement by AnnouncementEntity referencedOn AttachmentTable.announcement
var url by Attachments.url
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.
*/
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.
@ -55,7 +56,8 @@ abstract class BackendRepository internal constructor(
* @property url The URL to the GPG master key.
*/
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,
)
}
@ -66,7 +68,8 @@ abstract class BackendRepository internal constructor(
* @property contributors The contributors of the repository.
*/
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.
@ -95,10 +98,11 @@ abstract class BackendRepository internal constructor(
val tag: String,
val releaseNote: String,
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 {
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.
* @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.
@ -157,7 +161,7 @@ abstract class BackendRepository internal constructor(
* @param organization The name of the organization.
* @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.

View File

@ -39,15 +39,15 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
name = it.name,
downloadUrl = it.browserDownloadUrl,
)
}.toSet(),
},
)
}
override suspend fun contributors(
owner: String,
repository: String,
): Set<BackendContributor> {
val contributors: Set<GitHubContributor> = client.get(
): List<BackendContributor> {
val contributors: List<GitHubContributor> = client.get(
Contributors(
owner,
repository,
@ -61,12 +61,12 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
url = it.htmlUrl,
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.
val members: Set<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
val members: List<GitHubOrganization.GitHubMember> = client.get(Organization.Members(organization)).body()
return coroutineScope {
members.map { member ->
@ -78,7 +78,7 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
},
async {
// 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
@Suppress("UNCHECKED_CAST")
val gpgKeys = responses[1] as Set<GitHubUser.GitHubGpgKey>
val gpgKeys = responses[1] as List<GitHubUser.GitHubGpgKey>
BackendMember(
name = user.login,
@ -96,11 +96,11 @@ class GitHubBackendRepository(client: HttpClient) : BackendRepository(client) {
bio = user.bio,
gpgKeys =
BackendMember.GpgKeys(
ids = gpgKeys.map { it.keyId }.toSet(),
ids = gpgKeys.map { it.keyId },
url = "https://api.github.com/users/${user.login}.gpg",
),
)
}.toSet()
}
}
override suspend fun rateLimit(): BackendRateLimit {
@ -153,7 +153,8 @@ class GitHubOrganization {
@Serializable
class GitHubRelease(
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 body: String,
) {

View File

@ -8,7 +8,8 @@ class APIRelease(
val version: String,
val createdAt: LocalDateTime,
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 {
@ -42,7 +43,8 @@ class APIContributor(
@Serializable
class APIContributable(
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
@ -69,7 +71,8 @@ class APIAnnouncement(
val author: String? = null,
val title: String,
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 archivedAt: LocalDateTime? = null,
val level: Int = 0,
@ -81,7 +84,8 @@ class APIResponseAnnouncement(
val author: String? = null,
val title: String,
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 createdAt: LocalDateTime,
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.schema.APIAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncement
import app.revanced.api.configuration.schema.APIResponseAnnouncementId
import kotlinx.datetime.LocalDateTime
@ -11,11 +12,11 @@ internal class AnnouncementService(
suspend fun latestId(channel: String): APIResponseAnnouncementId? = announcementRepository.latestId(channel)
suspend fun latestId(): APIResponseAnnouncementId? = announcementRepository.latestId()
suspend fun latest(channel: String) = announcementRepository.latest(channel)
suspend fun latest() = announcementRepository.latest()
suspend fun latest(channel: String) = announcementRepository.latest(channel)?.toApi()
suspend fun latest() = announcementRepository.latest()?.toApi()
suspend fun all(channel: String) = announcementRepository.all(channel)
suspend fun all() = announcementRepository.all()
suspend fun all(channel: String) = announcementRepository.all(channel).map { it.toApi() }
suspend fun all() = announcementRepository.all().map { it.toApi() }
suspend fun new(new: APIAnnouncement) {
announcementRepository.new(new)
@ -32,4 +33,16 @@ internal class AnnouncementService(
suspend fun delete(id: Int) {
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,
backendRepository.contributors(configurationRepository.organization, it).map {
APIContributor(it.name, it.avatarUrl, it.url, it.contributions)
}.toSet(),
},
)
}
}
}.awaitAll().toSet()
}.awaitAll()
suspend fun team() = backendRepository.members(configurationRepository.organization).map { member ->
APIMember(
@ -41,7 +41,7 @@ internal class ApiService(
},
)
}.toSet()
}
suspend fun rateLimit() = backendRepository.rateLimit()?.let {
APIRateLimit(it.limit, it.remaining, it.reset)

View File

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