Implement proper rate limiter for IGDB API calls

This commit is contained in:
grimsi
2025-07-11 20:13:03 +02:00
parent c3a62fa8bb
commit da47781b16
3 changed files with 41 additions and 20 deletions
+13
View File
@@ -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 -1
View File
@@ -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