Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
import com.marklogic.client.impl.HTTPKerberosAuthInterceptor;
import com.marklogic.client.impl.HTTPSamlAuthInterceptor;
import com.marklogic.client.impl.SSLUtil;
import okhttp3.ConnectionPool;
import okhttp3.CookieJar;
import okhttp3.Dns;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.*;

import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
Expand Down Expand Up @@ -82,6 +78,9 @@ public static OkHttpClient.Builder newOkHttpClientBuilder(String host, DatabaseC
OkHttpUtil.configureSocketFactory(clientBuilder, sslContext, trustManager);
OkHttpUtil.configureHostnameVerifier(clientBuilder, sslVerifier);

// Trying this out for all calls initially to see how the regression test piplines do.
clientBuilder.addInterceptor(new RetryInterceptor(3, 1000, 2, 8000));

return clientBuilder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* Copyright (c) 2010-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
package com.marklogic.client.impl.okhttp;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;

import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;

/**
* OkHttp interceptor that retries requests on certain connection failures,
* which can be helpful when MarkLogic is temporarily unavailable during restarts.
*/
class RetryInterceptor implements Interceptor {

private final static Logger logger = org.slf4j.LoggerFactory.getLogger(RetryInterceptor.class);

private final int maxRetries;
private final long initialDelayMs;
private final double backoffMultiplier;
private final long maxDelayMs;

RetryInterceptor(int maxRetries, long initialDelayMs, double backoffMultiplier, long maxDelayMs) {
this.maxRetries = maxRetries;
this.initialDelayMs = initialDelayMs;
this.backoffMultiplier = backoffMultiplier;
this.maxDelayMs = maxDelayMs;
}

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
IOException lastException = null;

for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
return chain.proceed(request);
} catch (IOException e) {
lastException = e;

if (attempt == maxRetries || !isRetryableException(e)) {
logger.warn("Not retryable: {}; {}", e.getClass(), e.getMessage());
throw e;
}

long delay = calculateDelay(attempt);
logger.warn("Request to {} failed (attempt {}/{}): {}. Retrying in {}ms",
request.url(), attempt + 1, maxRetries, e.getMessage(), delay);

sleep(delay);
}
}

throw lastException;
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is unreachable because the for loop will always either return a successful response or throw an exception within the loop body. The lastException will never be null when reaching this point.

Suggested change
throw lastException;

Copilot uses AI. Check for mistakes.

}

private boolean isRetryableException(IOException e) {
return e instanceof ConnectException ||
e instanceof SocketTimeoutException ||
e instanceof UnknownHostException ||
(e.getMessage() != null && (
e.getMessage().contains("Failed to connect") ||
e.getMessage().contains("unexpected end of stream") ||
e.getMessage().contains("Connection reset") ||
e.getMessage().contains("Read timed out")
));
}

private long calculateDelay(int attempt) {
long delay = (long) (initialDelayMs * Math.pow(backoffMultiplier, attempt));
return Math.min(delay, maxDelayMs);
}

private void sleep(long delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
logger.warn("Ignoring InterruptedException while sleeping for retry delay: {}", ie.getMessage());
Copy link
Preview

Copilot AI Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When InterruptedException is caught, the thread's interrupt status should be restored by calling Thread.currentThread().interrupt() to properly handle thread interruption.

Suggested change
logger.warn("Ignoring InterruptedException while sleeping for retry delay: {}", ie.getMessage());
logger.warn("Interrupted while sleeping for retry delay: {}", ie.getMessage());
Thread.currentThread().interrupt();

Copilot uses AI. Check for mistakes.

}
}
}