From 2f0f5d5990531bd536985dda3512defe7c585d26 Mon Sep 17 00:00:00 2001 From: zth9 Date: Wed, 7 May 2025 14:42:16 +0800 Subject: [PATCH] feat: Support retry in open api client --- CHANGES.md | 1 + .../openapi/client/ApolloOpenApiClient.java | 27 +++++++- ...ApolloStandardHttpRequestRetryHandler.java | 69 +++++++++++++++++++ .../client/extend/IdempotentHttpMethod.java | 40 +++++++++++ .../ApolloOpenApiClientIntegrationTest.java | 3 + 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/ApolloStandardHttpRequestRetryHandler.java create mode 100644 apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/IdempotentHttpMethod.java diff --git a/CHANGES.md b/CHANGES.md index 455a28d9..718d9634 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Apollo Java 2.5.0 * [Feature Provide a new open APl to return the organization list](https://github.com/apolloconfig/apollo-java/pull/102) * [Feature Added a new feature to get instance count by namespace.](https://github.com/apolloconfig/apollo-java/pull/103) +* [Feature Support retry in open api client.](https://github.com/apolloconfig/apollo-java/pull/105) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/5?closed=1) diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java index 65ab6a01..4b41378d 100644 --- a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClient.java @@ -17,6 +17,8 @@ package com.ctrip.framework.apollo.openapi.client; import com.ctrip.framework.apollo.openapi.client.constant.ApolloOpenApiConstants; +import com.ctrip.framework.apollo.openapi.client.extend.ApolloStandardHttpRequestRetryHandler; +import com.ctrip.framework.apollo.openapi.client.extend.IdempotentHttpMethod; import com.ctrip.framework.apollo.openapi.client.service.AppOpenApiService; import com.ctrip.framework.apollo.openapi.client.service.ClusterOpenApiService; import com.ctrip.framework.apollo.openapi.client.service.ItemOpenApiService; @@ -55,10 +57,13 @@ public class ApolloOpenApiClient { private final InstanceOpenApiService instanceService; private static final Gson GSON = new GsonBuilder().setDateFormat(ApolloOpenApiConstants.JSON_DATE_FORMAT).create(); - private ApolloOpenApiClient(String portalUrl, String token, RequestConfig requestConfig) { + private ApolloOpenApiClient(String portalUrl, String token, RequestConfig requestConfig, + int retryCount, IdempotentHttpMethod[] idempotentHttpMethods) { this.portalUrl = portalUrl; this.token = token; CloseableHttpClient client = HttpClients.custom().setDefaultRequestConfig(requestConfig) + .setRetryHandler(retryCount > 0 ? + new ApolloStandardHttpRequestRetryHandler(retryCount, idempotentHttpMethods) : null) .setDefaultHeaders(Lists.newArrayList(new BasicHeader("Authorization", token))).build(); String baseUrl = this.portalUrl + ApolloOpenApiConstants.OPEN_API_V1_PREFIX; @@ -273,6 +278,8 @@ public static class ApolloOpenApiClientBuilder { private String token; private int connectTimeout = -1; private int readTimeout = -1; + private int retryCount = -1; + private IdempotentHttpMethod[] idempotentHttpMethods; /** * @param portalUrl The apollo portal url, e.g http://localhost:8070 @@ -306,6 +313,22 @@ public ApolloOpenApiClientBuilder withReadTimeout(int readTimeout) { return this; } + /** + * @param retryCount execute retry when an exception occurs, default no retry + */ + public ApolloOpenApiClientBuilder withRetryCount(int retryCount) { + this.retryCount = retryCount; + return this; + } + + /** + * @param idempotentHttpMethods idempotent HTTP methods will directly execute retries when exception + */ + public ApolloOpenApiClientBuilder withIdempotentHttpMethods(IdempotentHttpMethod... idempotentHttpMethods) { + this.idempotentHttpMethods = idempotentHttpMethods; + return this; + } + public ApolloOpenApiClient build() { Preconditions.checkArgument(!Strings.isNullOrEmpty(portalUrl), "Portal url should not be null or empty!"); Preconditions.checkArgument(portalUrl.startsWith("http://") || portalUrl.startsWith("https://"), "Portal url should start with http:// or https://" ); @@ -322,7 +345,7 @@ public ApolloOpenApiClient build() { RequestConfig requestConfig = RequestConfig.custom().setConnectTimeout(connectTimeout) .setSocketTimeout(readTimeout).build(); - return new ApolloOpenApiClient(portalUrl, token, requestConfig); + return new ApolloOpenApiClient(portalUrl, token, requestConfig, retryCount, idempotentHttpMethods); } } } diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/ApolloStandardHttpRequestRetryHandler.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/ApolloStandardHttpRequestRetryHandler.java new file mode 100644 index 00000000..669f9a91 --- /dev/null +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/ApolloStandardHttpRequestRetryHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.openapi.client.extend; + +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import org.apache.http.HttpRequest; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; + +import javax.net.ssl.SSLException; +import java.net.ConnectException; +import java.net.NoRouteToHostException; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.Locale; + +/** + * @author zth9 + * @date 2025-05-07 + */ +public class ApolloStandardHttpRequestRetryHandler extends DefaultHttpRequestRetryHandler { + + private final Set idempotentMethods; + + public ApolloStandardHttpRequestRetryHandler(int retryCount, IdempotentHttpMethod[] httpMethods) { + super(retryCount, false, Arrays.asList( + UnknownHostException.class, + ConnectException.class, + NoRouteToHostException.class, + SSLException.class)); + this.idempotentMethods = new HashSet<>(); + if (httpMethods == null || httpMethods.length == 0) { + // default set safe idempotent http method + httpMethods = IdempotentHttpMethod.safe(); + } + for (IdempotentHttpMethod httpMethod : httpMethods) { + if (httpMethod == null) { + continue; + } + idempotentMethods.add(httpMethod.name()); + } + } + + @Override + protected boolean handleAsIdempotent(final HttpRequest request) { + String method = request.getRequestLine().getMethod().toUpperCase(Locale.ROOT); + return idempotentMethods.contains(method); + } +} diff --git a/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/IdempotentHttpMethod.java b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/IdempotentHttpMethod.java new file mode 100644 index 00000000..a4190cc2 --- /dev/null +++ b/apollo-openapi/src/main/java/com/ctrip/framework/apollo/openapi/client/extend/IdempotentHttpMethod.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Apollo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package com.ctrip.framework.apollo.openapi.client.extend; + +/** + * @author zth9 + * @date 2025-05-11 + */ +public enum IdempotentHttpMethod { + GET, HEAD, PUT, DELETE, OPTIONS, TRACE; + + /** + * Usually, these methods are idempotent + */ + public static IdempotentHttpMethod[] safe() { + return new IdempotentHttpMethod[]{GET, HEAD, OPTIONS, TRACE}; + } + + /** + * Standard HTTP idempotent method. While PUT and DELETE are technically idempotent, repeated + * requests can yield different responses—such as a 404 on a second delete + */ + public static IdempotentHttpMethod[] standard() { + return new IdempotentHttpMethod[]{GET, HEAD, PUT, DELETE, OPTIONS, TRACE}; + } +} diff --git a/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java index 9d931ba2..1a43ca21 100644 --- a/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java +++ b/apollo-openapi/src/test/java/com/ctrip/framework/apollo/openapi/client/ApolloOpenApiClientIntegrationTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.ctrip.framework.apollo.openapi.client.extend.IdempotentHttpMethod; import com.ctrip.framework.apollo.openapi.dto.NamespaceReleaseDTO; import com.ctrip.framework.apollo.openapi.dto.OpenAppDTO; import com.ctrip.framework.apollo.openapi.dto.OpenAppNamespaceDTO; @@ -63,6 +64,8 @@ ApolloOpenApiClient newClient() { .withToken(someToken) .withReadTimeout(2000 * 1000) .withConnectTimeout(2000 * 1000) + .withRetryCount(3) + .withIdempotentHttpMethods(IdempotentHttpMethod.safe()) .build(); }