.
This commit is contained in:
@@ -10,7 +10,10 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.reactive.function.client.WebClientResponseException;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
import reactor.util.retry.RetryBackoffSpec;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
@@ -31,6 +34,18 @@ public class SerperSearchService {
|
||||
@Value("${serper.timeoutMs:30000}")
|
||||
private long timeoutMs;
|
||||
|
||||
@Value("${serper.retry.maxAttempts:3}")
|
||||
private int maxRetryAttempts;
|
||||
|
||||
@Value("${serper.retry.initialDelayMs:1000}")
|
||||
private long initialRetryDelayMs;
|
||||
|
||||
@Value("${serper.retry.maxDelayMs:10000}")
|
||||
private long maxRetryDelayMs;
|
||||
|
||||
@Value("${serper.retry.multiplier:2.0}")
|
||||
private double retryMultiplier;
|
||||
|
||||
public SerperSearchService() {
|
||||
this.webClient = WebClient.builder()
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
|
||||
@@ -62,19 +77,31 @@ public class SerperSearchService {
|
||||
}
|
||||
|
||||
try {
|
||||
Mono<Map> call = webClient.post()
|
||||
Mono<Map<String, Object>> call = webClient.post()
|
||||
.uri(apiUrl + "/search")
|
||||
.header("X-API-KEY", apiKey)
|
||||
.bodyValue(body)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class);
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.retryWhen(createRetrySpec("search"))
|
||||
.onErrorResume(err -> {
|
||||
log.warn("Serper API request failed after retries: {} - {}", err.getMessage(),
|
||||
err.getClass().getSimpleName());
|
||||
return Mono.empty();
|
||||
});
|
||||
|
||||
if (timeoutMs > 0) {
|
||||
call = call.timeout(Duration.ofMillis(timeoutMs));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> response = (Map<String, Object>) call.block();
|
||||
Map<String, Object> response = call.block();
|
||||
|
||||
if (response == null) {
|
||||
String msg = "Serper API request failed after all retry attempts";
|
||||
log.warn(msg);
|
||||
return new SerperSearchResult(query, "ERROR", msg, List.of());
|
||||
}
|
||||
|
||||
List<SerperSearchItem> items = extractOrganicItems(response);
|
||||
return new SerperSearchResult(query, "OK", null, items);
|
||||
} catch (WebClientResponseException e) {
|
||||
@@ -88,7 +115,6 @@ public class SerperSearchService {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<SerperSearchItem> extractOrganicItems(Map<String, Object> response) {
|
||||
if (response == null || response.isEmpty()) {
|
||||
return List.of();
|
||||
@@ -134,6 +160,52 @@ public class SerperSearchService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a retry specification for Serper API calls.
|
||||
* Retries on network errors (DNS failures, connection issues, timeouts) and 5xx server errors.
|
||||
*/
|
||||
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 5xx server errors and 429 rate limits
|
||||
return statusCode == 429 || statusCode >= 500;
|
||||
}
|
||||
// Retry on network errors (DNS failures, connection issues, timeouts)
|
||||
return throwable instanceof java.util.concurrent.TimeoutException
|
||||
|| throwable instanceof java.net.ConnectException
|
||||
|| throwable instanceof java.net.UnknownHostException
|
||||
|| throwable instanceof java.io.IOException
|
||||
|| throwable.getMessage() != null && (
|
||||
throwable.getMessage().contains("Failed to resolve")
|
||||
|| throwable.getMessage().contains("Name resolution")
|
||||
|| throwable.getMessage().contains("DNS"));
|
||||
})
|
||||
.doBeforeRetry(retrySignal -> {
|
||||
long attempt = retrySignal.totalRetries() + 1;
|
||||
Throwable failure = retrySignal.failure();
|
||||
|
||||
if (failure instanceof WebClientResponseException) {
|
||||
WebClientResponseException wcre = (WebClientResponseException) failure;
|
||||
int statusCode = wcre.getStatusCode().value();
|
||||
log.warn("Serper API returned {} for operation '{}'. Will retry (attempt {}/{})",
|
||||
statusCode, operation, attempt, maxRetryAttempts);
|
||||
} else {
|
||||
log.warn("Serper API network error for operation '{}' ({}). Will retry (attempt {}/{})",
|
||||
operation, failure.getClass().getSimpleName(), attempt, maxRetryAttempts);
|
||||
}
|
||||
})
|
||||
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
|
||||
log.error("Serper API request for operation '{}' exhausted all {} retry attempts. Giving up.",
|
||||
operation, maxRetryAttempts);
|
||||
return retrySignal.failure();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ openai.rateLimit.maxConcurrentRequests=2
|
||||
serper.api.key=837c09c1f2836b888e461a34074b5e6436be06e4
|
||||
serper.api.url=https://google.serper.dev
|
||||
serper.timeoutMs=30000
|
||||
serper.retry.maxAttempts=3
|
||||
serper.retry.initialDelayMs=1000
|
||||
serper.retry.maxDelayMs=10000
|
||||
serper.retry.multiplier=2.0
|
||||
|
||||
# OpenAI DALL-E Image Generation Configuration
|
||||
openai.image.model=dall-e-3
|
||||
|
||||
Reference in New Issue
Block a user