This commit is contained in:
root
2025-12-27 20:42:02 +05:00
parent dced51168c
commit e8b99c1fe5
2 changed files with 81 additions and 5 deletions
@@ -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