Skip to content

implemented working error handling and retry system into AIservice using Spring Retry - #79

Closed
avvvis wants to merge 2 commits into
mainfrom
AIretry
Closed

implemented working error handling and retry system into AIservice using Spring Retry#79
avvvis wants to merge 2 commits into
mainfrom
AIretry

Conversation

@avvvis

@avvvis avvvis commented Apr 12, 2026

Copy link
Copy Markdown
Collaborator

📄 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


⚠️ Side Effects or Risks

shouldnt break anything. Perhaps could add this retry into more places in the menu processing


@kingazm

kingazm commented May 3, 2026

Copy link
Copy Markdown
Collaborator

@avvvis have a look at conflics and seek review pls

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @EnableRetry to turn on Spring Retry support.
  • Annotated ProdAIService.parseMenuFromImage(...) with @Retryable and added an @Recover fallback.
  • Adjusted the prod AI service’s Spring profile activation (from !dev to prod).

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.

Comment on lines +5 to +10
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableRetry

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@avvvis please address

Comment on lines 26 to 30
@Service
@RequiredArgsConstructor
@Slf4j
@Profile("!dev")
@Profile({"prod"})
public class ProdAIService implements MenuAIService {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be resolved by @avvvis on conflict resolution, please look to current version in main

Comment on lines +41 to +43
public List<Dish> parseMenuFromImage(byte[] imageBytes) {
int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1;
log.info("Sending menu to AI - attempt {}/5", attempt);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +35 to +40
@Retryable(
retryFor = Exception.class,
maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s
)
@Override

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +35 to +38
@Retryable(
retryFor = Exception.class,
maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +35 to +43
@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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kingazm should @avvvis implement tests or does this look good to you?

@kon-mtal kon-mtal left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add dependencies, decide if you want to add tests, fix @retryable wrapping DB persistence code, fix conflicts, ask @kingazm or @haniazipser for final review.

Comment on lines 26 to 30
@Service
@RequiredArgsConstructor
@Slf4j
@Profile("!dev")
@Profile({"prod"})
public class ProdAIService implements MenuAIService {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be resolved by @avvvis on conflict resolution, please look to current version in main

Comment on lines +35 to +38
@Retryable(
retryFor = Exception.class,
maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Comment on lines +35 to +40
@Retryable(
retryFor = Exception.class,
maxAttempts = 5,
backoff = @Backoff(delay = 1000, multiplier = 2) // 1s, 2s, 4s, 8s
)
@Override

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +41 to +43
public List<Dish> parseMenuFromImage(byte[] imageBytes) {
int attempt = RetrySynchronizationManager.getContext().getRetryCount() + 1;
log.info("Sending menu to AI - attempt {}/5", attempt);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +35 to +43
@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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kingazm should @avvvis implement tests or does this look good to you?

// 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());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines +5 to +10
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
@EnableRetry

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@avvvis please address

Copilot AI commented May 10, 2026

Copy link
Copy Markdown

@kon-mtal I've opened a new pull request, #88, to work on those changes. Once the pull request is ready, I'll request review from you.

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
@avvvis

avvvis commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as this PR is outdated.

@avvvis avvvis closed this Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants