Conversation
|
@avvvis have a look at conflics and seek review pls |
There was a problem hiding this comment.
Pull request overview
This PR replaces the manual “try/catch + loop” retry logic in the production AI menu parsing service with Spring Retry annotations, and enables retry support at the application level.
Changes:
- Added
@EnableRetryto turn on Spring Retry support. - Annotated
ProdAIService.parseMenuFromImage(...)with@Retryableand added an@Recoverfallback. - Adjusted the prod AI service’s Spring profile activation (from
!devtoprod).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
src/main/java/com/backend/services/ProdAIService.java |
Replaces manual retry loop with Spring Retry (@Retryable / @Recover), adds retry-attempt logging, and changes profile activation. |
src/main/java/com/backend/BackendApplication.java |
Enables Spring Retry globally via @EnableRetry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import org.springframework.retry.annotation.EnableRetry; | ||
| import org.springframework.scheduling.annotation.EnableScheduling; | ||
|
|
||
| @SpringBootApplication | ||
| @EnableScheduling | ||
| @EnableRetry |
| @Service | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| @Profile("!dev") | ||
| @Profile({"prod"}) | ||
| public class ProdAIService implements MenuAIService { |
There was a problem hiding this comment.
Will be resolved by @avvvis on conflict resolution, please look to current version in main
| public List<Dish> parseMenuFromImage(byte[] imageBytes) { | ||
| int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1; | ||
| log.info("Sending menu to AI - attempt {}/5", attempt); |
| @Retryable( | ||
| retryFor = Exception.class, | ||
| maxAttempts = 5, | ||
| backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s | ||
| ) | ||
| @Override |
| @Retryable( | ||
| retryFor = Exception.class, | ||
| maxAttempts = 5, | ||
| backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s |
There was a problem hiding this comment.
@copilot create a PR which implements your suggested "only transient client exceptions from the AI call" and consider whether the trade-offs it introduces, in terms of say, complexity, do not outweigh the benefits.
| // Helper method to convert DishDTO to Dish entity | ||
| @Recover | ||
| public List<Dish> recover(Exception e, byte[] imageBytes) { | ||
| log.error("All GPT retry attempts exhausted: {}", e.getMessage()); |
| @Retryable( | ||
| retryFor = Exception.class, | ||
| maxAttempts = 5, | ||
| backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s | ||
| ) | ||
| @Override | ||
| public List<Dish> parseMenuFromImage(byte[] imageBytes) { | ||
| int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1; | ||
| log.info("Sending menu to AI - attempt {}/5", attempt); |
There was a problem hiding this comment.
Add dependencies, decide if you want to add tests, fix @retryable wrapping DB persistence code, fix conflicts, ask @kingazm or @haniazipser for final review.
| @Service | ||
| @RequiredArgsConstructor | ||
| @Slf4j | ||
| @Profile("!dev") | ||
| @Profile({"prod"}) | ||
| public class ProdAIService implements MenuAIService { |
There was a problem hiding this comment.
Will be resolved by @avvvis on conflict resolution, please look to current version in main
| @Retryable( | ||
| retryFor = Exception.class, | ||
| maxAttempts = 5, | ||
| backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s |
There was a problem hiding this comment.
@copilot create a PR which implements your suggested "only transient client exceptions from the AI call" and consider whether the trade-offs it introduces, in terms of say, complexity, do not outweigh the benefits.
| @Retryable( | ||
| retryFor = Exception.class, | ||
| maxAttempts = 5, | ||
| backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s | ||
| ) | ||
| @Override |
| public List<Dish> parseMenuFromImage(byte[] imageBytes) { | ||
| int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1; | ||
| log.info("Sending menu to AI - attempt {}/5", attempt); |
| @Retryable( | ||
| retryFor = Exception.class, | ||
| maxAttempts = 5, | ||
| backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s | ||
| ) | ||
| @Override | ||
| public List<Dish> parseMenuFromImage(byte[] imageBytes) { | ||
| int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1; | ||
| log.info("Sending menu to AI - attempt {}/5", attempt); |
| // Helper method to convert DishDTO to Dish entity | ||
| @Recover | ||
| public List<Dish> recover(Exception e, byte[] imageBytes) { | ||
| log.error("All GPT retry attempts exhausted: {}", e.getMessage()); |
| import org.springframework.retry.annotation.EnableRetry; | ||
| import org.springframework.scheduling.annotation.EnableScheduling; | ||
|
|
||
| @SpringBootApplication | ||
| @EnableScheduling | ||
| @EnableRetry |
Addresses Copilot review on ProdAIService and fixes two bugs surfaced during testing. Changes pom.xml — Added spring-retry and spring-boot-starter-aop to give @EnableRetry the classpath it needs. ProdAIService.java retryFor narrowed to transient errors only (ResourceAccessException, HttpServerErrorException); deterministic failures fail fast via a new non-retryable MenuExtractionException. Bounded backoff with maxDelay = 10000. ChatClient built once in the constructor; null-safe RetrySynchronizationManager.getContext(); MIME type detected from image bytes; defensive parseCategory (no crash on unexpected GPT output); saveAll instead of per-dish loop; @recover preserves the stack trace and rethrows. Bugs fixed during testing Infinite retry on unreadable images — empty AI responses were being retried 5× per call. Now thrown as non-retryable MenuExtractionException. RabbitMQ redelivery loop — an inner @transactional on parseMenuFromImage was marking the listener's outer transaction rollback-only on failure, causing the listener's commit to fail → message NACKed → requeued forever. Removed; the listener owns the single transaction boundary. Reviewer comments CommentHow resolvedMissing Spring Retry / AOP depsAdded to pom.xmlretryFor = Exception.class too broadNarrowed to transient onlyRetryable over GPT + DB save → duplicatesSave-time failures no longer trigger retrygetContext() can be nullNull-checked@Recover drops stack traceLogs full throwable, rethrows@Profile changeReverted to !devTests for retry behaviorDeferred
|
Closing as this PR is outdated. |
📄 Pull Request Description
Please fill out each section below.
The goal is that reviewers can understand everything without reading the code.
🧩 What was changed?
swapped primitive error handling into a retry mechanism from Spring framework
💡 Why was it changed?
previous mechanism was primitive and working poorly
⚙️ How was it implemented?
deleted try/except and a retry loop, emplaced it with a @Retry
shouldnt break anything. Perhaps could add this retry into more places in the menu processing