Skip to content
13 changes: 4 additions & 9 deletions src/main/java/io/github/doocs/im/ClientConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
import io.github.doocs.im.util.VersionInfoUtil;
import okhttp3.ConnectionPool;

import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.*;

/**
* 客户端配置类
Expand Down Expand Up @@ -44,11 +41,9 @@ public class ClientConfiguration {
* 默认重试错误码
*/
public static final Set<Integer> DEFAULT_BUSINESS_RETRY_CODES =
Collections.unmodifiableSet(new HashSet<Integer>() {{
add(10002);
add(20004);
add(20005);
}});
Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(10002, 20004, 20005))
);

/**
* 默认超时时间(毫秒)
Expand Down
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();
}
}
94 changes: 52 additions & 42 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,98 @@ 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 = 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 (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 (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) {
if (!enableBusinessRetry) {
return false;
}
if (businessRetryCodes == null || businessRetryCodes.isEmpty()) {
return false;
}
try {
if (businessRetryCodes == null || businessRetryCodes.isEmpty()) {
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