From 88386239609c88495e4acbcbe8a2cb3a2af3c02c Mon Sep 17 00:00:00 2001 From: Ziedelth Date: Mon, 20 Nov 2023 17:35:39 +0100 Subject: [PATCH] Added ProfileService and caching for improved performance --- src/main/kotlin/fr/ziedelth/Application.kt | 5 +- ...EpisodeTypesLangTypesPaginationCacheKey.kt | 11 ++++ .../caches/ProfilePaginationCacheKey.kt | 27 ++++++++ .../ziedelth/controllers/ProfileController.kt | 27 +++----- src/main/kotlin/fr/ziedelth/plugins/HTTP.kt | 22 ++++--- .../repositories/ProfileRepository.kt | 46 +------------ .../fr/ziedelth/services/ProfileService.kt | 64 +++++++++++++++++++ 7 files changed, 129 insertions(+), 73 deletions(-) create mode 100644 src/main/kotlin/fr/ziedelth/caches/ProfileEpisodeTypesLangTypesPaginationCacheKey.kt create mode 100644 src/main/kotlin/fr/ziedelth/caches/ProfilePaginationCacheKey.kt create mode 100644 src/main/kotlin/fr/ziedelth/services/ProfileService.kt diff --git a/src/main/kotlin/fr/ziedelth/Application.kt b/src/main/kotlin/fr/ziedelth/Application.kt index 043a9d6..e8a17a6 100644 --- a/src/main/kotlin/fr/ziedelth/Application.kt +++ b/src/main/kotlin/fr/ziedelth/Application.kt @@ -14,9 +14,10 @@ import nu.pattern.OpenCV import java.util.* private lateinit var database: Database +private var isDebug = false fun main(args: Array) { - val isDebug = args.isNotEmpty() && args[0] == "debug" + isDebug = args.isNotEmpty() && args[0] == "debug" val loadImage = !isDebug || (args.size > 1 && args[1] == "loadImage") if (isDebug) { @@ -71,7 +72,7 @@ private fun handleCommands() { fun Application.myApplicationModule() { Logger.info("Configure server...") - configureHTTP() + configureHTTP(isDebug) Logger.info("Configure routing...") configureRouting(database) Logger.info("Server configured and ready") diff --git a/src/main/kotlin/fr/ziedelth/caches/ProfileEpisodeTypesLangTypesPaginationCacheKey.kt b/src/main/kotlin/fr/ziedelth/caches/ProfileEpisodeTypesLangTypesPaginationCacheKey.kt new file mode 100644 index 0000000..c5827cc --- /dev/null +++ b/src/main/kotlin/fr/ziedelth/caches/ProfileEpisodeTypesLangTypesPaginationCacheKey.kt @@ -0,0 +1,11 @@ +package fr.ziedelth.caches + +import java.util.* + +data class ProfileEpisodeTypesLangTypesPaginationCacheKey( + override val profile: UUID, + val episodeTypes: List, + val langTypes: List, + override val page: Int, + override val limit: Int, +) : ProfilePaginationCacheKey(profile, page, limit) diff --git a/src/main/kotlin/fr/ziedelth/caches/ProfilePaginationCacheKey.kt b/src/main/kotlin/fr/ziedelth/caches/ProfilePaginationCacheKey.kt new file mode 100644 index 0000000..ce9c7a0 --- /dev/null +++ b/src/main/kotlin/fr/ziedelth/caches/ProfilePaginationCacheKey.kt @@ -0,0 +1,27 @@ +package fr.ziedelth.caches + +import java.util.* + +open class ProfilePaginationCacheKey( + open val profile: UUID, + open val page: Int, + open val limit: Int, +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ProfilePaginationCacheKey) return false + + if (profile != other.profile) return false + if (page != other.page) return false + if (limit != other.limit) return false + + return true + } + + override fun hashCode(): Int { + var result = profile.hashCode() + result = 31 * result + page + result = 31 * result + limit + return result + } +} diff --git a/src/main/kotlin/fr/ziedelth/controllers/ProfileController.kt b/src/main/kotlin/fr/ziedelth/controllers/ProfileController.kt index 1f5a86b..b10ec4b 100644 --- a/src/main/kotlin/fr/ziedelth/controllers/ProfileController.kt +++ b/src/main/kotlin/fr/ziedelth/controllers/ProfileController.kt @@ -10,6 +10,7 @@ import fr.ziedelth.repositories.EpisodeRepository import fr.ziedelth.repositories.ProfileRepository import fr.ziedelth.services.EpisodeTypeService import fr.ziedelth.services.LangTypeService +import fr.ziedelth.services.ProfileService import fr.ziedelth.utils.Constant import fr.ziedelth.utils.routes.* import fr.ziedelth.utils.routes.method.Delete @@ -27,6 +28,9 @@ class ProfileController : AbstractController("/profile") { @Inject private lateinit var profileRepository: ProfileRepository + @Inject + private lateinit var profileService: ProfileService + @Inject private lateinit var episodeTypeService: EpisodeTypeService @@ -98,6 +102,7 @@ class ProfileController : AbstractController("/profile") { @Authenticated private fun addToWatchlist(@JWTUser jwtUser: UUID, @QueryParam anime: String?, @QueryParam episode: String?): Response { profileRepository.addToWatchlist(profileRepository.find(jwtUser)!!, anime, episode) ?: return Response(HttpStatusCode.BadRequest) + profileService.invalidateProfile(jwtUser) return Response.ok() } @@ -114,6 +119,7 @@ class ProfileController : AbstractController("/profile") { @Authenticated private fun deleteToWatchlist(@JWTUser jwtUser: UUID, @QueryParam anime: String?, @QueryParam episode: String?): Response { profileRepository.removeToWatchlist(profileRepository.find(jwtUser)!!, anime, episode) ?: return Response(HttpStatusCode.BadRequest) + profileService.invalidateProfile(jwtUser) return Response.ok() } @@ -149,21 +155,7 @@ class ProfileController : AbstractController("/profile") { limit: Int ): Response { val (episodeTypesUuid, langTypesUuid) = getEpisodeAndLangTypesUuid(episodeTypes, langTypes) - return Response.ok(profileRepository.getMissingAnimes(jwtUser, episodeTypesUuid, langTypesUuid, page, limit)) - } - - @Path("/watchlist/episodes/missing/page/{page}/limit/{limit}") - @Get - @Authenticated - private fun paginationWatchlistEpisodesMissing( - @JWTUser jwtUser: UUID, - @QueryParam episodeTypes: String?, - @QueryParam langTypes: String?, - page: Int, - limit: Int - ): Response { - val (episodeTypesUuid, langTypesUuid) = getEpisodeAndLangTypesUuid(episodeTypes, langTypes) - return Response.ok(profileRepository.getMissingEpisodes(jwtUser, episodeTypesUuid, langTypesUuid, page, limit)) + return Response.ok(profileService.getMissingAnimes(jwtUser, episodeTypesUuid, langTypesUuid, page, limit)) } /** @@ -178,7 +170,7 @@ class ProfileController : AbstractController("/profile") { @Get @Authenticated private fun getWatchlistAnimesByPageAndLimit(@JWTUser jwtUser: UUID, page: Int, limit: Int): Response { - return Response.ok(profileRepository.getWatchlistAnimes(jwtUser, page, limit)) + return Response.ok(profileService.getWatchlistAnimes(jwtUser, page, limit)) } /** @@ -193,7 +185,7 @@ class ProfileController : AbstractController("/profile") { @Get @Authenticated private fun getWatchlistEpisodesByPageAndLimit(@JWTUser jwtUser: UUID, page: Int, limit: Int): Response { - return Response.ok(profileRepository.getWatchlistEpisodes(jwtUser, page, limit)) + return Response.ok(profileService.getWatchlistEpisodes(jwtUser, page, limit)) } @Path @@ -201,6 +193,7 @@ class ProfileController : AbstractController("/profile") { @Authenticated private fun deleteProfile(@JWTUser jwtUser: UUID): Response { profileRepository.delete(profileRepository.find(jwtUser)!!) + profileService.invalidateProfile(jwtUser) return Response.ok() } } diff --git a/src/main/kotlin/fr/ziedelth/plugins/HTTP.kt b/src/main/kotlin/fr/ziedelth/plugins/HTTP.kt index 96510c7..f21576e 100644 --- a/src/main/kotlin/fr/ziedelth/plugins/HTTP.kt +++ b/src/main/kotlin/fr/ziedelth/plugins/HTTP.kt @@ -14,7 +14,7 @@ import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.plugins.cors.routing.* import io.ktor.server.response.* -fun Application.configureHTTP() { +fun Application.configureHTTP(isDebug: Boolean = false) { install(Compression) { gzip { priority = 1.0 @@ -59,15 +59,17 @@ fun Application.configureHTTP() { } } - install(SwaggerUI) { - swagger { - swaggerUrl = "swagger" - forwardRoot = true - } - info { - title = "API" - version = "latest" - description = "API for testing and demonstration purposes." + if (isDebug) { + install(SwaggerUI) { + swagger { + swaggerUrl = "swagger" + forwardRoot = true + } + info { + title = "API" + version = "latest" + description = "API for testing and demonstration purposes." + } } } } diff --git a/src/main/kotlin/fr/ziedelth/repositories/ProfileRepository.kt b/src/main/kotlin/fr/ziedelth/repositories/ProfileRepository.kt index fce1374..cc30eb5 100644 --- a/src/main/kotlin/fr/ziedelth/repositories/ProfileRepository.kt +++ b/src/main/kotlin/fr/ziedelth/repositories/ProfileRepository.kt @@ -163,48 +163,6 @@ class ProfileRepository : AbstractRepository() { ) } - fun getMissingEpisodes( - uuid: UUID, - episodeTypes: List, - langTypes: List, - page: Int, - limit: Int - ): List { - val (animes, episodes) = database.inReadOnlyTransaction { - val animes = it.createQuery( - "SELECT a.anime.uuid FROM Profile p JOIN p.animes a WHERE p.uuid = :uuid", - UUID::class.java - ).setParameter("uuid", uuid).resultList - - val episodes = it.createQuery( - "SELECT e.episode.uuid FROM Profile p JOIN p.episodes e WHERE p.uuid = :uuid", - UUID::class.java - ).setParameter("uuid", uuid).resultList - - animes to episodes - } - - val exclusionCondition = if (episodes.isEmpty()) "" else "e.uuid NOT IN :episodes AND" - - return super.getByPage( - Episode::class.java, - page, - limit, - """ - FROM Episode e - WHERE e.anime.uuid IN :animes - AND $exclusionCondition - e.episodeType.uuid IN :episodeTypes - AND e.langType.uuid IN :langTypes - ORDER BY releaseDate DESC, anime.name, season DESC, number DESC, episodeType.name, langType.name - """.trimIndent(), - "animes" to animes, - if (episodes.isEmpty()) null else "episodes" to episodes, - "episodeTypes" to episodeTypes, - "langTypes" to langTypes - ) - } - /** * Retrieves a list of Anime objects from the watchlist of a profile identified by the given UUID. * @@ -238,9 +196,9 @@ class ProfileRepository : AbstractRepository() { * @param limit The maximum number of episodes to retrieve per page. * @return A list of Anime objects representing the episodes in the watchlist. */ - fun getWatchlistEpisodes(uuid: UUID, page: Int, limit: Int): List { + fun getWatchlistEpisodes(uuid: UUID, page: Int, limit: Int): List { return super.getByPage( - Anime::class.java, + Episode::class.java, page, limit, """ diff --git a/src/main/kotlin/fr/ziedelth/services/ProfileService.kt b/src/main/kotlin/fr/ziedelth/services/ProfileService.kt new file mode 100644 index 0000000..2dc749b --- /dev/null +++ b/src/main/kotlin/fr/ziedelth/services/ProfileService.kt @@ -0,0 +1,64 @@ +package fr.ziedelth.services + +import com.google.common.cache.CacheBuilder +import com.google.common.cache.CacheLoader +import com.google.inject.Inject +import fr.ziedelth.caches.ProfileEpisodeTypesLangTypesPaginationCacheKey +import fr.ziedelth.caches.ProfilePaginationCacheKey +import fr.ziedelth.dtos.MissingAnimeDto +import fr.ziedelth.entities.Anime +import fr.ziedelth.entities.Episode +import fr.ziedelth.repositories.ProfileRepository +import fr.ziedelth.utils.Logger +import java.util.* + +class ProfileService : AbstractService() { + @Inject + private lateinit var repository: ProfileRepository + + private val missingAnimesLoadingCache = CacheBuilder.newBuilder() + .build(object : CacheLoader>() { + override fun load(key: ProfileEpisodeTypesLangTypesPaginationCacheKey): List { + Logger.config("Updating profile missing animes cache") + return repository.getMissingAnimes(key.profile, key.episodeTypes, key.langTypes, key.page, key.limit) + } + }) + + private val watchlistAnimesLoadingCache = CacheBuilder.newBuilder() + .build(object : CacheLoader>() { + override fun load(key: ProfilePaginationCacheKey): List { + Logger.config("Updating profile animes cache") + return repository.getWatchlistAnimes(key.profile, key.page, key.limit) + } + }) + + private val watchlistEpisodesLoadingCache = CacheBuilder.newBuilder() + .build(object : CacheLoader>() { + override fun load(key: ProfilePaginationCacheKey): List { + Logger.config("Updating profile episodes cache") + return repository.getWatchlistEpisodes(key.profile, key.page, key.limit) + } + }) + + fun invalidateProfile(profile: UUID) { + Logger.warning("Invalidate all profile cache for profile $profile") + + val missingAnimesKeys = missingAnimesLoadingCache.asMap().keys.filter { it.profile == profile } + missingAnimesLoadingCache.invalidateAll(missingAnimesKeys) + + val watchlistAnimesKeys = watchlistAnimesLoadingCache.asMap().keys.filter { it.profile == profile } + watchlistAnimesLoadingCache.invalidateAll(watchlistAnimesKeys) + + val watchlistEpisodesKeys = watchlistEpisodesLoadingCache.asMap().keys.filter { it.profile == profile } + watchlistEpisodesLoadingCache.invalidateAll(watchlistEpisodesKeys) + } + + fun getMissingAnimes(profile: UUID, episodeTypes: List, langTypes: List, page: Int, limit: Int): List = + missingAnimesLoadingCache.getUnchecked(ProfileEpisodeTypesLangTypesPaginationCacheKey(profile, episodeTypes, langTypes, page, limit)) + + fun getWatchlistAnimes(profile: UUID, page: Int, limit: Int): List = + watchlistAnimesLoadingCache.getUnchecked(ProfilePaginationCacheKey(profile, page, limit)) + + fun getWatchlistEpisodes(profile: UUID, page: Int, limit: Int): List = + watchlistEpisodesLoadingCache.getUnchecked(ProfilePaginationCacheKey(profile, page, limit)) +} \ No newline at end of file