From 9d6f31abc049e4dc5b1a8597265a6540b5144aff Mon Sep 17 00:00:00 2001 From: subo <13162733306@163.com> Date: Wed, 7 May 2025 17:33:19 +0800 Subject: [PATCH 1/5] feat: Should retry based on business code --- .../github/doocs/im/ClientConfiguration.java | 29 ++++++++++++++-- .../io/github/doocs/im/util/HttpUtil.java | 33 ++++++++++++++++--- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/main/java/io/github/doocs/im/ClientConfiguration.java b/src/main/java/io/github/doocs/im/ClientConfiguration.java index de96fd3..b3e0790 100644 --- a/src/main/java/io/github/doocs/im/ClientConfiguration.java +++ b/src/main/java/io/github/doocs/im/ClientConfiguration.java @@ -31,6 +31,11 @@ public class ClientConfiguration { * 默认自动更新签名 */ public static final boolean DEFAULT_RENEW_SIG = true; + + /** + * 默认业务错误码重试开关关闭 + */ + public static final boolean DEFAULT_ENABLE_BUSINESS_RETRY = false; /** * 默认超时时间(毫秒) */ @@ -57,6 +62,7 @@ public class ClientConfiguration { private long callTimeout = DEFAULT_CALL_TIMEOUT; private long expireTime = DEFAULT_EXPIRE_TIME; private boolean autoRenewSig = DEFAULT_RENEW_SIG; + private boolean enableBusinessRetry = DEFAULT_ENABLE_BUSINESS_RETRY; private String userAgent = DEFAULT_USER_AGENT; private ConnectionPool connectionPool = DEFAULT_CONNECTION_POOL; @@ -64,7 +70,7 @@ public ClientConfiguration() { } public ClientConfiguration(int maxRetries, long retryIntervalMs, long connectTimeout, long readTimeout, long writeTimeout, - long callTimeout, long expireTime, boolean autoRenewSig, + long callTimeout, long expireTime, boolean autoRenewSig, boolean enableBusinessRetry, String userAgent, ConnectionPool connectionPool) { if (connectionPool == null) { connectionPool = DEFAULT_CONNECTION_POOL; @@ -77,6 +83,7 @@ public ClientConfiguration(int maxRetries, long retryIntervalMs, long connectTim this.callTimeout = callTimeout; this.expireTime = expireTime; this.autoRenewSig = autoRenewSig; + this.enableBusinessRetry = enableBusinessRetry; this.userAgent = userAgent; this.connectionPool = connectionPool; } @@ -90,6 +97,7 @@ private ClientConfiguration(Builder builder) { this.callTimeout = builder.callTimeout; this.expireTime = builder.expireTime; this.autoRenewSig = builder.autoRenewSig; + this.enableBusinessRetry = builder.enableBusinessRetry; this.userAgent = builder.userAgent; this.connectionPool = builder.connectionPool; } @@ -162,6 +170,14 @@ public void setAutoRenewSig(boolean autoRenewSig) { this.autoRenewSig = autoRenewSig; } + public boolean isEnableBusinessRetry() { + return enableBusinessRetry; + } + + public void setEnableBusinessRetry(boolean enableBusinessRetry) { + this.enableBusinessRetry = enableBusinessRetry; + } + public String getUserAgent() { return userAgent; } @@ -215,6 +231,9 @@ public boolean equals(Object o) { if (autoRenewSig != that.autoRenewSig) { return false; } + if (enableBusinessRetry != that.enableBusinessRetry) { + return false; + } if (!userAgent.equals(that.userAgent)) { return false; } @@ -223,7 +242,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(maxRetries, retryIntervalMs, connectTimeout, readTimeout, writeTimeout, callTimeout, expireTime, autoRenewSig, userAgent, connectionPool); + return Objects.hash(maxRetries, retryIntervalMs, connectTimeout, readTimeout, writeTimeout, callTimeout, expireTime, autoRenewSig, enableBusinessRetry, userAgent, connectionPool); } public static final class Builder { @@ -235,6 +254,7 @@ public static final class Builder { private long callTimeout = DEFAULT_CALL_TIMEOUT; private long expireTime = DEFAULT_EXPIRE_TIME; private boolean autoRenewSig = DEFAULT_RENEW_SIG; + private boolean enableBusinessRetry = DEFAULT_ENABLE_BUSINESS_RETRY; private String userAgent = DEFAULT_USER_AGENT; private ConnectionPool connectionPool = DEFAULT_CONNECTION_POOL; @@ -285,6 +305,11 @@ public Builder autoRenewSig(boolean autoRenewSig) { return this; } + public Builder enableBusinessRetry(boolean enableBusinessRetry) { + this.enableBusinessRetry = enableBusinessRetry; + return this; + } + public Builder userAgent(String userAgent) { this.userAgent = userAgent; return this; diff --git a/src/main/java/io/github/doocs/im/util/HttpUtil.java b/src/main/java/io/github/doocs/im/util/HttpUtil.java index b64a560..18df2a2 100644 --- a/src/main/java/io/github/doocs/im/util/HttpUtil.java +++ b/src/main/java/io/github/doocs/im/util/HttpUtil.java @@ -1,6 +1,7 @@ package io.github.doocs.im.util; import io.github.doocs.im.ClientConfiguration; +import io.github.doocs.im.model.response.GenericResult; import okhttp3.*; import java.io.IOException; @@ -31,7 +32,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())) + .addInterceptor(new RetryInterceptor(DEFAULT_CONFIG.getMaxRetries(), DEFAULT_CONFIG.getRetryIntervalMs(), null, DEFAULT_CONFIG.isEnableBusinessRetry())) .build(); private HttpUtil() { @@ -58,7 +59,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())) + .addInterceptor(new RetryInterceptor(cfg.getMaxRetries(), cfg.getRetryIntervalMs(), null, DEFAULT_CONFIG.isEnableBusinessRetry())) .build()); } @@ -104,10 +105,14 @@ class RetryInterceptor implements Interceptor { private static final int MAX_DELAY_MS = 10000; private final int maxRetries; private final long retryIntervalMs; + private final Set businessRetryCodes; + private final boolean enableBusinessRetry; - public RetryInterceptor(int maxRetries, long retryIntervalMs) { + public RetryInterceptor(int maxRetries, long retryIntervalMs, Set businessRetryCodes, boolean enableBusinessRetry) { this.maxRetries = maxRetries; this.retryIntervalMs = retryIntervalMs; + this.businessRetryCodes = businessRetryCodes; + this.enableBusinessRetry = enableBusinessRetry; } @Override @@ -153,7 +158,13 @@ private boolean shouldRetry(Response response) { if (code >= 500 && code < 600) { return true; } - return RETRYABLE_STATUS_CODES.contains(code); + if (RETRYABLE_STATUS_CODES.contains(code)) { + return true; + } + if (enableBusinessRetry) { + return shouldRetryBasedOnBusinessCode(response); + } + return false; } private void waitForRetry(int attempt) { @@ -164,4 +175,18 @@ private void waitForRetry(int attempt) { Thread.currentThread().interrupt(); } } + + private boolean shouldRetryBasedOnBusinessCode(Response response) { + try { + if (businessRetryCodes == null || businessRetryCodes.isEmpty()) { + 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 false; + } + } } From f0eb3db13d9a8ef5a260a1ca8d789a7d6acc73a0 Mon Sep 17 00:00:00 2001 From: subo <13162733306@163.com> Date: Tue, 13 May 2025 20:41:29 +0800 Subject: [PATCH 2/5] feat: New business code retry variable field --- .../github/doocs/im/ClientConfiguration.java | 38 ++++++++++++++++++- .../io/github/doocs/im/util/HttpUtil.java | 4 +- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/github/doocs/im/ClientConfiguration.java b/src/main/java/io/github/doocs/im/ClientConfiguration.java index b3e0790..0f7768f 100644 --- a/src/main/java/io/github/doocs/im/ClientConfiguration.java +++ b/src/main/java/io/github/doocs/im/ClientConfiguration.java @@ -3,7 +3,10 @@ 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; /** * 客户端配置类 @@ -36,6 +39,17 @@ public class ClientConfiguration { * 默认业务错误码重试开关关闭 */ public static final boolean DEFAULT_ENABLE_BUSINESS_RETRY = false; + + /** + * 默认重试错误码 + */ + public static final Set DEFAULT_BUSINESS_RETRY_CODES = + Collections.unmodifiableSet(new HashSet() {{ + add(10002); + add(20004); + add(20005); + }}); + /** * 默认超时时间(毫秒) */ @@ -63,6 +77,7 @@ public class ClientConfiguration { private long expireTime = DEFAULT_EXPIRE_TIME; private boolean autoRenewSig = DEFAULT_RENEW_SIG; private boolean enableBusinessRetry = DEFAULT_ENABLE_BUSINESS_RETRY; + private Set businessRetryCodes = DEFAULT_BUSINESS_RETRY_CODES; private String userAgent = DEFAULT_USER_AGENT; private ConnectionPool connectionPool = DEFAULT_CONNECTION_POOL; @@ -70,7 +85,7 @@ public ClientConfiguration() { } public ClientConfiguration(int maxRetries, long retryIntervalMs, long connectTimeout, long readTimeout, long writeTimeout, - long callTimeout, long expireTime, boolean autoRenewSig, boolean enableBusinessRetry, + long callTimeout, long expireTime, boolean autoRenewSig, boolean enableBusinessRetry, Set businessRetryCodes, String userAgent, ConnectionPool connectionPool) { if (connectionPool == null) { connectionPool = DEFAULT_CONNECTION_POOL; @@ -84,6 +99,7 @@ public ClientConfiguration(int maxRetries, long retryIntervalMs, long connectTim this.expireTime = expireTime; this.autoRenewSig = autoRenewSig; this.enableBusinessRetry = enableBusinessRetry; + this.businessRetryCodes = businessRetryCodes; this.userAgent = userAgent; this.connectionPool = connectionPool; } @@ -98,6 +114,7 @@ private ClientConfiguration(Builder builder) { this.expireTime = builder.expireTime; this.autoRenewSig = builder.autoRenewSig; this.enableBusinessRetry = builder.enableBusinessRetry; + this.businessRetryCodes = builder.businessRetryCodes; this.userAgent = builder.userAgent; this.connectionPool = builder.connectionPool; } @@ -178,6 +195,14 @@ public void setEnableBusinessRetry(boolean enableBusinessRetry) { this.enableBusinessRetry = enableBusinessRetry; } + public Set getBusinessRetryCodes() { + return businessRetryCodes; + } + + public void setBusinessRetryCodes(Set businessRetryCodes) { + this.businessRetryCodes = businessRetryCodes; + } + public String getUserAgent() { return userAgent; } @@ -234,6 +259,9 @@ public boolean equals(Object o) { if (enableBusinessRetry != that.enableBusinessRetry) { return false; } + if (businessRetryCodes != that.businessRetryCodes) { + return false; + } if (!userAgent.equals(that.userAgent)) { return false; } @@ -242,7 +270,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return Objects.hash(maxRetries, retryIntervalMs, connectTimeout, readTimeout, writeTimeout, callTimeout, expireTime, autoRenewSig, enableBusinessRetry, userAgent, connectionPool); + return Objects.hash(maxRetries, retryIntervalMs, connectTimeout, readTimeout, writeTimeout, callTimeout, expireTime, autoRenewSig, enableBusinessRetry, businessRetryCodes, userAgent, connectionPool); } public static final class Builder { @@ -255,6 +283,7 @@ public static final class Builder { private long expireTime = DEFAULT_EXPIRE_TIME; private boolean autoRenewSig = DEFAULT_RENEW_SIG; private boolean enableBusinessRetry = DEFAULT_ENABLE_BUSINESS_RETRY; + private Set businessRetryCodes = DEFAULT_BUSINESS_RETRY_CODES; private String userAgent = DEFAULT_USER_AGENT; private ConnectionPool connectionPool = DEFAULT_CONNECTION_POOL; @@ -310,6 +339,11 @@ public Builder enableBusinessRetry(boolean enableBusinessRetry) { return this; } + public Builder businessRetryCodes(Set businessRetryCodes) { + this.businessRetryCodes = businessRetryCodes; + return this; + } + public Builder userAgent(String userAgent) { this.userAgent = userAgent; return this; diff --git a/src/main/java/io/github/doocs/im/util/HttpUtil.java b/src/main/java/io/github/doocs/im/util/HttpUtil.java index 18df2a2..3eb04e2 100644 --- a/src/main/java/io/github/doocs/im/util/HttpUtil.java +++ b/src/main/java/io/github/doocs/im/util/HttpUtil.java @@ -32,7 +32,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(), null, DEFAULT_CONFIG.isEnableBusinessRetry())) + .addInterceptor(new RetryInterceptor(DEFAULT_CONFIG.getMaxRetries(), DEFAULT_CONFIG.getRetryIntervalMs(), DEFAULT_CONFIG.getBusinessRetryCodes(), DEFAULT_CONFIG.isEnableBusinessRetry())) .build(); private HttpUtil() { @@ -59,7 +59,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(), null, DEFAULT_CONFIG.isEnableBusinessRetry())) + .addInterceptor(new RetryInterceptor(cfg.getMaxRetries(), cfg.getRetryIntervalMs(), DEFAULT_CONFIG.getBusinessRetryCodes(), DEFAULT_CONFIG.isEnableBusinessRetry())) .build()); } From a4ad31ee30e6452409bd7b9ff40af029b5669922 Mon Sep 17 00:00:00 2001 From: subo <13162733306@163.com> Date: Fri, 16 May 2025 17:38:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=201=E3=80=81Repair=20the=20logic=20ba?= =?UTF-8?q?sed=20on=20the=20business=20code=20retry=20=20=20=20=20=20=202?= =?UTF-8?q?=E3=80=81Fixed=20entity=20class=20serialization=20issue=20=20?= =?UTF-8?q?=20=20=20=20=203=E3=80=81Add=20retry=20logic=20unit=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../im/model/response/BaseGenericResult.java | 44 +++ .../io/github/doocs/im/util/HttpUtil.java | 93 +++--- .../doocs/im/util/RetryInterceptorTest.java | 313 ++++++++++++++++++ 3 files changed, 404 insertions(+), 46 deletions(-) create mode 100644 src/main/java/io/github/doocs/im/model/response/BaseGenericResult.java create mode 100644 src/test/java/io/github/doocs/im/util/RetryInterceptorTest.java diff --git a/src/main/java/io/github/doocs/im/model/response/BaseGenericResult.java b/src/main/java/io/github/doocs/im/model/response/BaseGenericResult.java new file mode 100644 index 0000000..2ad6009 --- /dev/null +++ b/src/main/java/io/github/doocs/im/model/response/BaseGenericResult.java @@ -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(); + } +} diff --git a/src/main/java/io/github/doocs/im/util/HttpUtil.java b/src/main/java/io/github/doocs/im/util/HttpUtil.java index 6e4d54e..ba69962 100644 --- a/src/main/java/io/github/doocs/im/util/HttpUtil.java +++ b/src/main/java/io/github/doocs/im/util/HttpUtil.java @@ -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.*; @@ -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() { @@ -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(), DEFAULT_CONFIG.getBusinessRetryCodes(), DEFAULT_CONFIG.isEnableBusinessRetry(), BaseGenericResult.class)) .build()); } @@ -103,16 +104,20 @@ 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 businessRetryCodes; private final boolean enableBusinessRetry; + private final Class resultType; + private final Random random = new Random(); - public RetryInterceptor(int maxRetries, long retryIntervalMs, Set businessRetryCodes, boolean enableBusinessRetry) { - this.maxRetries = maxRetries; + public RetryInterceptor(int maxRetries, long retryIntervalMs, Set businessRetryCodes, boolean enableBusinessRetry, Class resultType) { + this.maxRetries = maxRetries + 1; this.retryIntervalMs = retryIntervalMs; - this.businessRetryCodes = businessRetryCodes; + this.businessRetryCodes = Collections.unmodifiableSet(new HashSet<>(Objects.requireNonNull(businessRetryCodes)));; this.enableBusinessRetry = enableBusinessRetry; + this.resultType = Objects.requireNonNull(resultType); } @Override @@ -120,72 +125,68 @@ 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; } } diff --git a/src/test/java/io/github/doocs/im/util/RetryInterceptorTest.java b/src/test/java/io/github/doocs/im/util/RetryInterceptorTest.java new file mode 100644 index 0000000..980c3f5 --- /dev/null +++ b/src/test/java/io/github/doocs/im/util/RetryInterceptorTest.java @@ -0,0 +1,313 @@ +package io.github.doocs.im.util; + +import io.github.doocs.im.model.response.BaseGenericResult; +import okhttp3.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +public class RetryInterceptorTest { + + private RetryInterceptor interceptor; + private TestChain testChain; + private Request request; + + @BeforeEach + public void setup() { + // Use virtual URL + request = new Request.Builder() + .url("http://example.com") + .build(); + + interceptor = new RetryInterceptor( + 2, 1000, + Collections.unmodifiableSet(new java.util.HashSet() {{ + add(10002); + add(20004); + add(20005); + }}), + true, + BaseGenericResult.class + ); + + // Constructor using default timeout parameter + testChain = new TestChain(request); + } + + // Custom Test Chain Implementation + private static class TestChain implements Interceptor.Chain { + private final Request request; + private final AtomicInteger callCount = new AtomicInteger(0); + private final java.util.function.IntFunction responseSupplier; + private final java.util.function.IntFunction exceptionSupplier; + private final int connectTimeoutMs; + private final int readTimeoutMs; + + public TestChain(Request request) { + this(request, i -> null, i -> null, 10000, 10000); + } + + public TestChain(Request request, + java.util.function.IntFunction responseSupplier, + java.util.function.IntFunction exceptionSupplier, + int connectTimeoutMs, + int readTimeoutMs) { + this.request = request; + this.responseSupplier = responseSupplier; + this.exceptionSupplier = exceptionSupplier; + this.connectTimeoutMs = connectTimeoutMs; + this.readTimeoutMs = readTimeoutMs; + } + + @Override + public Request request() { + return request; + } + + @Override + public Response proceed(Request request) throws IOException { + int count = callCount.incrementAndGet(); + IOException exception = exceptionSupplier.apply(count); + if (exception != null) { + throw exception; + } + Response response = responseSupplier.apply(count); + if (response == null) { + throw new AssertionError("No response configured for call " + count); + } + return response; + } + + @Override + public Connection connection() { + // No actual connection is required during testing, return null + return null; + } + + @Override + public Call call() { + // No actual Call object is required during testing, return null + return null; + } + + @Override + public int connectTimeoutMillis() { + return connectTimeoutMs; + } + + @Override + public Interceptor.Chain withConnectTimeout(int timeout, java.util.concurrent.TimeUnit unit) { + return new TestChain( + request, + responseSupplier, + exceptionSupplier, + (int) unit.toMillis(timeout), + readTimeoutMs + ); + } + + @Override + public int readTimeoutMillis() { + return readTimeoutMs; + } + + @Override + public Interceptor.Chain withReadTimeout(int timeout, java.util.concurrent.TimeUnit unit) { + return new TestChain( + request, + responseSupplier, + exceptionSupplier, + connectTimeoutMs, + (int) unit.toMillis(timeout) + ); + } + + @Override + public int writeTimeoutMillis() { + // Default write timeout + return 10000; + } + + @Override + public Interceptor.Chain withWriteTimeout(int timeout, java.util.concurrent.TimeUnit unit) { + // No need to implement during testing + return this; + } + + public int getCallCount() { + return callCount.get(); + } + } + + //---------------- Tool method: Create simulated response ----------------// + private Response createResponse(int code, String body) { + return new Response.Builder() + .request(request) + .protocol(Protocol.HTTP_1_1) + .code(code) + .message("") + .body(ResponseBody.create( + body, + MediaType.get("application/json") + )) + .build(); + } + + //---------------- Normal response test ----------------// + @Test + public void testNormalResponse_Http200() throws IOException { + // Simulate a successful response with a constructor that includes all parameters + testChain = new TestChain( + request, + i -> createResponse(200, "{ \"ErrorCode\": 0 }"), + i -> null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(200, response.code()); + // Verify a single request + assertEquals(1, testChain.getCallCount()); + } + + //---------------- HTTP error retry test ----------------// + @Test + public void testHttpRetry_SuccessAfterRetries() throws IOException { + // Use counters to control retry logic, including a constructor with all parameters + testChain = new TestChain( + request, + i -> i <= 2 ? createResponse(500, "") : createResponse(200, "{ \"ErrorCode\": 0 }"), + i -> null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(200, response.code()); + assertEquals(3, testChain.getCallCount()); // Verify the number of retries + } + + //---------------- Business error retry test ----------------// + @Test + public void testBusinessRetry_SuccessAfterRetries() throws IOException { + // Constructor containing all parameters + testChain = new TestChain( + request, + i -> createResponse(200, i < 3 ? "{ \"ErrorCode\": 10002 }" : "{ \"ErrorCode\": 0 }"), + i -> null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(200, response.code()); + assertEquals(3, testChain.getCallCount()); + } + + //---------------- Abnormal scenario testing ----------------// + @Test + public void testHttpRetry_MaxRetriesExceeded() { + // Constructor containing all parameters + testChain = new TestChain( + request, + i -> createResponse(500, ""), + i -> null, + 10000, + 10000 + ); + + assertThrows(IOException.class, () -> interceptor.intercept(testChain)); + assertEquals(3, testChain.getCallCount()); // Verify the number of retries + } + + //---------------- Timeout retry test ----------------// + @Test + public void testConnectTimeoutRetry() throws IOException { + // Simulate the first two connection timeouts and the third one is successful + testChain = new TestChain( + request, + i -> i > 2 ? createResponse(200, "{}") : null, + i -> i <= 2 ? new SocketTimeoutException("Connect timed out") : null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(200, response.code()); + assertEquals(3, testChain.getCallCount()); + } + + //---------------- ReadTime retry test ----------------// + @Test + public void testReadTimeoutRetry() throws IOException { + // Simulate the first two reads timed out, the third one succeeded + testChain = new TestChain( + request, + i -> i > 2 ? createResponse(200, "{}") : null, + i -> i <= 2 ? new SocketTimeoutException("Read timed out") : null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(200, response.code()); + assertEquals(3, testChain.getCallCount()); + } + + @Test + public void testConnectTimeoutExceedMaxRetries() { + // Simulate connection timeout every time, exceeding the maximum retry count + testChain = new TestChain( + request, + i -> null, + i -> new SocketTimeoutException("Connect timed out"), + 10000, + 10000 + ); + + assertThrows(SocketTimeoutException.class, () -> interceptor.intercept(testChain)); + assertEquals(3, testChain.getCallCount()); + } + + //---------------- Similar adjustments to other test cases (example)----------------// + @Test + public void testIOExceptionRetry() throws IOException { + // Constructor containing all parameters + testChain = new TestChain( + request, + i -> i > 2 ? createResponse(200, "{}") : null, + i -> i <= 2 ? new IOException("Simulate network errors") : null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(200, response.code()); + assertEquals(3, testChain.getCallCount()); + } + + @Test + public void testMaxRetriesZero() throws IOException { + interceptor = new RetryInterceptor(0, 1000, Collections.emptySet(), false, BaseGenericResult.class); + + // Constructor containing all parameters + testChain = new TestChain( + request, + i -> createResponse(500, ""), + i -> null, + 10000, + 10000 + ); + + Response response = interceptor.intercept(testChain); + assertEquals(500, response.code()); + assertEquals(1, testChain.getCallCount()); + } +} From a8c445e581afad11e28103253800cfc7c9085182 Mon Sep 17 00:00:00 2001 From: subo <13162733306@163.com> Date: Fri, 16 May 2025 17:46:00 +0800 Subject: [PATCH 4/5] feat: Business code retry assignment repair --- src/main/java/io/github/doocs/im/util/HttpUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/github/doocs/im/util/HttpUtil.java b/src/main/java/io/github/doocs/im/util/HttpUtil.java index ba69962..394b6bd 100644 --- a/src/main/java/io/github/doocs/im/util/HttpUtil.java +++ b/src/main/java/io/github/doocs/im/util/HttpUtil.java @@ -60,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(), BaseGenericResult.class)) + .addInterceptor(new RetryInterceptor(cfg.getMaxRetries(), cfg.getRetryIntervalMs(), config.getBusinessRetryCodes(), config.isEnableBusinessRetry(), BaseGenericResult.class)) .build()); } From 7b3edf283f094b3ede252fbfb6c23bcf04f0e323 Mon Sep 17 00:00:00 2001 From: subo <13162733306@163.com> Date: Fri, 16 May 2025 18:25:04 +0800 Subject: [PATCH 5/5] feat: Remove businessRetroCodes flight verification --- src/main/java/io/github/doocs/im/util/HttpUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/github/doocs/im/util/HttpUtil.java b/src/main/java/io/github/doocs/im/util/HttpUtil.java index 394b6bd..0fbac2c 100644 --- a/src/main/java/io/github/doocs/im/util/HttpUtil.java +++ b/src/main/java/io/github/doocs/im/util/HttpUtil.java @@ -60,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(), config.getBusinessRetryCodes(), config.isEnableBusinessRetry(), BaseGenericResult.class)) + .addInterceptor(new RetryInterceptor(cfg.getMaxRetries(), cfg.getRetryIntervalMs(), cfg.getBusinessRetryCodes(), cfg.isEnableBusinessRetry(), BaseGenericResult.class)) .build()); } @@ -115,7 +115,7 @@ class RetryInterceptor implements Interceptor { public RetryInterceptor(int maxRetries, long retryIntervalMs, Set businessRetryCodes, boolean enableBusinessRetry, Class resultType) { this.maxRetries = maxRetries + 1; this.retryIntervalMs = retryIntervalMs; - this.businessRetryCodes = Collections.unmodifiableSet(new HashSet<>(Objects.requireNonNull(businessRetryCodes)));; + this.businessRetryCodes = businessRetryCodes; this.enableBusinessRetry = enableBusinessRetry; this.resultType = Objects.requireNonNull(resultType); }