Skip to content

Commit ec45dd6

Browse files
committed
MLE-27388 Added test for 50x retry
1 parent bf0ae48 commit ec45dd6

File tree

4 files changed

+178
-13
lines changed

4 files changed

+178
-13
lines changed

marklogic-client-api/src/main/java/com/marklogic/client/impl/OkHttpServices.java

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ private Response sendRequestWithRetry(
523523
throw new MarkLogicIOException("Request cancelled: thread was interrupted before request could be sent");
524524
}
525525

526-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
526+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
527527
Response response = null;
528528
int status = -1;
529529

@@ -1195,7 +1195,7 @@ private TemporalDescriptor putPostDocumentImpl(RequestLogger reqlog, String meth
11951195
}
11961196
boolean isResendable = handleBase.isResendable();
11971197

1198-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
1198+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
11991199
Response response = null;
12001200
int status = -1;
12011201
Headers responseHeaders = null;
@@ -1348,7 +1348,7 @@ private TemporalDescriptor putPostDocumentImpl(RequestLogger reqlog, String meth
13481348
requestBldr = addVersionHeader(desc, requestBldr, "If-Match");
13491349
}
13501350

1351-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
1351+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
13521352
Response response = null;
13531353
int status = -1;
13541354
Headers responseHeaders = null;
@@ -2107,7 +2107,7 @@ void init() {
21072107
}
21082108

21092109
Response getResponse() {
2110-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, OkHttpServices.this::resetFirstRequestFlag);
2110+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, OkHttpServices.this::resetFirstRequestFlag);
21112111
Response response = null;
21122112
int status = -1;
21132113
for (; retryContext.shouldContinueRetrying(minRetryAttempts, maxDelayForRetries); retryContext.incrementRetry()) {
@@ -2635,7 +2635,7 @@ private void putPostValueImpl(RequestLogger reqlog, String method,
26352635
String connectPath = null;
26362636
Request.Builder requestBldr = null;
26372637

2638-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
2638+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
26392639
Response response = null;
26402640
int status = -1;
26412641
for (; retryContext.shouldContinueRetrying(minRetryAttempts, maxDelayForRetries); retryContext.incrementRetry()) {
@@ -3060,7 +3060,7 @@ public <R extends AbstractReadHandle, W extends AbstractWriteHandle> R putResour
30603060
String outputMimetype = outputBase.getMimetype();
30613061
Class as = outputBase.receiveAs();
30623062

3063-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
3063+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
30643064
Response response = null;
30653065
int status = -1;
30663066
for (; retryContext.shouldContinueRetrying(minRetryAttempts, maxDelayForRetries); retryContext.incrementRetry()) {
@@ -3223,7 +3223,7 @@ public <R extends AbstractReadHandle, W extends AbstractWriteHandle> R postResou
32233223
String outputMimetype = outputBase != null ? outputBase.getMimetype() : null;
32243224
Class as = outputBase != null ? outputBase.receiveAs() : null;
32253225

3226-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
3226+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
32273227
Response response = null;
32283228
int status = -1;
32293229
for (; retryContext.shouldContinueRetrying(minRetryAttempts, maxDelayForRetries); retryContext.incrementRetry()) {
@@ -3822,7 +3822,7 @@ private <W extends AbstractWriteHandle, U extends OkHttpResultIterator> U postIt
38223822
throws ResourceNotFoundException, ResourceNotResendableException, ForbiddenUserException, FailedRequestException {
38233823
if (params == null) params = new RequestParameters();
38243824
if (transaction != null) params.add("txid", transaction.getTransactionId());
3825-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
3825+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
38263826
Response response = null;
38273827
int status = -1;
38283828
for (; retryContext.shouldContinueRetrying(minRetryAttempts, maxDelayForRetries); retryContext.incrementRetry()) {
@@ -4928,7 +4928,7 @@ public InputStream match(QueryDefinition queryDef,
49284928
}
49294929
requestBldr = addTelemetryAgentId(requestBldr);
49304930

4931-
RetryContext retryContext = new RetryContext(logger, RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
4931+
RetryContext retryContext = new RetryContext(RETRYABLE_STATUS_CODES, this::resetFirstRequestFlag);
49324932
Response response = null;
49334933
int status = -1;
49344934
for (; retryContext.shouldContinueRetrying(minRetryAttempts, maxDelayForRetries); retryContext.incrementRetry()) {

marklogic-client-api/src/main/java/com/marklogic/client/impl/RetryContext.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import com.marklogic.client.FailedRetryException;
77
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
89

910
import java.util.Set;
1011

@@ -13,7 +14,9 @@
1314
* Tracks retry state, calculates delays, handles sleeping, and logs retry attempts.
1415
*/
1516
class RetryContext {
16-
private final Logger logger;
17+
18+
static final private Logger logger = LoggerFactory.getLogger(RetryContext.class);
19+
1720
private final Set<Integer> retryableStatusCodes;
1821
private final Runnable onMaxRetriesCallback;
1922

@@ -22,12 +25,10 @@ class RetryContext {
2225
private int nextDelay = 0;
2326

2427
/**
25-
* @param logger Logger for debug output
2628
* @param retryableStatusCodes Set of HTTP status codes that trigger retries
2729
* @param onMaxRetriesCallback Callback to invoke when max retries is exceeded (e.g., to reset first request flag)
2830
*/
29-
RetryContext(Logger logger, Set<Integer> retryableStatusCodes, Runnable onMaxRetriesCallback) {
30-
this.logger = logger;
31+
RetryContext(Set<Integer> retryableStatusCodes, Runnable onMaxRetriesCallback) {
3132
this.retryableStatusCodes = retryableStatusCodes;
3233
this.onMaxRetriesCallback = onMaxRetriesCallback;
3334
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
* Copyright (c) 2010-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
3+
*/
4+
package com.marklogic.client.impl.okhttp;
5+
6+
import com.marklogic.client.DatabaseClient;
7+
import com.marklogic.client.DatabaseClientFactory;
8+
import com.marklogic.client.FailedRequestException;
9+
import com.marklogic.client.extra.okhttpclient.OkHttpClientConfigurator;
10+
import com.marklogic.client.impl.RESTServices;
11+
import com.marklogic.client.test.Common;
12+
import okhttp3.Interceptor;
13+
import okhttp3.Protocol;
14+
import okhttp3.Request;
15+
import okhttp3.Response;
16+
import org.junit.jupiter.api.AfterEach;
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
20+
import java.io.IOException;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
23+
import static org.junit.jupiter.api.Assertions.*;
24+
25+
/**
26+
* Tests the retry logic in OkHttpServices using a custom interceptor that simulates server errors. This is not a
27+
* well-documented feature but users are currently able to discover it via the codebase.
28+
*/
29+
class RetryOn50XResponseTest {
30+
31+
/**
32+
* Custom interceptor that returns 502 Bad Gateway responses and counts how many times it's invoked.
33+
*/
34+
private static class BadGatewayInterceptor implements Interceptor {
35+
36+
private final AtomicInteger invocationCount = new AtomicInteger(0);
37+
private final int failureCount;
38+
39+
/**
40+
* @param failureCount Number of times to return 502 before allowing the request through
41+
*/
42+
public BadGatewayInterceptor(int failureCount) {
43+
this.failureCount = failureCount;
44+
}
45+
46+
@Override
47+
public Response intercept(Chain chain) throws IOException {
48+
int count = invocationCount.incrementAndGet();
49+
Request request = chain.request();
50+
51+
// Fail the first N requests
52+
if (count <= failureCount) {
53+
return new Response.Builder()
54+
.request(request)
55+
.protocol(Protocol.HTTP_1_1)
56+
.code(502)
57+
.message("Bad Gateway")
58+
.body(okhttp3.ResponseBody.create("Simulated 502 error", null))
59+
.build();
60+
}
61+
62+
// After N failures, let the request through
63+
return chain.proceed(request);
64+
}
65+
66+
public int getInvocationCount() {
67+
return invocationCount.get();
68+
}
69+
70+
public void reset() {
71+
invocationCount.set(0);
72+
}
73+
}
74+
75+
@BeforeEach
76+
void setUp() {
77+
// Configure very short retry delays for testing
78+
System.setProperty(RESTServices.MAX_DELAY_PROP, "3");
79+
System.setProperty(RESTServices.MIN_RETRY_PROP, "2");
80+
81+
// "Touch" the Common class to trigger the static block that removes existing configurators on
82+
// DatabaseClientFactory.
83+
Common.newServerPayload();
84+
}
85+
86+
@AfterEach
87+
void tearDown() {
88+
DatabaseClientFactory.removeConfigurators();
89+
System.clearProperty(RESTServices.MAX_DELAY_PROP);
90+
System.clearProperty(RESTServices.MIN_RETRY_PROP);
91+
}
92+
93+
@Test
94+
void testRetryWith502Responses() {
95+
// Create an interceptor that will fail 2 times, then succeed
96+
BadGatewayInterceptor interceptor = new BadGatewayInterceptor(2);
97+
98+
DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) builder ->
99+
builder.addInterceptor(interceptor));
100+
101+
try (DatabaseClient client = Common.newClient()) {
102+
client.checkConnection();
103+
assertEquals(3, interceptor.getInvocationCount(),
104+
"Expected 3 invocations: 2 failures followed by 1 success");
105+
}
106+
}
107+
108+
@Test
109+
void testRetryExceedsMaxAttempts() {
110+
// Create an interceptor that will fail 10 times (more than minRetry)
111+
BadGatewayInterceptor interceptor = new BadGatewayInterceptor(10);
112+
113+
DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) builder ->
114+
builder.addInterceptor(interceptor));
115+
116+
try (DatabaseClient client = Common.newClient()) {
117+
assertThrows(FailedRequestException.class, () -> {
118+
client.checkConnection();
119+
}, "Expected FailedRequestException after exhausting retries");
120+
121+
assertTrue(interceptor.getInvocationCount() >= 3,
122+
"Expected at least 3 retry attempts, but got " + interceptor.getInvocationCount());
123+
}
124+
}
125+
126+
@Test
127+
void testRetryCountIncreases() {
128+
// Test that retry attempts increase as expected
129+
BadGatewayInterceptor interceptor = new BadGatewayInterceptor(1);
130+
131+
DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) builder ->
132+
builder.addInterceptor(interceptor));
133+
134+
try (DatabaseClient client = Common.newClient()) {
135+
// First request: fails once, then succeeds
136+
client.checkConnection();
137+
assertEquals(2, interceptor.getInvocationCount(), "Expected 2 invocations for first request");
138+
139+
// Reset and try again
140+
interceptor.reset();
141+
client.checkConnection();
142+
assertEquals(2, interceptor.getInvocationCount(), "Expected 2 invocations for second request");
143+
}
144+
}
145+
146+
@Test
147+
void testNoRetryOnSuccessfulRequest() {
148+
// Interceptor that never fails
149+
BadGatewayInterceptor interceptor = new BadGatewayInterceptor(0);
150+
151+
DatabaseClientFactory.addConfigurator((OkHttpClientConfigurator) builder ->
152+
builder.addInterceptor(interceptor));
153+
154+
try (DatabaseClient client = Common.newClient()) {
155+
client.checkConnection();
156+
assertEquals(1, interceptor.getInvocationCount(),
157+
"Expected exactly 1 invocation when request succeeds immediately");
158+
}
159+
}
160+
}

marklogic-client-api/src/test/resources/logback-test.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@
1616
<appender-ref ref="STDOUT" />
1717
</logger>
1818

19+
<logger name="com.marklogic.client.impl.RetryContext" level="DEBUG" additivity="false">
20+
<appender-ref ref="STDOUT" />
21+
</logger>
22+
1923
</configuration>

0 commit comments

Comments
 (0)