From 348911c4ef4306efceffb270c14940e1a2c78efe Mon Sep 17 00:00:00 2001 From: Rodrigo Verdiani Date: Thu, 26 Mar 2026 14:02:23 -0300 Subject: [PATCH] refactor: add retry mechanism to Feign clients for improved resilience --- .../catalog/client/AniListClient.java | 13 ++++++++++ .../magamochi/catalog/client/JikanClient.java | 13 ++++++++++ .../com/magamochi/client/MangaDexClient.java | 25 +++++++++++++++++++ .../java/com/magamochi/client/NtfyClient.java | 7 ++++++ .../queue/consumer/ImageFetchConsumer.java | 9 ++++--- 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/magamochi/catalog/client/AniListClient.java b/src/main/java/com/magamochi/catalog/client/AniListClient.java index c2404ac..485b72f 100644 --- a/src/main/java/com/magamochi/catalog/client/AniListClient.java +++ b/src/main/java/com/magamochi/catalog/client/AniListClient.java @@ -3,15 +3,28 @@ package com.magamochi.catalog.client; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; 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.RequestBody; @FeignClient(name = "aniList", url = "https://graphql.anilist.co") public interface AniListClient { @PostMapping + @Retryable( + maxRetries = 5, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaResponse getManga(@RequestBody GraphQLRequest request); @PostMapping + @Retryable( + maxRetries = 5, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaSearchResponse searchManga(@RequestBody SearchGraphQLRequest request); record GraphQLRequest(String query, Variables variables) { diff --git a/src/main/java/com/magamochi/catalog/client/JikanClient.java b/src/main/java/com/magamochi/catalog/client/JikanClient.java index 9c27e53..2873770 100644 --- a/src/main/java/com/magamochi/catalog/client/JikanClient.java +++ b/src/main/java/com/magamochi/catalog/client/JikanClient.java @@ -3,6 +3,7 @@ package com.magamochi.catalog.client; import java.time.OffsetDateTime; import java.util.List; 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.PathVariable; 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") public interface JikanClient { @GetMapping + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) SearchResponse mangaSearch(@RequestParam String q); @GetMapping("/{id}") + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaResponse getMangaById(@PathVariable Long id); record SearchResponse(List data) { diff --git a/src/main/java/com/magamochi/client/MangaDexClient.java b/src/main/java/com/magamochi/client/MangaDexClient.java index 57ca8c5..a20791a 100644 --- a/src/main/java/com/magamochi/client/MangaDexClient.java +++ b/src/main/java/com/magamochi/client/MangaDexClient.java @@ -4,18 +4,37 @@ import com.magamochi.model.dto.MangaDexMangaDTO; import java.util.List; import java.util.UUID; import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.resilience.annotation.Retryable; import org.springframework.web.bind.annotation.*; @FeignClient(name = "mangaDex", url = "https://api.mangadex.org") public interface MangaDexClient { @GetMapping("/manga/{id}") + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaDexMangaDTO getManga(@PathVariable UUID id); @GetMapping("/manga/{id}/feed") + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaDexMangaFeedDTO getMangaFeed( @PathVariable UUID id, @RequestParam("contentRating[]") List contentRating); @GetMapping("/manga/{id}/feed") + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaDexMangaFeedDTO getMangaFeed( @PathVariable UUID id, @RequestParam int limit, @@ -23,6 +42,12 @@ public interface MangaDexClient { @RequestParam("contentRating[]") List contentRating); @GetMapping("/at-home/server/{chapterId}") + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) MangaChapterDataDTO getMangaChapter(@PathVariable UUID chapterId); record MangaDexMangaFeedDTO( diff --git a/src/main/java/com/magamochi/client/NtfyClient.java b/src/main/java/com/magamochi/client/NtfyClient.java index af6618b..8be0286 100644 --- a/src/main/java/com/magamochi/client/NtfyClient.java +++ b/src/main/java/com/magamochi/client/NtfyClient.java @@ -2,11 +2,18 @@ package com.magamochi.client; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; +import org.springframework.resilience.annotation.Retryable; import org.springframework.web.bind.annotation.*; @FeignClient(name = "ntfy", url = "${ntfy.endpoint}") public interface NtfyClient { @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) + @Retryable( + maxRetries = 3, + delay = 1000, + multiplier = 2, + maxDelay = 5000, + includes = Exception.class) void notify(@RequestBody Request dto); record Request(String topic, String message, String title) {} diff --git a/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java b/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java index 7a17062..1ea2b3f 100644 --- a/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java +++ b/src/main/java/com/magamochi/image/queue/consumer/ImageFetchConsumer.java @@ -1,5 +1,7 @@ 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.ImageUpdateCommand; import com.magamochi.image.queue.producer.ImageUpdateProducer; @@ -21,8 +23,9 @@ public class ImageFetchConsumer { log.info("Received image fetch command: {}", command); var imageId = imageFetchService.fetchImage(command.url(), command.contentType()); - - imageUpdateProducer.publishImageUpdateCommand( - new ImageUpdateCommand(command.entityId(), imageId), command.contentType()); + if (nonNull(imageId)) { + imageUpdateProducer.publishImageUpdateCommand( + new ImageUpdateCommand(command.entityId(), imageId), command.contentType()); + } } }