Implement rate limiter for IGDB API

Rename BlacklistEntry to UnmappableFile
This commit is contained in:
Simon Grimme
2022-07-16 02:07:46 +02:00
parent 183f58c64c
commit 132d4d3694
9 changed files with 59 additions and 16 deletions
+13
View File
@@ -36,6 +36,19 @@
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
</dependency> </dependency>
<!-- Webclient Rate limiter -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-reactor</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>1.7.1</version>
</dependency>
<!-- Persistence --> <!-- Persistence -->
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
@@ -1,5 +1,7 @@
package de.grimsi.gameyfin.config; package de.grimsi.gameyfin.config;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; import org.springframework.boot.web.reactive.function.client.WebClientCustomizer;
@@ -13,10 +15,19 @@ import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient;
import reactor.netty.transport.logging.AdvancedByteBufFormat; import reactor.netty.transport.logging.AdvancedByteBufFormat;
import java.time.Duration;
@Slf4j @Slf4j
@Configuration @Configuration
public class WebClientConfig implements WebClientCustomizer { public class WebClientConfig implements WebClientCustomizer {
public static final RateLimiter IGDB_RATE_LIMITER = RateLimiter.of("igdb-rate-limiter",
RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofSeconds(1))
.limitForPeriod(4)
.timeoutDuration(Duration.ofMinutes(1)) // max wait time for a request, if reached then error
.build());
@Override @Override
public void customize(WebClient.Builder webClientBuilder) { public void customize(WebClient.Builder webClientBuilder) {
HttpClient httpClient = HttpClient.create() HttpClient httpClient = HttpClient.create()
@@ -3,9 +3,7 @@ package de.grimsi.gameyfin.entities;
import lombok.*; import lombok.*;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import javax.persistence.Entity; import javax.persistence.*;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Objects; import java.util.Objects;
@Entity @Entity
@@ -14,16 +12,23 @@ import java.util.Objects;
@Setter @Setter
@ToString @ToString
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor public class UnmappableFile {
public class BlacklistEntry {
public UnmappableFile(String path) {
this.path = path;
}
@Id @Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
private String path; private String path;
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false; if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
BlacklistEntry that = (BlacklistEntry) o; UnmappableFile that = (UnmappableFile) o;
return path != null && Objects.equals(path, that.path); return path != null && Objects.equals(path, that.path);
} }
@@ -3,6 +3,7 @@ package de.grimsi.gameyfin.igdb;
import com.igdb.proto.Igdb; import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.config.WebClientConfig; import de.grimsi.gameyfin.config.WebClientConfig;
import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto; import de.grimsi.gameyfin.igdb.dto.TwitchOAuthTokenDto;
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -72,6 +73,7 @@ public class IgdbWrapper {
.bodyValue("fields *; where id = %d & category = %d; limit 1;".formatted(id, MAIN_GAME_CATEGORY_VALUE)) .bodyValue("fields *; where id = %d & category = %d; limit 1;".formatted(id, MAIN_GAME_CATEGORY_VALUE))
.retrieve() .retrieve()
.bodyToMono(Igdb.GameResult.class) .bodyToMono(Igdb.GameResult.class)
.transformDeferred(RateLimiterOperator.of(WebClientConfig.IGDB_RATE_LIMITER))
.block(); .block();
if (gameResult == null) return Optional.empty(); if (gameResult == null) return Optional.empty();
@@ -85,6 +87,7 @@ public class IgdbWrapper {
.bodyValue("fields *; search \"%s\"; where platforms = (%s) & category = %d;".formatted(searchTerm, preferredPlatforms, MAIN_GAME_CATEGORY_VALUE)) .bodyValue("fields *; search \"%s\"; where platforms = (%s) & category = %d;".formatted(searchTerm, preferredPlatforms, MAIN_GAME_CATEGORY_VALUE))
.retrieve() .retrieve()
.bodyToMono(Igdb.GameResult.class) .bodyToMono(Igdb.GameResult.class)
.transformDeferred(RateLimiterOperator.of(WebClientConfig.IGDB_RATE_LIMITER))
.block(); .block();
if (gameResult == null) { if (gameResult == null) {
@@ -1,9 +1,9 @@
package de.grimsi.gameyfin.repositories; package de.grimsi.gameyfin.repositories;
import de.grimsi.gameyfin.entities.BlacklistEntry; import de.grimsi.gameyfin.entities.UnmappableFile;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
public interface BlacklistRepository extends JpaRepository<BlacklistEntry, String> { public interface UnmappableFileRepository extends JpaRepository<UnmappableFile, String> {
boolean existsByPath(String path); boolean existsByPath(String path);
} }
@@ -3,6 +3,7 @@ package de.grimsi.gameyfin.rest;
import com.igdb.proto.Igdb; import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.dto.GameDto; import de.grimsi.gameyfin.dto.GameDto;
import de.grimsi.gameyfin.entities.DetectedGame; import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.entities.UnmappableFile;
import de.grimsi.gameyfin.igdb.IgdbWrapper; import de.grimsi.gameyfin.igdb.IgdbWrapper;
import de.grimsi.gameyfin.service.FilesystemService; import de.grimsi.gameyfin.service.FilesystemService;
import de.grimsi.gameyfin.service.GameService; import de.grimsi.gameyfin.service.GameService;
@@ -68,4 +69,9 @@ public class GameyfinDevController {
filesystemService.scanGameLibrary(); filesystemService.scanGameLibrary();
} }
@GetMapping(value = "/dev/unmappedFiles", produces = MediaType.APPLICATION_JSON_VALUE)
public List<UnmappableFile> getUnmappedFiles() {
return gameService.getAllUnmappedFiles();
}
} }
@@ -1,11 +1,11 @@
package de.grimsi.gameyfin.service; package de.grimsi.gameyfin.service;
import com.igdb.proto.Igdb; import com.igdb.proto.Igdb;
import de.grimsi.gameyfin.entities.BlacklistEntry; import de.grimsi.gameyfin.entities.UnmappableFile;
import de.grimsi.gameyfin.entities.DetectedGame; import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.igdb.IgdbWrapper; import de.grimsi.gameyfin.igdb.IgdbWrapper;
import de.grimsi.gameyfin.mapper.GameMapper; import de.grimsi.gameyfin.mapper.GameMapper;
import de.grimsi.gameyfin.repositories.BlacklistRepository; import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
import de.grimsi.gameyfin.repositories.DetectedGameRepository; import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
@@ -38,7 +38,7 @@ public class FilesystemService {
private DetectedGameRepository detectedGameRepository; private DetectedGameRepository detectedGameRepository;
@Autowired @Autowired
private BlacklistRepository blacklistRepository; private UnmappableFileRepository unmappableFileRepository;
public List<Path> getGameFiles() { public List<Path> getGameFiles() {
@@ -68,7 +68,7 @@ public class FilesystemService {
// Filter out the games we already know and the ones we already tried to map to a game without success // Filter out the games we already know and the ones we already tried to map to a game without success
gameFiles = gameFiles.stream() gameFiles = gameFiles.stream()
.filter(g -> !detectedGameRepository.existsByPath(g.toString())) .filter(g -> !detectedGameRepository.existsByPath(g.toString()))
.filter(g -> !blacklistRepository.existsByPath(g.toString())) .filter(g -> !unmappableFileRepository.existsByPath(g.toString()))
.peek(p -> log.info("Found new potential game: {}", p)) .peek(p -> log.info("Found new potential game: {}", p))
.toList(); .toList();
@@ -78,7 +78,7 @@ public class FilesystemService {
.map(p -> { .map(p -> {
Optional<Igdb.Game> optionalGame = igdbWrapper.searchForGameByTitle(getFilename(p)); Optional<Igdb.Game> optionalGame = igdbWrapper.searchForGameByTitle(getFilename(p));
return optionalGame.map(game -> Map.entry(p, game)).or(() -> { return optionalGame.map(game -> Map.entry(p, game)).or(() -> {
blacklistRepository.save(new BlacklistEntry(p.toString())); unmappableFileRepository.save(new UnmappableFile(p.toString()));
newBlacklistCounter.getAndIncrement(); newBlacklistCounter.getAndIncrement();
log.info("Added path '{}' to blacklist", p); log.info("Added path '{}' to blacklist", p);
return Optional.empty(); return Optional.empty();
@@ -1,7 +1,8 @@
package de.grimsi.gameyfin.service; package de.grimsi.gameyfin.service;
import de.grimsi.gameyfin.entities.DetectedGame; import de.grimsi.gameyfin.entities.DetectedGame;
import de.grimsi.gameyfin.repositories.BlacklistRepository; import de.grimsi.gameyfin.entities.UnmappableFile;
import de.grimsi.gameyfin.repositories.UnmappableFileRepository;
import de.grimsi.gameyfin.repositories.DetectedGameRepository; import de.grimsi.gameyfin.repositories.DetectedGameRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -15,9 +16,13 @@ public class GameService {
private DetectedGameRepository detectedGameRepository; private DetectedGameRepository detectedGameRepository;
@Autowired @Autowired
private BlacklistRepository blacklistRepository; private UnmappableFileRepository unmappableFileRepository;
public List<DetectedGame> getAllDetectedGames() { public List<DetectedGame> getAllDetectedGames() {
return detectedGameRepository.findAll(); return detectedGameRepository.findAll();
} }
public List<UnmappableFile> getAllUnmappedFiles() {
return unmappableFileRepository.findAll();
}
} }
+1 -1
View File
@@ -1,5 +1,5 @@
gameyfin: gameyfin:
root: D:\Games root: \\NAS-Simon\Öffentlich\Spiele
cache: C:\Projects\privat\gameyfin-library\.gameyfin cache: C:\Projects\privat\gameyfin-library\.gameyfin
db: C:\Projects\privat\gameyfin-library\.gameyfin db: C:\Projects\privat\gameyfin-library\.gameyfin
igdb: igdb: