diff --git a/src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java b/src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java index b860a4a..613d862 100644 --- a/src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java +++ b/src/main/java/kz/konturai/parser/service/OpenAIAnalyticsService.java @@ -19,6 +19,9 @@ import java.util.Map; import reactor.netty.http.client.HttpClient; import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; import jakarta.annotation.PostConstruct; @Service @@ -34,6 +37,18 @@ public class OpenAIAnalyticsService { @Value("${openai.timeoutMs:90000}") private long timeoutMs; + @Value("${openai.retry.maxAttempts:5}") + private int maxRetryAttempts; + + @Value("${openai.retry.initialDelayMs:1000}") + private long initialRetryDelayMs; + + @Value("${openai.retry.maxDelayMs:60000}") + private long maxRetryDelayMs; + + @Value("${openai.retry.multiplier:2.0}") + private double retryMultiplier; + @Value("${openai.api.key}") private String apiKey; @@ -156,16 +171,20 @@ public class OpenAIAnalyticsService { .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) + .retryWhen(createRetrySpec("generate")) .onErrorResume(err -> { - logger.error("OpenAI request failed: {} - {}", err.getMessage(), + logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(), err.getClass().getSimpleName()); - if (err.getMessage().contains("401")) { - logger.error( - "OpenAI API key is invalid or expired. Please check your openai.api.key configuration."); - } else if (err.getMessage().contains("429")) { - logger.error("OpenAI API rate limit exceeded. Please try again later."); - } else if (err.getMessage().contains("500")) { - logger.error("OpenAI API server error. Please try again later."); + if (err instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) err; + if (wcre.getStatusCode().value() == 401) { + logger.error( + "OpenAI API key is invalid or expired. Please check your openai.api.key configuration."); + } else if (wcre.getStatusCode().value() == 429) { + logger.error("OpenAI API rate limit exceeded after all retry attempts."); + } else if (wcre.getStatusCode().value() >= 500) { + logger.error("OpenAI API server error after all retry attempts."); + } } return Mono.empty(); }) @@ -217,16 +236,20 @@ public class OpenAIAnalyticsService { .retrieve() .bodyToMono(new ParameterizedTypeReference>() { }) + .retryWhen(createRetrySpec("generateWithInstruction")) .onErrorResume(err -> { - logger.error("OpenAI request failed: {} - {}", err.getMessage(), + logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(), err.getClass().getSimpleName()); - if (err.getMessage().contains("401")) { - logger.error( - "OpenAI API key is invalid or expired. Please check your openai.api.key configuration."); - } else if (err.getMessage().contains("429")) { - logger.error("OpenAI API rate limit exceeded. Please try again later."); - } else if (err.getMessage().contains("500")) { - logger.error("OpenAI API server error. Please try again later."); + if (err instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) err; + if (wcre.getStatusCode().value() == 401) { + logger.error( + "OpenAI API key is invalid or expired. Please check your openai.api.key configuration."); + } else if (wcre.getStatusCode().value() == 429) { + logger.error("OpenAI API rate limit exceeded after all retry attempts."); + } else if (wcre.getStatusCode().value() >= 500) { + logger.error("OpenAI API server error after all retry attempts."); + } } return Mono.empty(); }) @@ -254,4 +277,37 @@ public class OpenAIAnalyticsService { return null; } } + + private RetryBackoffSpec createRetrySpec(String operation) { + return Retry.backoff(maxRetryAttempts, Duration.ofMillis(initialRetryDelayMs)) + .maxBackoff(Duration.ofMillis(maxRetryDelayMs)) + .multiplier(retryMultiplier) + .filter(throwable -> { + if (throwable instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) throwable; + int statusCode = wcre.getStatusCode().value(); + // Retry on 429 (rate limit) and 5xx (server errors) + boolean shouldRetry = statusCode == 429 || statusCode >= 500; + if (shouldRetry) { + logger.warn("OpenAI API returned {} for operation '{}'. Will retry...", statusCode, + operation); + } + return shouldRetry; + } + // Retry on network errors (timeouts, connection issues) + return throwable instanceof java.util.concurrent.TimeoutException + || throwable instanceof java.net.ConnectException + || throwable instanceof java.io.IOException; + }) + .doBeforeRetry(retrySignal -> { + long attempt = retrySignal.totalRetries() + 1; + logger.info("Retrying OpenAI request for operation '{}' (attempt {}/{})", + operation, attempt, maxRetryAttempts); + }) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts", + operation, maxRetryAttempts); + return retrySignal.failure(); + }); + } } diff --git a/src/main/java/kz/konturai/parser/service/OpenAiChartService.java b/src/main/java/kz/konturai/parser/service/OpenAiChartService.java index 545c581..5ae9236 100644 --- a/src/main/java/kz/konturai/parser/service/OpenAiChartService.java +++ b/src/main/java/kz/konturai/parser/service/OpenAiChartService.java @@ -17,6 +17,8 @@ import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.util.retry.Retry; +import reactor.util.retry.RetryBackoffSpec; @Service public class OpenAiChartService { @@ -31,6 +33,18 @@ public class OpenAiChartService { @Value("${openai.timeoutMs:90000}") private long timeoutMs; + @Value("${openai.retry.maxAttempts:5}") + private int maxRetryAttempts; + + @Value("${openai.retry.initialDelayMs:1000}") + private long initialRetryDelayMs; + + @Value("${openai.retry.maxDelayMs:60000}") + private long maxRetryDelayMs; + + @Value("${openai.retry.multiplier:2.0}") + private double retryMultiplier; + public OpenAiChartService( @Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String apiUrl, @Value("${openai.api.key:}") String apiKey) { @@ -93,6 +107,7 @@ public class OpenAiChartService { .bodyToMono(new ParameterizedTypeReference>() { }) .timeout(Duration.ofMillis(timeoutMs)) + .retryWhen(createRetrySpec(taskName)) .doOnSuccess(response -> { // 4. Логируем успешный, но еще не обработанный ответ logger.info("Raw OpenAI response for task '{}': {}", taskName, response); @@ -242,4 +257,37 @@ public class OpenAiChartService { return buildSvgPrompt(json, "ru"); } + private RetryBackoffSpec createRetrySpec(String operation) { + return Retry.backoff(maxRetryAttempts, Duration.ofMillis(initialRetryDelayMs)) + .maxBackoff(Duration.ofMillis(maxRetryDelayMs)) + .multiplier(retryMultiplier) + .filter(throwable -> { + if (throwable instanceof WebClientResponseException) { + WebClientResponseException wcre = (WebClientResponseException) throwable; + int statusCode = wcre.getStatusCode().value(); + // Retry on 429 (rate limit) and 5xx (server errors) + boolean shouldRetry = statusCode == 429 || statusCode >= 500; + if (shouldRetry) { + logger.warn("OpenAI API returned {} for operation '{}'. Will retry...", statusCode, + operation); + } + return shouldRetry; + } + // Retry on network errors (timeouts, connection issues) + return throwable instanceof java.util.concurrent.TimeoutException + || throwable instanceof java.net.ConnectException + || throwable instanceof java.io.IOException; + }) + .doBeforeRetry(retrySignal -> { + long attempt = retrySignal.totalRetries() + 1; + logger.info("Retrying OpenAI request for operation '{}' (attempt {}/{})", + operation, attempt, maxRetryAttempts); + }) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> { + logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts", + operation, maxRetryAttempts); + return retrySignal.failure(); + }); + } + } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index df747f9..34e3e49 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -58,6 +58,10 @@ openai.api.key=sk-proj-zEsggv3MCgvZ2aQbRhpCfyZPKn-isyylNhO16dekXGpM3yzi9H4g0zSjp openai.api.url=https://api.openai.com/v1/chat/completions openai.model.name=gpt-4o-mini openai.timeoutMs=90000 +openai.retry.maxAttempts=5 +openai.retry.initialDelayMs=1000 +openai.retry.maxDelayMs=60000 +openai.retry.multiplier=2.0 # Email Configuration spring.mail.host=smtp.gmail.com