Skip to content

Commit cf9c3b2

Browse files
Merge pull request #258 from contentstack/feat/DX-3884-retry-mechanism
feat: Add retry options to Config and integrate with OkHttpClient in Stack
2 parents fee3918 + 8a2939b commit cf9c3b2

File tree

9 files changed

+2239
-5
lines changed

9 files changed

+2239
-5
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# CHANGELOG
22

3+
## v2.4.0
4+
5+
### Feb 02, 2026
6+
- Enhancement: Retry mechanism added
7+
38
## v2.3.2
49

510
### Jan 05, 2026

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<modelVersion>4.0.0</modelVersion>
66
<groupId>com.contentstack.sdk</groupId>
77
<artifactId>java</artifactId>
8-
<version>2.3.2</version>
8+
<version>2.4.0</version>
99
<packaging>jar</packaging>
1010
<name>contentstack-java</name>
1111
<description>Java SDK for Contentstack Content Delivery API</description>
@@ -314,7 +314,7 @@
314314
<configuration>
315315
<!-- Tests are skipped by default; use -Dtest to specify which tests to run -->
316316
<!-- Example: -Dtest='*IT' for integration tests, -Dtest='Test*' for unit tests -->
317-
<skipTests>true</skipTests>
317+
<!-- <skipTests>true</skipTests> -->
318318
<!-- OPTIMIZED: Parallel execution with controlled concurrency -->
319319
<parallel>classes</parallel>
320320
<threadCount>4</threadCount>

src/main/java/com/contentstack/sdk/Config.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class Config {
3131
protected Proxy proxy = null;
3232
protected String[] earlyAccess = null;
3333
protected ConnectionPool connectionPool = new ConnectionPool();
34+
protected RetryOptions retryOptions = new RetryOptions();
3435
public String releaseId;
3536
public String previewTimestamp;
3637

@@ -129,6 +130,26 @@ public void setPlugins(List<ContentstackPlugin> plugins) {
129130
this.plugins = plugins;
130131
}
131132

133+
/**
134+
* Sets the retry options.
135+
*
136+
* @param retryOptions the retry options
137+
* @return the config
138+
*/
139+
public Config setRetryOptions(RetryOptions retryOptions) {
140+
this.retryOptions = retryOptions;
141+
return this;
142+
}
143+
144+
/**
145+
* Gets the retry options.
146+
*
147+
* @return the retry options
148+
*/
149+
public RetryOptions getRetryOptions() {
150+
return this.retryOptions;
151+
}
152+
132153
/**
133154
* Gets host.
134155
*
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.contentstack.sdk;
2+
3+
import java.io.IOException;
4+
5+
@FunctionalInterface
6+
public interface CustomBackoffStrategy {
7+
/**
8+
* Calculates delay before next retry.
9+
*
10+
* @param attempt current attempt number (0-based)
11+
* @param statusCode HTTP status code (or -1 for network errors)
12+
* @param exception the exception that occurred (may be null)
13+
* @return delay in milliseconds before next retry
14+
*/
15+
long calculateDelay(int attempt, int statusCode, IOException exception);
16+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.contentstack.sdk;
2+
3+
import java.io.IOException;
4+
import java.util.Arrays;
5+
import java.util.logging.Logger;
6+
7+
import okhttp3.Interceptor;
8+
import okhttp3.Request;
9+
import okhttp3.Response;
10+
11+
public class RetryInterceptor implements Interceptor {
12+
13+
private static final Logger logger = Logger.getLogger(RetryInterceptor.class.getName());
14+
private final RetryOptions retryOptions;
15+
16+
17+
public RetryInterceptor(RetryOptions retryOptions) {
18+
if (retryOptions == null) {
19+
throw new NullPointerException("RetryOptions cannot be null");
20+
}
21+
this.retryOptions = retryOptions;
22+
}
23+
24+
@Override
25+
public Response intercept(Chain chain) throws IOException {
26+
Request request = chain.request();
27+
Response response = null;
28+
IOException lastException = null;
29+
30+
// If retry is disabled, just proceed with the request once
31+
if (!retryOptions.isRetryEnabled()) {
32+
return chain.proceed(request);
33+
}
34+
35+
int attempt = 0;
36+
// retryLimit means number of retries, so total attempts = 1 initial + retryLimit retries
37+
int maxAttempts = retryOptions.getRetryLimit() + 1;
38+
39+
while (attempt < maxAttempts) {
40+
41+
try {
42+
if(response != null) {
43+
response.close();
44+
}
45+
response = chain.proceed(request);
46+
47+
if (shouldRetry(response.code()) && (attempt + 1) < maxAttempts) {
48+
logger.fine("Retry attempt " + (attempt + 1) + " for status " + response.code() + " on " + request.url());
49+
50+
long delay = calculateDelay(attempt, response.code(), null);
51+
Thread.sleep(delay);
52+
attempt++;
53+
continue;
54+
}
55+
return response;
56+
57+
} catch (IOException e) {
58+
// Network error occurred
59+
lastException = e;
60+
61+
if ((attempt + 1) < maxAttempts) {
62+
try {
63+
long delay = calculateDelay(attempt, -1, e);
64+
Thread.sleep(delay);
65+
attempt++;
66+
} catch (InterruptedException ie) {
67+
Thread.currentThread().interrupt();
68+
throw new IOException("Retry interrupted", ie);
69+
}
70+
continue;
71+
} else {
72+
// No more retries, throw the exception
73+
throw e;
74+
}
75+
76+
} catch (InterruptedException e) {
77+
// Thread was interrupted during sleep
78+
Thread.currentThread().interrupt();
79+
if (response != null) response.close();
80+
throw new IOException("Retry interrupted", e);
81+
}
82+
}
83+
84+
// Should not reach here normally
85+
if (lastException != null) {
86+
throw lastException;
87+
}
88+
return response;
89+
}
90+
91+
/**
92+
* Determines if a status code should trigger a retry.
93+
*
94+
* @param statusCode HTTP status code
95+
* @return true if this status code is retryable
96+
*/
97+
private boolean shouldRetry(int statusCode) {
98+
return Arrays.stream(retryOptions.getRetryableStatusCodes()).anyMatch(code -> code == statusCode);
99+
}
100+
101+
/**
102+
* Calculates the delay before the next retry attempt.
103+
*
104+
* @param attempt current attempt number (0-based)
105+
* @param statusCode HTTP status code (-1 for network errors)
106+
* @param exception the IOException that occurred (may be null)
107+
* @return delay in milliseconds
108+
*/
109+
private long calculateDelay(int attempt, int statusCode, IOException exception) {
110+
111+
if(retryOptions.hasCustomBackoff()) {
112+
return retryOptions.getCustomBackoffStrategy().calculateDelay(attempt, statusCode, exception);
113+
}
114+
long baseDelay = retryOptions.getRetryDelay();
115+
116+
switch (retryOptions.getBackoffStrategy()) {
117+
case FIXED:
118+
// baseDelay already set above
119+
break;
120+
121+
case LINEAR:
122+
baseDelay = retryOptions.getRetryDelay() * (attempt + 1);
123+
break;
124+
125+
case EXPONENTIAL:
126+
baseDelay = (long) (retryOptions.getRetryDelay() * Math.pow(2,attempt));
127+
break;
128+
129+
default:
130+
// baseDelay already set above
131+
break;
132+
}
133+
134+
return baseDelay;
135+
}
136+
137+
}

0 commit comments

Comments
 (0)