Skip to content

Trying again to fix logical issues #202

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed
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
@@ -0,0 +1,44 @@
package io.github.doocs.im.model.response;

import java.io.Serializable;

public class BaseGenericResult extends GenericResult implements Serializable {

private static final long serialVersionUID = -8713954419178432365L;


@Override
public String getActionStatus() {
return super.getActionStatus();
}

@Override
public void setActionStatus(String actionStatus) {
super.setActionStatus(actionStatus);
}

@Override
public String getErrorInfo() {
return super.getErrorInfo();
}

@Override
public void setErrorInfo(String errorInfo) {
super.setErrorInfo(errorInfo);
}

@Override
public Integer getErrorCode() {
return super.getErrorCode();
}

@Override
public void setErrorCode(Integer errorCode) {
super.setErrorCode(errorCode);
}

@Override
public String toString() {
return super.toString();
}
}
91 changes: 46 additions & 45 deletions src/main/java/io/github/doocs/im/util/HttpUtil.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.doocs.im.util;

import io.github.doocs.im.ClientConfiguration;
import io.github.doocs.im.model.response.BaseGenericResult;
import io.github.doocs.im.model.response.GenericResult;
import okhttp3.*;

Expand Down Expand Up @@ -32,7 +33,7 @@ public class HttpUtil {
.writeTimeout(DEFAULT_CONFIG.getWriteTimeout(), TimeUnit.MILLISECONDS)
.callTimeout(DEFAULT_CONFIG.getCallTimeout(), TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(false)
.addInterceptor(new RetryInterceptor(DEFAULT_CONFIG.getMaxRetries(), DEFAULT_CONFIG.getRetryIntervalMs(), DEFAULT_CONFIG.getBusinessRetryCodes(), DEFAULT_CONFIG.isEnableBusinessRetry()))
.addInterceptor(new RetryInterceptor(DEFAULT_CONFIG.getMaxRetries(), DEFAULT_CONFIG.getRetryIntervalMs(), DEFAULT_CONFIG.getBusinessRetryCodes(), DEFAULT_CONFIG.isEnableBusinessRetry(), BaseGenericResult.class))
.build();

private HttpUtil() {
Expand All @@ -59,7 +60,7 @@ private static OkHttpClient getClient(ClientConfiguration config) {
.writeTimeout(cfg.getWriteTimeout(), TimeUnit.MILLISECONDS)
.callTimeout(cfg.getCallTimeout(), TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(false)
.addInterceptor(new RetryInterceptor(cfg.getMaxRetries(), cfg.getRetryIntervalMs(), DEFAULT_CONFIG.getBusinessRetryCodes(), DEFAULT_CONFIG.isEnableBusinessRetry()))
.addInterceptor(new RetryInterceptor(cfg.getMaxRetries(), cfg.getRetryIntervalMs(), cfg.getBusinessRetryCodes(), cfg.isEnableBusinessRetry(), BaseGenericResult.class))
.build());
}

Expand Down Expand Up @@ -103,89 +104,89 @@ class RetryInterceptor implements Interceptor {
Stream.of(408, 429, 500, 502, 503, 504).collect(Collectors.toSet())
);
private static final int MAX_DELAY_MS = 10000;
private static final int MAX_BODY_SIZE = 1 * 1024 * 1024;
private final int maxRetries;
private final long retryIntervalMs;
private final Set<Integer> businessRetryCodes;
private final boolean enableBusinessRetry;
private final Class<? extends GenericResult> resultType;
private final Random random = new Random();

public RetryInterceptor(int maxRetries, long retryIntervalMs, Set<Integer> businessRetryCodes, boolean enableBusinessRetry) {
this.maxRetries = maxRetries;
public RetryInterceptor(int maxRetries, long retryIntervalMs, Set<Integer> businessRetryCodes, boolean enableBusinessRetry, Class<? extends GenericResult> resultType) {
this.maxRetries = maxRetries + 1;
this.retryIntervalMs = retryIntervalMs;
this.businessRetryCodes = businessRetryCodes;
this.enableBusinessRetry = enableBusinessRetry;
this.resultType = Objects.requireNonNull(resultType);
}

@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException exception = null;
for (int attempt = 0; attempt <= maxRetries; ++attempt) {
if (response != null) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
if (response != null)
response.close();
}
try {
response = chain.proceed(request);
if (response.isSuccessful() && !shouldRetry(response)) {
if (response.isSuccessful()) {
if (enableBusinessRetry && shouldRetryForBusiness(response)) {
waitForRetry(attempt);
continue;
}
return response;
}
if (!shouldRetry(response)) {
} else {
if (shouldRetryForHttp(response)) {
waitForRetry(attempt);
continue;
}
return response;
}
} catch (IOException e) {
if (attempt >= maxRetries) {
throw e;
}
exception = e;
}
if (attempt < maxRetries) {
if (attempt == maxRetries) throw e;
waitForRetry(attempt);
}
}

if (response != null) {
return response;
}
if (exception != null) {
throw exception;
} else {
throw new IOException("Failed to get a valid response after all retries and no exception was caught.");
}
if (exception != null) throw exception;
if (response != null) return response;
throw new IOException("Failed after all retries with no response");
}

private boolean shouldRetry(Response response) {
final int code = response.code();
if (code >= 500 && code < 600) {
return true;
}
if (RETRYABLE_STATUS_CODES.contains(code)) {
return true;
}
if (enableBusinessRetry) {
return shouldRetryBasedOnBusinessCode(response);
}
return false;
private boolean shouldRetryForHttp(Response response) {
int code = response.code();
return code >= 500 || RETRYABLE_STATUS_CODES.contains(code);
}

private void waitForRetry(int attempt) {
private void waitForRetry(int attempt) throws IOException {
try {
final long delayMs = Math.min(MAX_DELAY_MS, retryIntervalMs * (1L << attempt));
TimeUnit.MILLISECONDS.sleep(delayMs);
long delay = calculateBackoff(attempt);
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", e);
}
}

private boolean shouldRetryBasedOnBusinessCode(Response response) {
private long calculateBackoff(int attempt) {
double jitter = 0.8 + random.nextDouble() * 0.4;
long calculated = (long) (retryIntervalMs * Math.pow(2, attempt) * jitter);
return Math.min(calculated, MAX_DELAY_MS);
}

private boolean shouldRetryForBusiness(Response response) {
try {
if (businessRetryCodes == null || businessRetryCodes.isEmpty()) {
if (businessRetryCodes.isEmpty()) return false;
ResponseBody peekBody = response.peekBody(MAX_BODY_SIZE);
String responseBody = peekBody.source().readByteString().utf8();
GenericResult result = JsonUtil.str2Obj(responseBody, resultType);
if (result == null || result.getErrorCode() == null) {
return false;
}
String responseBody = Objects.requireNonNull(response.body()).string();
GenericResult genericResult = JsonUtil.str2Obj(responseBody, GenericResult.class);
int businessCode = genericResult.getErrorCode();
return businessRetryCodes.contains(businessCode);
} catch (IOException | IllegalStateException e) {
return businessRetryCodes.contains(result.getErrorCode());
} catch (Exception e) {
return false;
}
}
Expand Down
Loading