Skip to content

Trying again to fix logical issues #203

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

Merged
merged 11 commits into from
May 16, 2025
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