mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 08:15:48 +00:00
Implement proper rate limiter for IGDB API calls
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
val resilience4jVersion = "2.2.0"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
}
|
}
|
||||||
@@ -8,6 +10,17 @@ dependencies {
|
|||||||
// IGDB API client
|
// IGDB API client
|
||||||
implementation("io.github.husnjak:igdb-api-jvm:1.3.1")
|
implementation("io.github.husnjak:igdb-api-jvm:1.3.1")
|
||||||
|
|
||||||
|
// Resilience4j for rate limiting
|
||||||
|
implementation("io.github.resilience4j:resilience4j-ratelimiter:${resilience4jVersion}") {
|
||||||
|
exclude(group = "org.slf4j")
|
||||||
|
}
|
||||||
|
implementation("io.github.resilience4j:resilience4j-bulkhead:${resilience4jVersion}") {
|
||||||
|
exclude(group = "org.slf4j")
|
||||||
|
}
|
||||||
|
implementation("io.github.resilience4j:resilience4j-all:${resilience4jVersion}") {
|
||||||
|
exclude(group = "org.slf4j")
|
||||||
|
}
|
||||||
|
|
||||||
// Fuzzy string matching
|
// Fuzzy string matching
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
package org.gameyfin.plugins.metadata.igdb
|
package org.gameyfin.plugins.metadata.igdb
|
||||||
|
|
||||||
import com.api.igdb.apicalypse.APICalypse
|
import com.api.igdb.apicalypse.APICalypse
|
||||||
import com.api.igdb.exceptions.RequestException
|
|
||||||
import com.api.igdb.request.IGDBWrapper
|
import com.api.igdb.request.IGDBWrapper
|
||||||
import com.api.igdb.request.TwitchAuthenticator
|
import com.api.igdb.request.TwitchAuthenticator
|
||||||
import com.api.igdb.request.games
|
import com.api.igdb.request.games
|
||||||
|
import io.github.resilience4j.bulkhead.Bulkhead
|
||||||
|
import io.github.resilience4j.bulkhead.BulkheadConfig
|
||||||
|
import io.github.resilience4j.decorators.Decorators
|
||||||
|
import io.github.resilience4j.ratelimiter.RateLimiter
|
||||||
|
import io.github.resilience4j.ratelimiter.RateLimiterConfig
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
import org.gameyfin.pluginapi.core.config.ConfigMetadata
|
import org.gameyfin.pluginapi.core.config.ConfigMetadata
|
||||||
import org.gameyfin.pluginapi.core.config.PluginConfigError
|
import org.gameyfin.pluginapi.core.config.PluginConfigError
|
||||||
@@ -15,10 +19,9 @@ import org.gameyfin.pluginapi.gamemetadata.GameMetadata
|
|||||||
import org.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
import org.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||||
import org.pf4j.Extension
|
import org.pf4j.Extension
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import proto.Game
|
import proto.Game
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
||||||
|
|
||||||
@@ -89,7 +92,21 @@ class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
|||||||
class IgdbMetadataProvider : GameMetadataProvider {
|
class IgdbMetadataProvider : GameMetadataProvider {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val log = LoggerFactory.getLogger(this::class.java)
|
private val rateLimiter: RateLimiter = RateLimiter.of(
|
||||||
|
"igdb-api",
|
||||||
|
RateLimiterConfig.custom()
|
||||||
|
.limitForPeriod(4)
|
||||||
|
.limitRefreshPeriod(Duration.ofSeconds(1))
|
||||||
|
.timeoutDuration(Duration.ofMinutes(10))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
private val bulkhead: Bulkhead = Bulkhead.of(
|
||||||
|
"igdb-api",
|
||||||
|
BulkheadConfig.custom()
|
||||||
|
.maxConcurrentCalls(8)
|
||||||
|
.maxWaitDuration(Duration.ofMinutes(10)) // Wait up to 10s for a slot
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
private val QUERY_FIELDS = listOf(
|
private val QUERY_FIELDS = listOf(
|
||||||
"slug",
|
"slug",
|
||||||
@@ -168,21 +185,12 @@ class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun queryIgdbGames(query: APICalypse): List<Game> {
|
private fun queryIgdbGames(query: APICalypse): List<Game> {
|
||||||
return try {
|
val supplier = { IGDBWrapper.games(query) }
|
||||||
IGDBWrapper.games(query)
|
val decorated = Decorators.ofSupplier(supplier)
|
||||||
} catch (e: RequestException) {
|
.withBulkhead(bulkhead)
|
||||||
// FIXME: Handle rate limit errors with exponential backoff
|
.withRateLimiter(rateLimiter)
|
||||||
if (e.statusCode == 429) {
|
.decorate()
|
||||||
val randomInterval = (1..5).random().toLong()
|
return decorated.get()
|
||||||
log.warn("IGDB rate limit exceeded, retrying in $randomInterval seconds...")
|
|
||||||
TimeUnit.SECONDS.sleep(randomInterval)
|
|
||||||
|
|
||||||
return queryIgdbGames(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.error("Request to IGDB API failed with HTTP ${e.statusCode}")
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toGameMetadata(game: Game): GameMetadata {
|
private fun toGameMetadata(game: Game): GameMetadata {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Plugin-Version: 1.0.0.beta2
|
Plugin-Version: 1.0.0.beta3
|
||||||
Plugin-Class: org.gameyfin.plugins.metadata.igdb.IgdbPlugin
|
Plugin-Class: org.gameyfin.plugins.metadata.igdb.IgdbPlugin
|
||||||
Plugin-Id: org.gameyfin.plugins.metadata.igdb
|
Plugin-Id: org.gameyfin.plugins.metadata.igdb
|
||||||
Plugin-Name: IGDB Metadata
|
Plugin-Name: IGDB Metadata
|
||||||
|
|||||||
Reference in New Issue
Block a user