This commit is contained in:
root
2025-12-27 20:19:39 +05:00
parent 7e4a7a1d83
commit febc216c1e
3 changed files with 93 additions and 25 deletions
@@ -3730,15 +3730,19 @@ public class MarketingAnalysisService {
Map<String, Object> analysisJson = generateJsonAnalysis(request);
if (analysisJson == null || analysisJson.isEmpty()) {
logger.error("generateMarketingReport: Failed to generate JSON analysis");
logger.error(
"generateMarketingReport: Failed to generate JSON analysis - likely due to OpenAI API rate limits");
// При ошибке возвращаем базовую структуру с сообщением об ошибке
report.put("analysisTypes", analysisTypeNames);
report.put("detailLevel", detailLevelName);
report.put("fullAnalysis", "Не удалось сгенерировать анализ. Пожалуйста, попробуйте позже.");
report.put("summary", "Маркетинговый анализ для " + request.getProduct());
report.put("fullAnalysis", "Не удалось сгенерировать анализ из-за превышения лимита запросов к OpenAI API. "
+
"Система автоматически повторит попытку с задержкой. Пожалуйста, попробуйте позже или уменьшите частоту запросов.");
report.put("summary", "Маркетинговый анализ для " + request.getProduct() +
" (временно недоступен из-за лимита API)");
report.put("chartsData", new HashMap<>());
logger.warn("generateMarketingReport: Returning report with error message");
logger.warn("generateMarketingReport: Returning report with rate limit error message");
} else {
logger.info("generateMarketingReport: JSON analysis generated successfully. Keys: {}",
analysisJson.keySet());
@@ -25,6 +25,7 @@ import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import jakarta.annotation.PostConstruct;
@Service
@@ -55,6 +56,15 @@ public class OpenAIAnalyticsService {
@Value("${openai.retry.multiplier:2.0}")
private double retryMultiplier;
@Value("${openai.retry.rateLimitMaxAttempts:10}")
private int rateLimitMaxAttempts;
@Value("${openai.retry.rateLimitInitialDelayMs:5000}")
private long rateLimitInitialDelayMs;
@Value("${openai.retry.rateLimitMaxDelayMs:300000}")
private long rateLimitMaxDelayMs;
@Value("${openai.rateLimit.maxConcurrentRequests:2}")
private int maxConcurrentRequests;
@@ -395,18 +405,49 @@ public class OpenAIAnalyticsService {
}
private RetryBackoffSpec createRetrySpec(String operation) {
return Retry.backoff(maxRetryAttempts, Duration.ofMillis(initialRetryDelayMs))
// Store Retry-After duration for 429 errors
AtomicReference<Duration> retryAfterDuration = new AtomicReference<>(null);
// Use the higher max attempts to handle rate limit errors better
// This will apply to all retries, but rate limit errors need more attempts
int effectiveMaxAttempts = Math.max(maxRetryAttempts, rateLimitMaxAttempts);
return Retry.backoff(effectiveMaxAttempts, Duration.ofMillis(initialRetryDelayMs))
.maxBackoff(Duration.ofMillis(maxRetryDelayMs))
.multiplier(retryMultiplier)
.filter(throwable -> {
if (throwable instanceof WebClientResponseException) {
WebClientResponseException wcre = (WebClientResponseException) throwable;
int statusCode = wcre.getStatusCode().value();
if (statusCode == 429) {
// Extract Retry-After header for 429 errors
String retryAfterHeader = wcre.getHeaders().getFirst("Retry-After");
if (retryAfterHeader != null) {
try {
int seconds = Integer.parseInt(retryAfterHeader);
// Add 10% buffer to be safe, but cap at max delay
int delaySeconds = (int) Math.min(seconds * 1.1, rateLimitMaxDelayMs / 1000);
retryAfterDuration.set(Duration.ofSeconds(delaySeconds));
logger.warn(
"OpenAI API returned 429 for operation '{}'. Retry-After: {} seconds. Will wait {}ms before retry.",
operation, seconds, retryAfterDuration.get().toMillis());
} catch (NumberFormatException e) {
// Ignore if header is not a number
retryAfterDuration.set(null);
}
} else {
retryAfterDuration.set(null);
}
} else {
retryAfterDuration.set(null);
}
// Only retry on 429 (rate limit) and 5xx (server errors)
// Don't retry on 401, 403, etc.
return statusCode == 429 || statusCode >= 500;
}
// Retry on network errors (timeouts, connection issues)
retryAfterDuration.set(null);
return throwable instanceof java.util.concurrent.TimeoutException
|| throwable instanceof java.net.ConnectException
|| throwable instanceof java.io.IOException;
@@ -414,36 +455,56 @@ public class OpenAIAnalyticsService {
.doBeforeRetry(retrySignal -> {
long attempt = retrySignal.totalRetries() + 1;
Throwable failure = retrySignal.failure();
Duration retryAfter = null;
if (failure instanceof WebClientResponseException) {
// If we have a Retry-After duration for 429 errors, sleep for that duration
// This ensures we respect the API's requested delay before Reactor applies its backoff
Duration customDelay = retryAfterDuration.get();
if (customDelay != null && failure instanceof WebClientResponseException) {
WebClientResponseException wcre = (WebClientResponseException) failure;
if (wcre.getStatusCode().value() == 429) {
// Try to extract Retry-After header
String retryAfterHeader = wcre.getHeaders().getFirst("Retry-After");
if (retryAfterHeader != null) {
try {
int seconds = Integer.parseInt(retryAfterHeader);
retryAfter = Duration.ofSeconds(seconds);
logger.warn(
"OpenAI API returned 429 for operation '{}'. Retry-After: {} seconds. Will retry in {}ms (attempt {}/{})",
operation, seconds, retryAfter.toMillis(), attempt, maxRetryAttempts);
} catch (NumberFormatException e) {
// Ignore if header is not a number
}
try {
logger.warn(
"Respecting Retry-After header: sleeping for {}ms (attempt {}/{})",
customDelay.toMillis(), attempt, rateLimitMaxAttempts);
Thread.sleep(customDelay.toMillis());
retryAfterDuration.set(null); // Reset after use
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warn("Interrupted while waiting for Retry-After delay");
}
}
}
if (retryAfter == null) {
logger.warn("OpenAI API returned error for operation '{}'. Will retry (attempt {}/{})",
// Log retry attempt
if (failure instanceof WebClientResponseException) {
WebClientResponseException wcre = (WebClientResponseException) failure;
int statusCode = wcre.getStatusCode().value();
int maxAttempts = statusCode == 429 ? rateLimitMaxAttempts : maxRetryAttempts;
if (statusCode == 429 && customDelay == null) {
logger.warn("OpenAI API returned 429 for operation '{}'. Will retry with exponential backoff (attempt {}/{})",
operation, attempt, maxAttempts);
} else if (statusCode != 429) {
logger.warn("OpenAI API returned {} for operation '{}'. Will retry (attempt {}/{})",
statusCode, operation, attempt, maxAttempts);
}
} else {
logger.warn("OpenAI API network error for operation '{}'. Will retry (attempt {}/{})",
operation, attempt, maxRetryAttempts);
}
})
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
Throwable failure = retrySignal.failure();
int maxAttempts = maxRetryAttempts;
if (failure instanceof WebClientResponseException) {
WebClientResponseException wcre = (WebClientResponseException) failure;
if (wcre.getStatusCode().value() == 429) {
maxAttempts = rateLimitMaxAttempts;
}
}
logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts. Giving up.",
operation, maxRetryAttempts);
operation, maxAttempts);
return retrySignal.failure();
});
}
}
@@ -68,6 +68,9 @@ openai.retry.maxAttempts=3
openai.retry.initialDelayMs=2000
openai.retry.maxDelayMs=60000
openai.retry.multiplier=2.0
openai.retry.rateLimitMaxAttempts=10
openai.retry.rateLimitInitialDelayMs=5000
openai.retry.rateLimitMaxDelayMs=300000
openai.rateLimit.maxConcurrentRequests=2
# Serper.dev Web Search Configuration