refactor: add retry mechanism to Feign clients for improved resilience

This commit is contained in:
Rodrigo Verdiani 2026-03-26 14:02:23 -03:00
parent f0a151159e
commit 348911c4ef
5 changed files with 64 additions and 3 deletions

View File

@ -3,15 +3,28 @@ package com.magamochi.catalog.client;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List; import java.util.List;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "aniList", url = "https://graphql.anilist.co") @FeignClient(name = "aniList", url = "https://graphql.anilist.co")
public interface AniListClient { public interface AniListClient {
@PostMapping @PostMapping
@Retryable(
maxRetries = 5,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaResponse getManga(@RequestBody GraphQLRequest request); MangaResponse getManga(@RequestBody GraphQLRequest request);
@PostMapping @PostMapping
@Retryable(
maxRetries = 5,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaSearchResponse searchManga(@RequestBody SearchGraphQLRequest request); MangaSearchResponse searchManga(@RequestBody SearchGraphQLRequest request);
record GraphQLRequest(String query, Variables variables) { record GraphQLRequest(String query, Variables variables) {

View File

@ -3,6 +3,7 @@ package com.magamochi.catalog.client;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.List; import java.util.List;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -10,9 +11,21 @@ import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "jikan", url = "https://api.jikan.moe/v4/manga") @FeignClient(name = "jikan", url = "https://api.jikan.moe/v4/manga")
public interface JikanClient { public interface JikanClient {
@GetMapping @GetMapping
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
SearchResponse mangaSearch(@RequestParam String q); SearchResponse mangaSearch(@RequestParam String q);
@GetMapping("/{id}") @GetMapping("/{id}")
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaResponse getMangaById(@PathVariable Long id); MangaResponse getMangaById(@PathVariable Long id);
record SearchResponse(List<MangaData> data) { record SearchResponse(List<MangaData> data) {

View File

@ -4,18 +4,37 @@ import com.magamochi.model.dto.MangaDexMangaDTO;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@FeignClient(name = "mangaDex", url = "https://api.mangadex.org") @FeignClient(name = "mangaDex", url = "https://api.mangadex.org")
public interface MangaDexClient { public interface MangaDexClient {
@GetMapping("/manga/{id}") @GetMapping("/manga/{id}")
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaDexMangaDTO getManga(@PathVariable UUID id); MangaDexMangaDTO getManga(@PathVariable UUID id);
@GetMapping("/manga/{id}/feed") @GetMapping("/manga/{id}/feed")
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaDexMangaFeedDTO getMangaFeed( MangaDexMangaFeedDTO getMangaFeed(
@PathVariable UUID id, @RequestParam("contentRating[]") List<String> contentRating); @PathVariable UUID id, @RequestParam("contentRating[]") List<String> contentRating);
@GetMapping("/manga/{id}/feed") @GetMapping("/manga/{id}/feed")
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaDexMangaFeedDTO getMangaFeed( MangaDexMangaFeedDTO getMangaFeed(
@PathVariable UUID id, @PathVariable UUID id,
@RequestParam int limit, @RequestParam int limit,
@ -23,6 +42,12 @@ public interface MangaDexClient {
@RequestParam("contentRating[]") List<String> contentRating); @RequestParam("contentRating[]") List<String> contentRating);
@GetMapping("/at-home/server/{chapterId}") @GetMapping("/at-home/server/{chapterId}")
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
MangaChapterDataDTO getMangaChapter(@PathVariable UUID chapterId); MangaChapterDataDTO getMangaChapter(@PathVariable UUID chapterId);
record MangaDexMangaFeedDTO( record MangaDexMangaFeedDTO(

View File

@ -2,11 +2,18 @@ package com.magamochi.client;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@FeignClient(name = "ntfy", url = "${ntfy.endpoint}") @FeignClient(name = "ntfy", url = "${ntfy.endpoint}")
public interface NtfyClient { public interface NtfyClient {
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@Retryable(
maxRetries = 3,
delay = 1000,
multiplier = 2,
maxDelay = 5000,
includes = Exception.class)
void notify(@RequestBody Request dto); void notify(@RequestBody Request dto);
record Request(String topic, String message, String title) {} record Request(String topic, String message, String title) {}

View File

@ -1,5 +1,7 @@
package com.magamochi.image.queue.consumer; package com.magamochi.image.queue.consumer;
import static java.util.Objects.nonNull;
import com.magamochi.common.queue.command.ImageFetchCommand; import com.magamochi.common.queue.command.ImageFetchCommand;
import com.magamochi.common.queue.command.ImageUpdateCommand; import com.magamochi.common.queue.command.ImageUpdateCommand;
import com.magamochi.image.queue.producer.ImageUpdateProducer; import com.magamochi.image.queue.producer.ImageUpdateProducer;
@ -21,8 +23,9 @@ public class ImageFetchConsumer {
log.info("Received image fetch command: {}", command); log.info("Received image fetch command: {}", command);
var imageId = imageFetchService.fetchImage(command.url(), command.contentType()); var imageId = imageFetchService.fetchImage(command.url(), command.contentType());
if (nonNull(imageId)) {
imageUpdateProducer.publishImageUpdateCommand( imageUpdateProducer.publishImageUpdateCommand(
new ImageUpdateCommand(command.entityId(), imageId), command.contentType()); new ImageUpdateCommand(command.entityId(), imageId), command.contentType());
} }
} }
}