.
This commit is contained in:
@@ -22,6 +22,8 @@ import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
|||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||||
import reactor.util.retry.Retry;
|
import reactor.util.retry.Retry;
|
||||||
import reactor.util.retry.RetryBackoffSpec;
|
import reactor.util.retry.RetryBackoffSpec;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -49,9 +51,14 @@ public class OpenAIAnalyticsService {
|
|||||||
@Value("${openai.retry.multiplier:2.0}")
|
@Value("${openai.retry.multiplier:2.0}")
|
||||||
private double retryMultiplier;
|
private double retryMultiplier;
|
||||||
|
|
||||||
|
@Value("${openai.rateLimit.maxConcurrentRequests:2}")
|
||||||
|
private int maxConcurrentRequests;
|
||||||
|
|
||||||
@Value("${openai.api.key}")
|
@Value("${openai.api.key}")
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
|
||||||
|
private Semaphore requestSemaphore;
|
||||||
|
|
||||||
private String openaiUrl;
|
private String openaiUrl;
|
||||||
|
|
||||||
public OpenAIAnalyticsService(
|
public OpenAIAnalyticsService(
|
||||||
@@ -82,7 +89,10 @@ public class OpenAIAnalyticsService {
|
|||||||
.defaultHeader("Authorization", "Bearer " + apiKey)
|
.defaultHeader("Authorization", "Bearer " + apiKey)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
logger.info("OpenAIAnalyticsService initialized with model: {}", modelName);
|
this.requestSemaphore = new Semaphore(maxConcurrentRequests, true);
|
||||||
|
|
||||||
|
logger.info("OpenAIAnalyticsService initialized with model: {} and max concurrent requests: {}",
|
||||||
|
modelName, maxConcurrentRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketItem.Analytics analyzeText(String text) {
|
public MarketItem.Analytics analyzeText(String text) {
|
||||||
@@ -164,46 +174,61 @@ public class OpenAIAnalyticsService {
|
|||||||
requestBody.put("temperature", 0.3);
|
requestBody.put("temperature", 0.3);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, Object> response = this.webClient.post()
|
// Acquire semaphore permit before making request
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
if (!requestSemaphore.tryAcquire(30, TimeUnit.SECONDS)) {
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
logger.warn("Timeout waiting for OpenAI request slot. Skipping request.");
|
||||||
.bodyValue(requestBody)
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
|
||||||
})
|
|
||||||
.retryWhen(createRetrySpec("generate"))
|
|
||||||
.onErrorResume(err -> {
|
|
||||||
logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(),
|
|
||||||
err.getClass().getSimpleName());
|
|
||||||
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();
|
|
||||||
})
|
|
||||||
.block(Duration.ofMillis(timeoutMs));
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
try {
|
||||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
|
Map<String, Object> response = this.webClient.post()
|
||||||
if (choices != null && !choices.isEmpty()) {
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
Map<String, Object> firstChoice = choices.get(0);
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
Map<String, Object> messageObj = (Map<String, Object>) firstChoice.get("message");
|
.bodyValue(requestBody)
|
||||||
if (messageObj != null) {
|
.retrieve()
|
||||||
Object content = messageObj.get("content");
|
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
||||||
return content == null ? null : String.valueOf(content).trim();
|
})
|
||||||
|
.retryWhen(createRetrySpec("generate"))
|
||||||
|
.onErrorResume(err -> {
|
||||||
|
logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(),
|
||||||
|
err.getClass().getSimpleName());
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
.block(Duration.ofMillis(timeoutMs));
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
|
||||||
|
if (choices != null && !choices.isEmpty()) {
|
||||||
|
Map<String, Object> firstChoice = choices.get(0);
|
||||||
|
Map<String, Object> messageObj = (Map<String, Object>) firstChoice.get("message");
|
||||||
|
if (messageObj != null) {
|
||||||
|
Object content = messageObj.get("content");
|
||||||
|
return content == null ? null : String.valueOf(content).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// Always release semaphore permit
|
||||||
|
requestSemaphore.release();
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.warn("Interrupted while waiting for OpenAI request slot: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Error calling OpenAI generate: {}", e.getMessage());
|
logger.warn("Error calling OpenAI generate: {}", e.getMessage());
|
||||||
@@ -229,48 +254,63 @@ public class OpenAIAnalyticsService {
|
|||||||
requestBody.put("temperature", 0.3);
|
requestBody.put("temperature", 0.3);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Map<String, Object> response = this.webClient.post()
|
// Acquire semaphore permit before making request
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
if (!requestSemaphore.tryAcquire(30, TimeUnit.SECONDS)) {
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
logger.warn("Timeout waiting for OpenAI request slot. Skipping request.");
|
||||||
.bodyValue(requestBody)
|
|
||||||
.retrieve()
|
|
||||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
|
||||||
})
|
|
||||||
.retryWhen(createRetrySpec("generateWithInstruction"))
|
|
||||||
.onErrorResume(err -> {
|
|
||||||
logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(),
|
|
||||||
err.getClass().getSimpleName());
|
|
||||||
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();
|
|
||||||
})
|
|
||||||
.block(Duration.ofMillis(timeoutMs));
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
try {
|
||||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
|
Map<String, Object> response = this.webClient.post()
|
||||||
if (choices != null && !choices.isEmpty()) {
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
Map<String, Object> firstChoice = choices.get(0);
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
Map<String, Object> messageObj = (Map<String, Object>) firstChoice.get("message");
|
.bodyValue(requestBody)
|
||||||
if (messageObj != null) {
|
.retrieve()
|
||||||
Object content = messageObj.get("content");
|
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
||||||
String result = content == null ? null : String.valueOf(content).trim();
|
})
|
||||||
System.out.println("OpenAI response: " + result);
|
.retryWhen(createRetrySpec("generateWithInstruction"))
|
||||||
return result;
|
.onErrorResume(err -> {
|
||||||
|
logger.error("OpenAI request failed after retries: {} - {}", err.getMessage(),
|
||||||
|
err.getClass().getSimpleName());
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
.block(Duration.ofMillis(timeoutMs));
|
||||||
|
|
||||||
|
if (response == null) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
|
||||||
|
if (choices != null && !choices.isEmpty()) {
|
||||||
|
Map<String, Object> firstChoice = choices.get(0);
|
||||||
|
Map<String, Object> messageObj = (Map<String, Object>) firstChoice.get("message");
|
||||||
|
if (messageObj != null) {
|
||||||
|
Object content = messageObj.get("content");
|
||||||
|
String result = content == null ? null : String.valueOf(content).trim();
|
||||||
|
System.out.println("OpenAI response: " + result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
// Always release semaphore permit
|
||||||
|
requestSemaphore.release();
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.warn("Interrupted while waiting for OpenAI request slot: {}", e.getMessage());
|
||||||
return null;
|
return null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("Error calling OpenAI generate (generic): {}", e.getMessage());
|
logger.warn("Error calling OpenAI generate (generic): {}", e.getMessage());
|
||||||
@@ -286,13 +326,9 @@ public class OpenAIAnalyticsService {
|
|||||||
if (throwable instanceof WebClientResponseException) {
|
if (throwable instanceof WebClientResponseException) {
|
||||||
WebClientResponseException wcre = (WebClientResponseException) throwable;
|
WebClientResponseException wcre = (WebClientResponseException) throwable;
|
||||||
int statusCode = wcre.getStatusCode().value();
|
int statusCode = wcre.getStatusCode().value();
|
||||||
// Retry on 429 (rate limit) and 5xx (server errors)
|
// Only retry on 429 (rate limit) and 5xx (server errors)
|
||||||
boolean shouldRetry = statusCode == 429 || statusCode >= 500;
|
// Don't retry on 401, 403, etc.
|
||||||
if (shouldRetry) {
|
return statusCode == 429 || statusCode >= 500;
|
||||||
logger.warn("OpenAI API returned {} for operation '{}'. Will retry...", statusCode,
|
|
||||||
operation);
|
|
||||||
}
|
|
||||||
return shouldRetry;
|
|
||||||
}
|
}
|
||||||
// Retry on network errors (timeouts, connection issues)
|
// Retry on network errors (timeouts, connection issues)
|
||||||
return throwable instanceof java.util.concurrent.TimeoutException
|
return throwable instanceof java.util.concurrent.TimeoutException
|
||||||
@@ -301,11 +337,35 @@ public class OpenAIAnalyticsService {
|
|||||||
})
|
})
|
||||||
.doBeforeRetry(retrySignal -> {
|
.doBeforeRetry(retrySignal -> {
|
||||||
long attempt = retrySignal.totalRetries() + 1;
|
long attempt = retrySignal.totalRetries() + 1;
|
||||||
logger.info("Retrying OpenAI request for operation '{}' (attempt {}/{})",
|
Throwable failure = retrySignal.failure();
|
||||||
operation, attempt, maxRetryAttempts);
|
|
||||||
|
Duration retryAfter = null;
|
||||||
|
if (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryAfter == null) {
|
||||||
|
logger.warn("OpenAI API returned error for operation '{}'. Will retry (attempt {}/{})",
|
||||||
|
operation, attempt, maxRetryAttempts);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
||||||
logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts",
|
logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts. Giving up.",
|
||||||
operation, maxRetryAttempts);
|
operation, maxRetryAttempts);
|
||||||
return retrySignal.failure();
|
return retrySignal.failure();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||||
import reactor.util.retry.Retry;
|
import reactor.util.retry.Retry;
|
||||||
import reactor.util.retry.RetryBackoffSpec;
|
import reactor.util.retry.RetryBackoffSpec;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class OpenAiChartService {
|
public class OpenAiChartService {
|
||||||
@@ -45,6 +48,11 @@ public class OpenAiChartService {
|
|||||||
@Value("${openai.retry.multiplier:2.0}")
|
@Value("${openai.retry.multiplier:2.0}")
|
||||||
private double retryMultiplier;
|
private double retryMultiplier;
|
||||||
|
|
||||||
|
@Value("${openai.rateLimit.maxConcurrentRequests:2}")
|
||||||
|
private int maxConcurrentRequests;
|
||||||
|
|
||||||
|
private Semaphore requestSemaphore;
|
||||||
|
|
||||||
public OpenAiChartService(
|
public OpenAiChartService(
|
||||||
@Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String apiUrl,
|
@Value("${openai.api.url:https://api.openai.com/v1/chat/completions}") String apiUrl,
|
||||||
@Value("${openai.api.key:}") String apiKey) {
|
@Value("${openai.api.key:}") String apiKey) {
|
||||||
@@ -66,6 +74,12 @@ public class OpenAiChartService {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initialize() {
|
||||||
|
this.requestSemaphore = new Semaphore(maxConcurrentRequests, true);
|
||||||
|
logger.info("OpenAiChartService initialized with max concurrent requests: {}", maxConcurrentRequests);
|
||||||
|
}
|
||||||
|
|
||||||
public Mono<String> getChartDataJson(String aggregatedLearnings) {
|
public Mono<String> getChartDataJson(String aggregatedLearnings) {
|
||||||
return getChartDataJson(aggregatedLearnings, "ru");
|
return getChartDataJson(aggregatedLearnings, "ru");
|
||||||
}
|
}
|
||||||
@@ -99,60 +113,86 @@ public class OpenAiChartService {
|
|||||||
logger.info("Calling OpenAI API for task '{}' with model '{}'.", taskName, modelName);
|
logger.info("Calling OpenAI API for task '{}' with model '{}'.", taskName, modelName);
|
||||||
logger.info("OpenAI Prompt: {}", prompt);
|
logger.info("OpenAI Prompt: {}", prompt);
|
||||||
|
|
||||||
return this.webClient.post()
|
// Acquire semaphore permit before making request
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
return Mono.fromCallable(() -> {
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
try {
|
||||||
.bodyValue(body)
|
if (!requestSemaphore.tryAcquire(30, TimeUnit.SECONDS)) {
|
||||||
.retrieve()
|
logger.warn("Timeout waiting for OpenAI request slot for task '{}'. Skipping request.", taskName);
|
||||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
return null;
|
||||||
})
|
}
|
||||||
.timeout(Duration.ofMillis(timeoutMs))
|
return requestSemaphore;
|
||||||
.retryWhen(createRetrySpec(taskName))
|
} catch (InterruptedException e) {
|
||||||
.doOnSuccess(response -> {
|
Thread.currentThread().interrupt();
|
||||||
// 4. Логируем успешный, но еще не обработанный ответ
|
logger.warn("Interrupted while waiting for OpenAI request slot for task '{}'", taskName);
|
||||||
logger.info("Raw OpenAI response for task '{}': {}", taskName, response);
|
return null;
|
||||||
})
|
}
|
||||||
.map(resp -> {
|
})
|
||||||
try {
|
.flatMap(semaphore -> {
|
||||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) resp.get("choices");
|
if (semaphore == null) {
|
||||||
if (choices == null || choices.isEmpty()) {
|
return Mono.empty();
|
||||||
// 5. Логируем аномальные, но не ошибочные ситуации
|
|
||||||
logger.warn("OpenAI response for task '{}' contained no 'choices'.", taskName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
|
|
||||||
if (message == null) {
|
|
||||||
logger.warn("OpenAI choice for task '{}' contained no 'message'.", taskName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Object content = message.get("content");
|
|
||||||
if (content == null) {
|
|
||||||
logger.warn("OpenAI message for task '{}' contained no 'content'.", taskName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String responseContent = String.valueOf(content);
|
|
||||||
logger.info("Successfully extracted content from OpenAI for task '{}'. Content length: {}",
|
|
||||||
taskName, responseContent.length());
|
|
||||||
return responseContent;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// 6. Логируем ошибку парсинга, если структура ответа неожиданная
|
|
||||||
logger.error("Failed to parse OpenAI response for task '{}'. Response body: {}", taskName, resp,
|
|
||||||
e);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
})
|
return this.webClient.post()
|
||||||
.doOnError(e -> {
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
// 7. Логируем ошибки сети или HTTP-статусов (4xx, 5xx)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
if (e instanceof WebClientResponseException) {
|
.bodyValue(body)
|
||||||
WebClientResponseException wcre = (WebClientResponseException) e;
|
.retrieve()
|
||||||
logger.error("Error from OpenAI API for task '{}'. Status: {}, Body: {}",
|
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
|
||||||
taskName, wcre.getStatusCode(), wcre.getResponseBodyAsString(), e);
|
})
|
||||||
} else {
|
.timeout(Duration.ofMillis(timeoutMs))
|
||||||
logger.error("Generic WebClient error for task '{}'.", taskName, e);
|
.retryWhen(createRetrySpec(taskName))
|
||||||
}
|
.doFinally(signalType -> {
|
||||||
})
|
// Always release semaphore permit
|
||||||
.onErrorResume(e -> Mono.empty()); // В случае любой ошибки возвращаем пустой результат, чтобы не
|
semaphore.release();
|
||||||
// прерывать цепочку вызовов
|
})
|
||||||
|
.doOnSuccess(response -> {
|
||||||
|
// 4. Логируем успешный, но еще не обработанный ответ
|
||||||
|
logger.info("Raw OpenAI response for task '{}': {}", taskName, response);
|
||||||
|
})
|
||||||
|
.map(resp -> {
|
||||||
|
try {
|
||||||
|
List<Map<String, Object>> choices = (List<Map<String, Object>>) resp.get("choices");
|
||||||
|
if (choices == null || choices.isEmpty()) {
|
||||||
|
// 5. Логируем аномальные, но не ошибочные ситуации
|
||||||
|
logger.warn("OpenAI response for task '{}' contained no 'choices'.", taskName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
|
||||||
|
if (message == null) {
|
||||||
|
logger.warn("OpenAI choice for task '{}' contained no 'message'.", taskName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object content = message.get("content");
|
||||||
|
if (content == null) {
|
||||||
|
logger.warn("OpenAI message for task '{}' contained no 'content'.", taskName);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String responseContent = String.valueOf(content);
|
||||||
|
logger.info(
|
||||||
|
"Successfully extracted content from OpenAI for task '{}'. Content length: {}",
|
||||||
|
taskName, responseContent.length());
|
||||||
|
return responseContent;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 6. Логируем ошибку парсинга, если структура ответа неожиданная
|
||||||
|
logger.error("Failed to parse OpenAI response for task '{}'. Response body: {}",
|
||||||
|
taskName, resp,
|
||||||
|
e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnError(e -> {
|
||||||
|
// 7. Логируем ошибки сети или HTTP-статусов (4xx, 5xx)
|
||||||
|
if (e instanceof WebClientResponseException) {
|
||||||
|
WebClientResponseException wcre = (WebClientResponseException) e;
|
||||||
|
logger.error("Error from OpenAI API for task '{}'. Status: {}, Body: {}",
|
||||||
|
taskName, wcre.getStatusCode(), wcre.getResponseBodyAsString(), e);
|
||||||
|
} else {
|
||||||
|
logger.error("Generic WebClient error for task '{}'.", taskName, e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onErrorResume(e -> Mono.empty()); // В случае любой ошибки возвращаем пустой результат,
|
||||||
|
// чтобы не
|
||||||
|
// прерывать цепочку вызовов
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildChartDataPrompt(String learningsAsString, String language) {
|
private String buildChartDataPrompt(String learningsAsString, String language) {
|
||||||
@@ -265,13 +305,9 @@ public class OpenAiChartService {
|
|||||||
if (throwable instanceof WebClientResponseException) {
|
if (throwable instanceof WebClientResponseException) {
|
||||||
WebClientResponseException wcre = (WebClientResponseException) throwable;
|
WebClientResponseException wcre = (WebClientResponseException) throwable;
|
||||||
int statusCode = wcre.getStatusCode().value();
|
int statusCode = wcre.getStatusCode().value();
|
||||||
// Retry on 429 (rate limit) and 5xx (server errors)
|
// Only retry on 429 (rate limit) and 5xx (server errors)
|
||||||
boolean shouldRetry = statusCode == 429 || statusCode >= 500;
|
// Don't retry on 401, 403, etc.
|
||||||
if (shouldRetry) {
|
return statusCode == 429 || statusCode >= 500;
|
||||||
logger.warn("OpenAI API returned {} for operation '{}'. Will retry...", statusCode,
|
|
||||||
operation);
|
|
||||||
}
|
|
||||||
return shouldRetry;
|
|
||||||
}
|
}
|
||||||
// Retry on network errors (timeouts, connection issues)
|
// Retry on network errors (timeouts, connection issues)
|
||||||
return throwable instanceof java.util.concurrent.TimeoutException
|
return throwable instanceof java.util.concurrent.TimeoutException
|
||||||
@@ -280,11 +316,35 @@ public class OpenAiChartService {
|
|||||||
})
|
})
|
||||||
.doBeforeRetry(retrySignal -> {
|
.doBeforeRetry(retrySignal -> {
|
||||||
long attempt = retrySignal.totalRetries() + 1;
|
long attempt = retrySignal.totalRetries() + 1;
|
||||||
logger.info("Retrying OpenAI request for operation '{}' (attempt {}/{})",
|
Throwable failure = retrySignal.failure();
|
||||||
operation, attempt, maxRetryAttempts);
|
|
||||||
|
Duration retryAfter = null;
|
||||||
|
if (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (retryAfter == null) {
|
||||||
|
logger.warn("OpenAI API returned error for operation '{}'. Will retry (attempt {}/{})",
|
||||||
|
operation, attempt, maxRetryAttempts);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
||||||
logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts",
|
logger.error("OpenAI request for operation '{}' exhausted all {} retry attempts. Giving up.",
|
||||||
operation, maxRetryAttempts);
|
operation, maxRetryAttempts);
|
||||||
return retrySignal.failure();
|
return retrySignal.failure();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -58,10 +58,11 @@ openai.api.key=sk-proj-zEsggv3MCgvZ2aQbRhpCfyZPKn-isyylNhO16dekXGpM3yzi9H4g0zSjp
|
|||||||
openai.api.url=https://api.openai.com/v1/chat/completions
|
openai.api.url=https://api.openai.com/v1/chat/completions
|
||||||
openai.model.name=gpt-4o-mini
|
openai.model.name=gpt-4o-mini
|
||||||
openai.timeoutMs=90000
|
openai.timeoutMs=90000
|
||||||
openai.retry.maxAttempts=5
|
openai.retry.maxAttempts=3
|
||||||
openai.retry.initialDelayMs=1000
|
openai.retry.initialDelayMs=2000
|
||||||
openai.retry.maxDelayMs=60000
|
openai.retry.maxDelayMs=60000
|
||||||
openai.retry.multiplier=2.0
|
openai.retry.multiplier=2.0
|
||||||
|
openai.rateLimit.maxConcurrentRequests=2
|
||||||
|
|
||||||
# Email Configuration
|
# Email Configuration
|
||||||
spring.mail.host=smtp.gmail.com
|
spring.mail.host=smtp.gmail.com
|
||||||
|
|||||||
Reference in New Issue
Block a user